mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(navbar): navbar menu implemented
This commit is contained in:
parent
5be0df4ab3
commit
20ffb55b23
@ -34,25 +34,33 @@
|
||||
"postpack": "clean-package restore"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"framer-motion": ">=6.2.8",
|
||||
"react": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/dom-utils": "workspace:*",
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/theme": "workspace:*",
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/dom-utils": "workspace:*",
|
||||
"@nextui-org/use-aria-toggle-button": "workspace:*",
|
||||
"@nextui-org/use-scroll-position": "workspace:*",
|
||||
"@react-aria/utils": "^3.16.0"
|
||||
"@react-aria/focus": "^3.12.0",
|
||||
"@react-aria/interactions": "^3.15.0",
|
||||
"@react-aria/overlays": "^3.14.0",
|
||||
"@react-aria/utils": "^3.16.0",
|
||||
"@react-stately/toggle": "^3.5.1",
|
||||
"@react-stately/utils": "^3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextui-org/link": "workspace:*",
|
||||
"@nextui-org/avatar": "workspace:*",
|
||||
"@nextui-org/dropdown": "workspace:*",
|
||||
"@nextui-org/button": "workspace:*",
|
||||
"@nextui-org/dropdown": "workspace:*",
|
||||
"@nextui-org/input": "workspace:*",
|
||||
"react-lorem-component": "0.13.0",
|
||||
"@nextui-org/link": "workspace:*",
|
||||
"@nextui-org/shared-icons": "workspace:*",
|
||||
"clean-package": "2.2.0",
|
||||
"react": "^18.0.0"
|
||||
"react": "^18.0.0",
|
||||
"react-lorem-component": "0.13.0"
|
||||
},
|
||||
"clean-package": "../../../clean-package.config.json",
|
||||
"tsup": {
|
||||
|
||||
@ -3,6 +3,9 @@ export type {NavbarProps} from "./navbar";
|
||||
export type {NavbarBrandProps} from "./navbar-brand";
|
||||
export type {NavbarContentProps} from "./navbar-content";
|
||||
export type {NavbarItemProps} from "./navbar-item";
|
||||
export type {NavbarMenuToggleProps} from "./navbar-menu-toggle";
|
||||
export type {NavbarMenuProps} from "./navbar-menu";
|
||||
export type {NavbarMenuItemProps} from "./navbar-menu-item";
|
||||
|
||||
// export hooks
|
||||
export {useNavbar} from "./use-navbar";
|
||||
@ -15,3 +18,6 @@ export {default as Navbar} from "./navbar";
|
||||
export {default as NavbarBrand} from "./navbar-brand";
|
||||
export {default as NavbarContent} from "./navbar-content";
|
||||
export {default as NavbarItem} from "./navbar-item";
|
||||
export {default as NavbarMenuToggle} from "./navbar-menu-toggle";
|
||||
export {default as NavbarMenu} from "./navbar-menu";
|
||||
export {default as NavbarMenuItem} from "./navbar-menu-item";
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import {createContext} from "@nextui-org/shared-utils";
|
||||
|
||||
import {ContextType} from "./use-navbar";
|
||||
import {UseNavbarReturn} from "./use-navbar";
|
||||
|
||||
export const [NavbarProvider, useNavbarContext] = createContext<ContextType>({
|
||||
export const [NavbarProvider, useNavbarContext] = createContext<UseNavbarReturn>({
|
||||
name: "NavbarContext",
|
||||
strict: true,
|
||||
errorMessage:
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {useId} from "react";
|
||||
|
||||
import {useNavbarContext} from "./navbar-context";
|
||||
|
||||
@ -20,8 +19,6 @@ const NavbarItem = forwardRef<NavbarItemProps, "li">((props, ref) => {
|
||||
const Component = as || "li";
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const itemId = useId();
|
||||
|
||||
const {slots, classNames} = useNavbarContext();
|
||||
|
||||
const styles = clsx(classNames?.item, className);
|
||||
@ -31,7 +28,6 @@ const NavbarItem = forwardRef<NavbarItemProps, "li">((props, ref) => {
|
||||
ref={domRef}
|
||||
className={slots.item?.({class: styles})}
|
||||
data-active={dataAttr(isActive)}
|
||||
id={itemId}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
|
||||
59
packages/components/navbar/src/navbar-menu-item.tsx
Normal file
59
packages/components/navbar/src/navbar-menu-item.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {motion, HTMLMotionProps} from "framer-motion";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
|
||||
import {menuItemVariants} from "./navbar-menu-transitions";
|
||||
import {useNavbarContext} from "./navbar-context";
|
||||
|
||||
export interface NavbarMenuItemProps extends HTMLNextUIProps<"li"> {
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Whether to disable the animation.
|
||||
*/
|
||||
disableAnimation?: boolean;
|
||||
/**
|
||||
* The props to modify the framer motion animation. Use the `variants` API to create your own animation.
|
||||
*/
|
||||
motionProps?: HTMLMotionProps<"li">;
|
||||
}
|
||||
|
||||
const NavbarMenuItem = forwardRef<NavbarMenuItemProps, "li">((props, ref) => {
|
||||
const {className, children, disableAnimation, motionProps, ...otherProps} = props;
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {slots, isMenuOpen, classNames} = useNavbarContext();
|
||||
|
||||
const styles = clsx(classNames?.menuItem, className);
|
||||
|
||||
if (disableAnimation) {
|
||||
return (
|
||||
<li
|
||||
ref={domRef}
|
||||
className={slots.menuItem?.({class: styles})}
|
||||
data-open={dataAttr(isMenuOpen)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.li
|
||||
ref={domRef}
|
||||
className={slots.menuItem?.({class: styles})}
|
||||
data-open={dataAttr(isMenuOpen)}
|
||||
variants={menuItemVariants}
|
||||
{...mergeProps(motionProps, otherProps)}
|
||||
>
|
||||
{children}
|
||||
</motion.li>
|
||||
);
|
||||
});
|
||||
|
||||
NavbarMenuItem.displayName = "NextUI.NavbarMenuItem";
|
||||
|
||||
export default NavbarMenuItem;
|
||||
101
packages/components/navbar/src/navbar-menu-toggle.tsx
Normal file
101
packages/components/navbar/src/navbar-menu-toggle.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import {AriaToggleButtonProps, useAriaToggleButton} from "@nextui-org/use-aria-toggle-button";
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {useToggleState} from "@react-stately/toggle";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
import {useMemo, ReactNode} from "react";
|
||||
|
||||
import {useNavbarContext} from "./navbar-context";
|
||||
|
||||
export type ToggleIconProps = {
|
||||
/**
|
||||
* The current open status.
|
||||
*/
|
||||
isOpen?: boolean;
|
||||
};
|
||||
|
||||
export interface Props extends Omit<HTMLNextUIProps<"button">, keyof AriaToggleButtonProps> {
|
||||
/**
|
||||
* The value of the input element, used when submitting an HTML form. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefvalue).
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* Text to display for screen readers.
|
||||
* @default open/close navigation menu
|
||||
*/
|
||||
srOnlyText?: string;
|
||||
/**
|
||||
* The icon to display.
|
||||
*/
|
||||
icon?: ReactNode | ((props: ToggleIconProps) => ReactNode) | null;
|
||||
}
|
||||
|
||||
export type NavbarMenuToggleProps = Props & AriaToggleButtonProps;
|
||||
|
||||
const NavbarMenuToggle = forwardRef<NavbarMenuToggleProps, "button">((props, ref) => {
|
||||
const {
|
||||
as,
|
||||
icon,
|
||||
className,
|
||||
onChange,
|
||||
autoFocus,
|
||||
srOnlyText: srOnlyTextProp,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const Component = as || "button";
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {slots, classNames, setIsMenuOpen} = useNavbarContext();
|
||||
|
||||
const handleChange = (isOpen: boolean) => {
|
||||
onChange?.(isOpen);
|
||||
setIsMenuOpen(isOpen);
|
||||
};
|
||||
|
||||
const state = useToggleState({...otherProps, onChange: handleChange});
|
||||
|
||||
const {buttonProps, isPressed} = useAriaToggleButton(props, state, domRef);
|
||||
const {isFocusVisible, focusProps} = useFocusRing({autoFocus});
|
||||
const {isHovered, hoverProps} = useHover({});
|
||||
|
||||
const toggleStyles = clsx(classNames?.toggle, className);
|
||||
|
||||
const child = useMemo(() => {
|
||||
if (typeof icon === "function") {
|
||||
return icon({isOpen: state.isSelected});
|
||||
}
|
||||
|
||||
return icon || <span className={slots.toggleIcon({class: classNames?.toggleIcon})} />;
|
||||
}, [icon, slots.toggleIcon, classNames?.toggleIcon]);
|
||||
|
||||
const srOnlyText = useMemo(() => {
|
||||
if (srOnlyTextProp) {
|
||||
return srOnlyTextProp;
|
||||
}
|
||||
|
||||
return state.isSelected ? "close navigation menu" : "open navigation menu";
|
||||
}, [srOnlyTextProp, state.isSelected]);
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={domRef}
|
||||
className={slots.toggle?.({class: toggleStyles})}
|
||||
data-focus-visible={dataAttr(isFocusVisible)}
|
||||
data-hover={dataAttr(isHovered)}
|
||||
data-open={dataAttr(state.isSelected)}
|
||||
data-pressed={dataAttr(isPressed)}
|
||||
{...mergeProps(buttonProps, focusProps, hoverProps, otherProps)}
|
||||
>
|
||||
<span className={slots.srOnly()}>{srOnlyText}</span>
|
||||
{child}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
NavbarMenuToggle.displayName = "NextUI.NavbarMenuToggle";
|
||||
|
||||
export default NavbarMenuToggle;
|
||||
27
packages/components/navbar/src/navbar-menu-transitions.ts
Normal file
27
packages/components/navbar/src/navbar-menu-transitions.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {Variants} from "framer-motion";
|
||||
|
||||
export const menuVariants: Variants = {
|
||||
open: {
|
||||
transition: {staggerChildren: 0.07, delayChildren: 0.15},
|
||||
},
|
||||
closed: {
|
||||
transition: {staggerChildren: 0.05, staggerDirection: -1},
|
||||
},
|
||||
};
|
||||
|
||||
export const menuItemVariants: Variants = {
|
||||
open: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
y: {stiffness: 1000, velocity: -100},
|
||||
},
|
||||
},
|
||||
closed: {
|
||||
y: 50,
|
||||
opacity: 0,
|
||||
transition: {
|
||||
y: {stiffness: 1000},
|
||||
},
|
||||
},
|
||||
};
|
||||
67
packages/components/navbar/src/navbar-menu.tsx
Normal file
67
packages/components/navbar/src/navbar-menu.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {HTMLMotionProps, motion} from "framer-motion";
|
||||
import {usePreventScroll} from "@react-aria/overlays";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
|
||||
import {menuVariants} from "./navbar-menu-transitions";
|
||||
import {useNavbarContext} from "./navbar-context";
|
||||
|
||||
export interface NavbarMenuProps extends HTMLNextUIProps<"ul"> {
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Whether to disable the animation.
|
||||
* @default false
|
||||
*/
|
||||
disableAnimation?: boolean;
|
||||
/**
|
||||
* The props to modify the framer motion animation. Use the `variants` API to create your own animation.
|
||||
*/
|
||||
motionProps?: HTMLMotionProps<"ul">;
|
||||
}
|
||||
|
||||
const NavbarMenu = forwardRef<NavbarMenuProps, "ul">((props, ref) => {
|
||||
const {className, children, disableAnimation = false, motionProps, ...otherProps} = props;
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {slots, isMenuOpen, classNames} = useNavbarContext();
|
||||
|
||||
const styles = clsx(classNames?.menu, className);
|
||||
|
||||
usePreventScroll({
|
||||
isDisabled: !isMenuOpen,
|
||||
});
|
||||
|
||||
if (disableAnimation) {
|
||||
return (
|
||||
<ul
|
||||
ref={domRef}
|
||||
className={slots.menu?.({class: styles})}
|
||||
data-open={dataAttr(isMenuOpen)}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.ul
|
||||
ref={domRef}
|
||||
animate={isMenuOpen ? "open" : "closed"}
|
||||
className={slots.menu?.({class: styles})}
|
||||
data-open={dataAttr(isMenuOpen)}
|
||||
initial={false}
|
||||
variants={menuVariants}
|
||||
{...mergeProps(motionProps, otherProps)}
|
||||
>
|
||||
{children}
|
||||
</motion.ul>
|
||||
);
|
||||
});
|
||||
|
||||
NavbarMenu.displayName = "NextUI.NavbarMenu";
|
||||
|
||||
export default NavbarMenu;
|
||||
@ -1,21 +1,28 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {pickChildren} from "@nextui-org/shared-utils";
|
||||
|
||||
import {UseNavbarProps, useNavbar} from "./use-navbar";
|
||||
import {NavbarProvider} from "./navbar-context";
|
||||
import NavbarMenu from "./navbar-menu";
|
||||
|
||||
export interface NavbarProps extends Omit<UseNavbarProps, "ref"> {
|
||||
export interface NavbarProps extends Omit<UseNavbarProps, "ref" | "hideOnScroll"> {
|
||||
children?: React.ReactNode | React.ReactNode[];
|
||||
}
|
||||
|
||||
const Navbar = forwardRef<NavbarProps, "div">((props, ref) => {
|
||||
const {children, ...otherProps} = props;
|
||||
|
||||
const {Component, getBaseProps, getWrapperProps, context} = useNavbar({ref, ...otherProps});
|
||||
const context = useNavbar({ref, ...otherProps});
|
||||
|
||||
const Component = context.Component;
|
||||
|
||||
const [childrenWithoutMenu, menu] = pickChildren(children, NavbarMenu);
|
||||
|
||||
return (
|
||||
<NavbarProvider value={context}>
|
||||
<Component {...getBaseProps()}>
|
||||
<header {...getWrapperProps()}>{children}</header>
|
||||
<Component {...context.getBaseProps()}>
|
||||
<header {...context.getWrapperProps()}>{childrenWithoutMenu}</header>
|
||||
{menu}
|
||||
</Component>
|
||||
</NavbarProvider>
|
||||
);
|
||||
|
||||
@ -3,10 +3,11 @@ import type {NavbarVariantProps, SlotsToClasses, NavbarSlots} from "@nextui-org/
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
import {navbar} from "@nextui-org/theme";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx, ReactRef} from "@nextui-org/shared-utils";
|
||||
import {clsx, dataAttr, ReactRef} from "@nextui-org/shared-utils";
|
||||
import {useMemo, useState} from "react";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {useScrollPosition} from "@nextui-org/use-scroll-position";
|
||||
import {useControlledState} from "@react-stately/utils";
|
||||
|
||||
export interface UseNavbarProps extends HTMLNextUIProps<"nav", NavbarVariantProps> {
|
||||
/**
|
||||
@ -19,6 +20,21 @@ export interface UseNavbarProps extends HTMLNextUIProps<"nav", NavbarVariantProp
|
||||
* @default `window`
|
||||
*/
|
||||
parentRef?: React.RefObject<HTMLElement>;
|
||||
/**
|
||||
* The height of the navbar.
|
||||
* @default "4rem" (64px)
|
||||
*/
|
||||
height?: number | string;
|
||||
/**
|
||||
* Whether the menu is open.
|
||||
* @default false
|
||||
*/
|
||||
isMenuOpen?: boolean;
|
||||
/**
|
||||
* Whether the menu should be open by default.
|
||||
* @default false
|
||||
*/
|
||||
isMenuDefaultOpen?: boolean;
|
||||
/**
|
||||
* Whether the navbar should hide on scroll or not.
|
||||
* @default false
|
||||
@ -29,6 +45,12 @@ export interface UseNavbarProps extends HTMLNextUIProps<"nav", NavbarVariantProp
|
||||
* @default false
|
||||
*/
|
||||
disableScrollHandler?: boolean;
|
||||
/**
|
||||
* The event handler for the menu open state.
|
||||
* @param isOpen boolean
|
||||
* @returns void
|
||||
*/
|
||||
onMenuOpenChange?: (isOpen: boolean | undefined) => void;
|
||||
/**
|
||||
* The scroll event handler for the navbar. The event fires when the navbar parent element is scrolled.
|
||||
* it only works if `disableScrollHandler` is set to `false` or `shouldHideOnScroll` is set to `true`.
|
||||
@ -44,17 +66,16 @@ export interface UseNavbarProps extends HTMLNextUIProps<"nav", NavbarVariantProp
|
||||
* base:"base-classes",
|
||||
* wrapper: "wrapper-classes",
|
||||
* brand: "brand-classes",
|
||||
* content: "content-classes",
|
||||
* item: "item-classes",
|
||||
* menu: "menu-classes", // the one that appears when the menu is open
|
||||
* menuItem: "menu-item-classes",
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
classNames?: SlotsToClasses<NavbarSlots>;
|
||||
}
|
||||
|
||||
export type ContextType = {
|
||||
slots: ReturnType<typeof navbar>;
|
||||
classNames?: SlotsToClasses<NavbarSlots>;
|
||||
};
|
||||
|
||||
export function useNavbar(originalProps: UseNavbarProps) {
|
||||
const [props, variantProps] = mapPropsVariants(originalProps, navbar.variantKeys);
|
||||
|
||||
@ -62,9 +83,13 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
ref,
|
||||
as,
|
||||
parentRef,
|
||||
height = "4rem",
|
||||
shouldHideOnScroll = false,
|
||||
disableScrollHandler = false,
|
||||
onScrollPositionChange,
|
||||
isMenuOpen: isMenuOpenProp,
|
||||
isMenuDefaultOpen = false,
|
||||
onMenuOpenChange = () => {},
|
||||
className,
|
||||
classNames,
|
||||
...otherProps
|
||||
@ -74,22 +99,23 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useControlledState<boolean | undefined>(
|
||||
isMenuOpenProp,
|
||||
isMenuDefaultOpen || false,
|
||||
onMenuOpenChange,
|
||||
);
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
navbar({
|
||||
...variantProps,
|
||||
position: isSticky ? "sticky" : originalProps?.position,
|
||||
hideOnScroll: shouldHideOnScroll,
|
||||
}),
|
||||
[...Object.values(variantProps), isSticky],
|
||||
[...Object.values(variantProps), shouldHideOnScroll],
|
||||
);
|
||||
|
||||
const context: ContextType = {
|
||||
slots,
|
||||
classNames,
|
||||
};
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
|
||||
useScrollPosition({
|
||||
@ -98,7 +124,7 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
callback: ({prevPos, currPos}) => {
|
||||
onScrollPositionChange?.(currPos.y);
|
||||
if (shouldHideOnScroll) {
|
||||
setIsSticky((prev) => {
|
||||
setIsHidden((prev) => {
|
||||
const next = currPos.y > prevPos.y;
|
||||
|
||||
return next !== prev ? next : prev;
|
||||
@ -109,16 +135,32 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
|
||||
const getBaseProps: PropGetter = (props = {}) => ({
|
||||
...mergeProps(otherProps, props),
|
||||
"data-hide": dataAttr(isHidden),
|
||||
"data-menu-open": dataAttr(isMenuOpen),
|
||||
ref: domRef,
|
||||
className: slots.base({class: clsx(baseStyles, props?.className)}),
|
||||
style: {
|
||||
"--navbar-height": height,
|
||||
...props?.style,
|
||||
},
|
||||
});
|
||||
|
||||
const getWrapperProps: PropGetter = (props = {}) => ({
|
||||
...props,
|
||||
"data-menu-open": dataAttr(isMenuOpen),
|
||||
className: slots.wrapper({class: clsx(classNames?.wrapper, props?.className)}),
|
||||
});
|
||||
|
||||
return {Component, slots, domRef, context, getBaseProps, getWrapperProps};
|
||||
return {
|
||||
Component,
|
||||
slots,
|
||||
domRef,
|
||||
classNames,
|
||||
isMenuOpen,
|
||||
setIsMenuOpen,
|
||||
getBaseProps,
|
||||
getWrapperProps,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseNavbarReturn = ReturnType<typeof useNavbar>;
|
||||
|
||||
@ -4,8 +4,19 @@ import {navbar} from "@nextui-org/theme";
|
||||
import {Link} from "@nextui-org/link";
|
||||
import {Button} from "@nextui-org/button";
|
||||
import Lorem from "react-lorem-component";
|
||||
import {Dropdown, DropdownTrigger, DropdownMenu, DropdownItem} from "@nextui-org/dropdown";
|
||||
import {ChevronDown, Lock, Activity, Flash, Server, TagUser, Scale} from "@nextui-org/shared-icons";
|
||||
|
||||
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, NavbarProps} from "../src";
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
NavbarMenuToggle,
|
||||
NavbarProps,
|
||||
} from "../src";
|
||||
|
||||
export default {
|
||||
title: "Components/Navbar",
|
||||
@ -57,7 +68,7 @@ const App = React.forwardRef(({children}: any, ref: any) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="max-w-[920px] max-h-[600px] overflow-x-hidden overflow-y-scroll shadow-md relative border border-neutral"
|
||||
className="max-w-[90%] sm:max-w-[80%] max-h-[90vh] overflow-x-hidden overflow-y-scroll shadow-md relative border border-neutral"
|
||||
>
|
||||
{children}
|
||||
<div className="flex flex-col gap-4 px-10 mt-8">
|
||||
@ -72,45 +83,272 @@ const App = React.forwardRef(({children}: any, ref: any) => {
|
||||
|
||||
App.displayName = "App";
|
||||
|
||||
const Template: ComponentStory<typeof Navbar> = (args: NavbarProps) => (
|
||||
<App>
|
||||
<Navbar {...args}>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold hidden sm:block text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden sm:flex">
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Features
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive as={Link} href="#">
|
||||
Customers
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Integrations
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Pricing
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Company
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent>
|
||||
<NavbarItem as={Link} href="#">
|
||||
Login
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
</App>
|
||||
);
|
||||
const Template: ComponentStory<typeof Navbar> = (args: NavbarProps) => {
|
||||
// for hide on scroll cases
|
||||
const parentRef = React.useRef(null);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
return (
|
||||
<App ref={parentRef}>
|
||||
<Navbar {...args} parentRef={parentRef}>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold hidden sm:block text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden md:flex">
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Features
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive as={Link} href="#">
|
||||
Customers
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Integrations
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Pricing
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} className="hidden lg:block" color="foreground" href="#">
|
||||
Company
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent>
|
||||
<NavbarItem as={Link} href="#">
|
||||
Login
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
</App>
|
||||
);
|
||||
};
|
||||
|
||||
const WithMenuTemplate: ComponentStory<typeof Navbar> = (args: NavbarProps) => {
|
||||
const parentRef = React.useRef(null);
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
|
||||
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Log Out",
|
||||
];
|
||||
|
||||
return (
|
||||
<App ref={parentRef}>
|
||||
<Navbar
|
||||
isBordered
|
||||
parentRef={parentRef}
|
||||
position="sticky"
|
||||
onMenuOpenChange={setIsMenuOpen}
|
||||
{...args}
|
||||
>
|
||||
<NavbarContent>
|
||||
<NavbarMenuToggle
|
||||
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
|
||||
className="sm:hidden"
|
||||
/>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold hidden sm:block text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
<NavbarContent className="hidden sm:flex">
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Features
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive as={Link} href="#">
|
||||
Customers
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Integrations
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Pricing
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} className="hidden lg:block" color="foreground" href="#">
|
||||
Company
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent>
|
||||
<NavbarItem as={Link} href="#">
|
||||
Login
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarMenu disableAnimation>
|
||||
{menuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
color={
|
||||
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
</App>
|
||||
);
|
||||
};
|
||||
|
||||
const WithDropdownTemplate: ComponentStory<typeof Navbar> = (args: NavbarProps) => {
|
||||
const icons = {
|
||||
chevron: <ChevronDown fill="currentColor" size={16} />,
|
||||
scale: <Scale className="text-warning" fill="currentColor" size={30} />,
|
||||
lock: <Lock className="text-success" fill="currentColor" size={30} />,
|
||||
activity: <Activity className="text-secondary" fill="currentColor" size={30} />,
|
||||
flash: <Flash className="text-primary" fill="currentColor" size={30} />,
|
||||
server: <Server className="text-success" fill="currentColor" size={30} />,
|
||||
user: <TagUser className="text-danger" fill="currentColor" size={30} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<App>
|
||||
<Navbar isBordered position="sticky" {...args}>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold hidden sm:block text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden gap-0 sm:flex">
|
||||
<Dropdown>
|
||||
<NavbarItem>
|
||||
<DropdownTrigger>
|
||||
<Button endIcon={icons.chevron} radius="full" variant="light">
|
||||
Features
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
</NavbarItem>
|
||||
<DropdownMenu
|
||||
aria-label="ACME features"
|
||||
className="w-[340px]"
|
||||
itemStyles={{
|
||||
base: "gap-4",
|
||||
wrapper: "py-3",
|
||||
}}
|
||||
>
|
||||
<DropdownItem
|
||||
key="autoscaling"
|
||||
description="ACME scales apps to meet user demand, automagically, based on load."
|
||||
startContent={icons.scale}
|
||||
>
|
||||
Autoscaling
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="safe_and_sound"
|
||||
description="A secure mission control, without the policy headache. Permissions, 2FA, and more."
|
||||
startContent={icons.lock}
|
||||
>
|
||||
Safe and Sound
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="usage_metrics"
|
||||
description="Real-time metrics to debug issues. Slow query added? We’ll show you exactly where."
|
||||
startContent={icons.activity}
|
||||
>
|
||||
Usage Metrics
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="production_ready"
|
||||
description="ACME runs on ACME, join us and others serving requests at web scale."
|
||||
startContent={icons.flash}
|
||||
>
|
||||
Production Ready
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="99_uptime"
|
||||
description="Applications stay on the grid with high availability and high uptime guarantees."
|
||||
startContent={icons.server}
|
||||
>
|
||||
+99% Uptime
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="supreme_support"
|
||||
description="Overcome any challenge with a supporting team ready to respond."
|
||||
startContent={icons.user}
|
||||
>
|
||||
+Supreme Support
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<NavbarItem isActive as={Link} className="px-4" href="#">
|
||||
Customers
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} className="px-4" color="foreground" href="#">
|
||||
Integrations
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} className="px-4" color="foreground" href="#">
|
||||
Pricing
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} className="hidden px-4 lg:block" color="foreground" href="#">
|
||||
Company
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent>
|
||||
<NavbarItem as={Link} href="#">
|
||||
Login
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button as={Link} color="primary" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
</App>
|
||||
);
|
||||
};
|
||||
|
||||
export const Static = Template.bind({});
|
||||
Static.args = {
|
||||
...defaultProps,
|
||||
position: "static",
|
||||
};
|
||||
|
||||
export const Sticky = Template.bind({});
|
||||
Sticky.args = {
|
||||
...defaultProps,
|
||||
position: "sticky",
|
||||
};
|
||||
|
||||
export const Floating = Template.bind({});
|
||||
Floating.args = {
|
||||
...defaultProps,
|
||||
position: "floating",
|
||||
};
|
||||
|
||||
export const HideOnScroll = Template.bind({});
|
||||
HideOnScroll.args = {
|
||||
...defaultProps,
|
||||
position: "sticky",
|
||||
shouldHideOnScroll: true,
|
||||
};
|
||||
|
||||
export const WithMenu = WithMenuTemplate.bind({});
|
||||
WithMenu.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const WithDropdown = WithDropdownTemplate.bind({});
|
||||
|
||||
WithDropdown.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
AriaPopoverProps,
|
||||
useOverlayTrigger,
|
||||
usePopover as useReactAriaPopover,
|
||||
usePreventScroll,
|
||||
} from "@react-aria/overlays";
|
||||
import {OverlayTriggerProps} from "@react-types/overlays";
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
@ -47,6 +48,11 @@ export interface Props extends HTMLNextUIProps<"div"> {
|
||||
* @default false
|
||||
*/
|
||||
showArrow?: boolean;
|
||||
/**
|
||||
* Whether the scroll event should be blocked when the overlay is open.
|
||||
* @default true
|
||||
*/
|
||||
shouldBlockScroll?: boolean;
|
||||
/**
|
||||
* Type of overlay that is opened by the trigger.
|
||||
*/
|
||||
@ -95,6 +101,7 @@ export function usePopover(originalProps: UsePopoverProps) {
|
||||
onOpenChange,
|
||||
shouldFlip = true,
|
||||
containerPadding = 12,
|
||||
shouldBlockScroll = true,
|
||||
placement: placementProp = "top",
|
||||
triggerType = "dialog",
|
||||
showArrow = false,
|
||||
@ -137,6 +144,10 @@ export function usePopover(originalProps: UsePopoverProps) {
|
||||
|
||||
const state = stateProp || innerState;
|
||||
|
||||
usePreventScroll({
|
||||
isDisabled: !state.isOpen || !shouldBlockScroll,
|
||||
});
|
||||
|
||||
const {popoverProps, underlayProps, arrowProps, placement} = useReactAriaPopover(
|
||||
{
|
||||
triggerRef,
|
||||
|
||||
@ -37,7 +37,7 @@ const base: SemanticBaseColors = {
|
||||
},
|
||||
dark: {
|
||||
background: {
|
||||
DEFAULT: "#000000",
|
||||
DEFAULT: "#0B0B0C",
|
||||
},
|
||||
foreground: {
|
||||
DEFAULT: "#ECEDEE",
|
||||
|
||||
@ -51,7 +51,7 @@ const dropdownItem = tv({
|
||||
],
|
||||
wrapper: "w-full flex flex-col items-start justify-center",
|
||||
title: "flex-1",
|
||||
description: ["text-xs", "text-neutral-500", "truncate", "group-hover:text-current"],
|
||||
description: ["text-xs", "w-full", "text-neutral-500", "group-hover:text-current"],
|
||||
selectedIcon: ["text-inherit", "w-3", "h-3", "flex-shrink-0"],
|
||||
shortcut: [
|
||||
"px-1",
|
||||
|
||||
@ -7,17 +7,41 @@ import {tv} from "tailwind-variants";
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const {base, wrapper, brand, content, item} = navbar({...})
|
||||
* const {
|
||||
* base,
|
||||
* wrapper,
|
||||
* toggle,
|
||||
* srOnly,
|
||||
* toggleIcon,
|
||||
* brand,
|
||||
* content,
|
||||
* item,
|
||||
* menu,
|
||||
* menuItem
|
||||
* } = navbar({...})
|
||||
*
|
||||
* <nav className={base()}>
|
||||
* <div className={wrapper()}>
|
||||
* <nav className={base()} style={{ "--navbar-height": "4rem" }}>
|
||||
* <header className={wrapper()}>
|
||||
* <button className={toggle()}>
|
||||
* <span className={srOnly()}>Open/Close menu</span>
|
||||
* <span className={toggleIcon()} aria-hidden="true"/>
|
||||
* </button>
|
||||
* <div className={brand()}>Brand</div>
|
||||
* <ul className={content()}>
|
||||
* <li className={item()}>Item 1</li>
|
||||
* <li className={item()}>Item 2</li>
|
||||
* <li className={item()}>Item 3</li>
|
||||
* </ul>
|
||||
* </div>
|
||||
* <ul className={content()}>
|
||||
* <li className={item()}>Login</li>
|
||||
* <li className={item()}>Sign Up</li>
|
||||
* </ul>
|
||||
* </header>
|
||||
* <ul className={menu()}>
|
||||
* <li className={menuItem()}>Item 1</li>
|
||||
* <li className={menuItem()}>Item 2</li>
|
||||
* <li className={menuItem()}>Item 3</li>
|
||||
* </ul>
|
||||
* </nav>
|
||||
* ```
|
||||
*/
|
||||
@ -33,7 +57,7 @@ const navbar = tv({
|
||||
"justify-center",
|
||||
"border-b",
|
||||
"border-neutral-100",
|
||||
"shadow-lg",
|
||||
"shadow-md",
|
||||
],
|
||||
wrapper: [
|
||||
"flex",
|
||||
@ -42,9 +66,63 @@ const navbar = tv({
|
||||
"items-center",
|
||||
"justify-between",
|
||||
"w-full",
|
||||
"h-16",
|
||||
"h-[var(--navbar-height)]",
|
||||
"px-6",
|
||||
],
|
||||
toggle: [
|
||||
"group",
|
||||
"flex",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"w-6",
|
||||
"h-10",
|
||||
"outline-none",
|
||||
"rounded-sm",
|
||||
// focus ring
|
||||
"data-[focus-visible=true]:outline-none",
|
||||
"data-[focus-visible=true]:ring-2",
|
||||
"data-[focus-visible=true]:ring-primary",
|
||||
"data-[focus-visible=true]:ring-offset-2",
|
||||
"data-[focus-visible=true]:ring-offset-background",
|
||||
"data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
],
|
||||
srOnly: ["sr-only"],
|
||||
toggleIcon: [
|
||||
"w-full",
|
||||
"h-full",
|
||||
"pointer-events-none",
|
||||
"flex",
|
||||
"flex-col",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"text-foreground",
|
||||
"group-data-[pressed=true]:opacity-70",
|
||||
"transition-opacity",
|
||||
// before - first line
|
||||
"before:content-['']",
|
||||
"before:block",
|
||||
"before:h-px",
|
||||
"before:w-6",
|
||||
"before:bg-current",
|
||||
"before:transition-transform",
|
||||
"before:duration-150",
|
||||
"before:-translate-y-1",
|
||||
"before:rotate-0",
|
||||
"group-data-[open=true]:before:translate-y-px",
|
||||
"group-data-[open=true]:before:rotate-45",
|
||||
// after - second line
|
||||
"after:content-['']",
|
||||
"after:block",
|
||||
"after:h-px",
|
||||
"after:w-6",
|
||||
"after:bg-current",
|
||||
"after:transition-transform",
|
||||
"after:duration-150",
|
||||
"after:translate-y-1",
|
||||
"after:rotate-0",
|
||||
"group-data-[open=true]:after:translate-y-0",
|
||||
"group-data-[open=true]:after:-rotate-45",
|
||||
],
|
||||
brand: [
|
||||
"flex",
|
||||
"flex-row",
|
||||
@ -65,6 +143,24 @@ const navbar = tv({
|
||||
// active
|
||||
"data-[active=true]:font-semibold",
|
||||
],
|
||||
menu: [
|
||||
"hidden",
|
||||
"px-6",
|
||||
"pt-4",
|
||||
"absolute",
|
||||
"max-w-full",
|
||||
"top-[calc(var(--navbar-height)_+_1px)]",
|
||||
"h-[calc(100vh_-_var(--navbar-height)_-_1px)]",
|
||||
"inset-x-0",
|
||||
"bottom-0",
|
||||
"w-screen",
|
||||
"bg-background",
|
||||
"data-[open=true]:flex",
|
||||
"flex-col",
|
||||
"gap-3",
|
||||
"overflow-y-auto",
|
||||
],
|
||||
menuItem: ["text-lg"],
|
||||
},
|
||||
variants: {
|
||||
position: {
|
||||
@ -74,7 +170,8 @@ const navbar = tv({
|
||||
sticky: {},
|
||||
floating: {
|
||||
base: "shadow-none border-b-0",
|
||||
wrapper: "mt-4 mx-8 shadow-lg border border-neutral-100 rounded-xl",
|
||||
wrapper: "mt-4 mx-8 shadow-md border border-neutral-100 rounded-xl",
|
||||
menu: "mt-5 mx-8 border border-neutral-100 rounded-xl max-w-[calc(100%_-_4rem)]",
|
||||
},
|
||||
},
|
||||
maxWidth: {
|
||||
@ -97,6 +194,19 @@ const navbar = tv({
|
||||
wrapper: "max-w-full",
|
||||
},
|
||||
},
|
||||
hideOnScroll: {
|
||||
true: {
|
||||
base: [
|
||||
"sticky",
|
||||
"top-0",
|
||||
"inset-x-0",
|
||||
"transition-transform",
|
||||
"!duration-400",
|
||||
"translate-y-0",
|
||||
"data-[hide=true]:-translate-y-full",
|
||||
],
|
||||
},
|
||||
},
|
||||
isBordered: {
|
||||
true: {},
|
||||
},
|
||||
@ -104,9 +214,7 @@ const navbar = tv({
|
||||
false: {
|
||||
base: "bg-background",
|
||||
},
|
||||
true: {
|
||||
base: "backdrop-blur-xl backdrop-saturate-200 bg-background/50",
|
||||
},
|
||||
true: {},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@ -121,6 +229,23 @@ const navbar = tv({
|
||||
base: "sticky top-0 inset-x-0",
|
||||
},
|
||||
},
|
||||
{
|
||||
isBlurred: true,
|
||||
position: ["static", "sticky"],
|
||||
class: {
|
||||
base:
|
||||
"backdrop-blur-xl backdrop-saturate-200 bg-background/50 data-[menu-open=true]:bg-background",
|
||||
},
|
||||
},
|
||||
{
|
||||
isBlurred: true,
|
||||
position: "floating",
|
||||
class: {
|
||||
base: "bg-gradient-to-b from-background to-background/50",
|
||||
wrapper:
|
||||
"backdrop-blur-xl backdrop-saturate-200 bg-background/50 data-[menu-open=true]:bg-background",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@ -205,6 +205,7 @@ const corePlugin = (config: ConfigObject | ConfigFunction = {}, defaultTheme: De
|
||||
transitionDuration: {
|
||||
0: "0ms",
|
||||
250: "250ms",
|
||||
400: "400ms",
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
"soft-spring": "cubic-bezier(0.155, 1.105, 0.295, 1.12)",
|
||||
|
||||
24
packages/hooks/use-aria-toggle-button/README.md
Normal file
24
packages/hooks/use-aria-toggle-button/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# @nextui-org/use-aria-toggle-button
|
||||
|
||||
A Quick description of the component
|
||||
|
||||
> This is an internal utility, not intended for public usage.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add @nextui-org/use-aria-toggle-button
|
||||
# or
|
||||
npm i @nextui-org/use-aria-toggle-button
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
Yes please! See the
|
||||
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
|
||||
for details.
|
||||
|
||||
## Licence
|
||||
|
||||
This project is licensed under the terms of the
|
||||
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).
|
||||
59
packages/hooks/use-aria-toggle-button/package.json
Normal file
59
packages/hooks/use-aria-toggle-button/package.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@nextui-org/use-aria-toggle-button",
|
||||
"version": "2.0.0-beta.1",
|
||||
"description": "Internal hook to handle button a11y and events, this is based on react-aria button hook but without the onClick warning",
|
||||
"keywords": [
|
||||
"use-aria-toggle-button"
|
||||
],
|
||||
"author": "Junior Garcia <jrgarciadev@gmail.com>",
|
||||
"homepage": "https://nextui.org",
|
||||
"license": "MIT",
|
||||
"main": "src/index.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nextui-org/nextui.git",
|
||||
"directory": "packages/hooks/use-aria-toggle-button"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/nextui-org/nextui/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src --dts",
|
||||
"build:fast": "tsup src",
|
||||
"dev": "yarn build:fast -- --watch",
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepack": "clean-package",
|
||||
"postpack": "clean-package restore"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/use-aria-button": "workspace:*",
|
||||
"@react-stately/toggle": "^3.5.1",
|
||||
"@react-aria/utils": "^3.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-types/shared": "^3.18.0",
|
||||
"@react-types/button": "^3.7.2",
|
||||
"clean-package": "2.2.0",
|
||||
"react": "^18.0.0"
|
||||
},
|
||||
"clean-package": "../../../clean-package.config.json",
|
||||
"tsup": {
|
||||
"clean": true,
|
||||
"target": "es2019",
|
||||
"format": [
|
||||
"cjs",
|
||||
"esm"
|
||||
]
|
||||
}
|
||||
}
|
||||
85
packages/hooks/use-aria-toggle-button/src/index.ts
Normal file
85
packages/hooks/use-aria-toggle-button/src/index.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2020 Adobe. All rights reserved.
|
||||
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License. You may obtain a copy
|
||||
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under
|
||||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
||||
* OF ANY KIND, either express or implied. See the License for the specific language
|
||||
* governing permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AnchorHTMLAttributes,
|
||||
ButtonHTMLAttributes,
|
||||
ElementType,
|
||||
HTMLAttributes,
|
||||
InputHTMLAttributes,
|
||||
RefObject,
|
||||
} from "react";
|
||||
import {AriaToggleButtonProps} from "@react-types/button";
|
||||
import {chain} from "@react-aria/utils";
|
||||
import {DOMAttributes} from "@react-types/shared";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {ToggleState} from "@react-stately/toggle";
|
||||
import {ButtonAria, useAriaButton} from "@nextui-org/use-aria-button";
|
||||
|
||||
export type {AriaToggleButtonProps};
|
||||
|
||||
// Order with overrides is important: 'button' should be default
|
||||
export function useAriaToggleButton(
|
||||
props: AriaToggleButtonProps<"button">,
|
||||
state: ToggleState,
|
||||
ref: RefObject<HTMLButtonElement>,
|
||||
): ButtonAria<ButtonHTMLAttributes<HTMLButtonElement>>;
|
||||
export function useAriaToggleButton(
|
||||
props: AriaToggleButtonProps<"a">,
|
||||
state: ToggleState,
|
||||
ref: RefObject<HTMLAnchorElement>,
|
||||
): ButtonAria<AnchorHTMLAttributes<HTMLAnchorElement>>;
|
||||
export function useAriaToggleButton(
|
||||
props: AriaToggleButtonProps<"div">,
|
||||
state: ToggleState,
|
||||
ref: RefObject<HTMLDivElement>,
|
||||
): ButtonAria<HTMLAttributes<HTMLDivElement>>;
|
||||
export function useAriaToggleButton(
|
||||
props: AriaToggleButtonProps<"input">,
|
||||
state: ToggleState,
|
||||
ref: RefObject<HTMLInputElement>,
|
||||
): ButtonAria<InputHTMLAttributes<HTMLInputElement>>;
|
||||
export function useAriaToggleButton(
|
||||
props: AriaToggleButtonProps<"span">,
|
||||
state: ToggleState,
|
||||
ref: RefObject<HTMLSpanElement>,
|
||||
): ButtonAria<HTMLAttributes<HTMLSpanElement>>;
|
||||
export function useAriaToggleButton(
|
||||
props: AriaToggleButtonProps<ElementType>,
|
||||
state: ToggleState,
|
||||
ref: RefObject<Element>,
|
||||
): ButtonAria<DOMAttributes>;
|
||||
/**
|
||||
* Provides the behavior and accessibility implementation for a toggle button component.
|
||||
* ToggleButtons allow users to toggle a selection on or off, for example switching between two states or modes.
|
||||
*/
|
||||
export function useAriaToggleButton(
|
||||
props: AriaToggleButtonProps<ElementType>,
|
||||
state: ToggleState,
|
||||
ref: RefObject<any>,
|
||||
): ButtonAria<HTMLAttributes<any>> {
|
||||
const {isSelected} = state;
|
||||
const {isPressed, buttonProps} = useAriaButton(
|
||||
{
|
||||
...props,
|
||||
onPress: chain(state.toggle, props.onPress),
|
||||
},
|
||||
ref,
|
||||
);
|
||||
|
||||
return {
|
||||
isPressed,
|
||||
buttonProps: mergeProps(buttonProps, {
|
||||
"aria-pressed": isSelected,
|
||||
}),
|
||||
};
|
||||
}
|
||||
4
packages/hooks/use-aria-toggle-button/tsconfig.json
Normal file
4
packages/hooks/use-aria-toggle-button/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": ["src", "index.ts"]
|
||||
}
|
||||
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
@ -1091,12 +1091,33 @@ importers:
|
||||
'@nextui-org/theme':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/theme
|
||||
'@nextui-org/use-aria-toggle-button':
|
||||
specifier: workspace:*
|
||||
version: link:../../hooks/use-aria-toggle-button
|
||||
'@nextui-org/use-scroll-position':
|
||||
specifier: workspace:*
|
||||
version: link:../../hooks/use-scroll-position
|
||||
'@react-aria/focus':
|
||||
specifier: ^3.12.0
|
||||
version: 3.12.0(react@18.2.0)
|
||||
'@react-aria/interactions':
|
||||
specifier: ^3.15.0
|
||||
version: 3.15.0(react@18.2.0)
|
||||
'@react-aria/overlays':
|
||||
specifier: ^3.14.0
|
||||
version: 3.14.0(react-dom@18.2.0)(react@18.2.0)
|
||||
'@react-aria/utils':
|
||||
specifier: ^3.16.0
|
||||
version: 3.16.0(react@18.2.0)
|
||||
'@react-stately/toggle':
|
||||
specifier: ^3.5.1
|
||||
version: 3.5.1(react@18.2.0)
|
||||
'@react-stately/utils':
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0(react@18.2.0)
|
||||
framer-motion:
|
||||
specifier: '>=6.2.8'
|
||||
version: 10.11.2(react-dom@18.2.0)(react@18.2.0)
|
||||
devDependencies:
|
||||
'@nextui-org/avatar':
|
||||
specifier: workspace:*
|
||||
@ -1113,6 +1134,9 @@ importers:
|
||||
'@nextui-org/link':
|
||||
specifier: workspace:*
|
||||
version: link:../link
|
||||
'@nextui-org/shared-icons':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-icons
|
||||
clean-package:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0
|
||||
@ -1761,6 +1785,31 @@ importers:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
|
||||
packages/hooks/use-aria-toggle-button:
|
||||
dependencies:
|
||||
'@nextui-org/use-aria-button':
|
||||
specifier: workspace:*
|
||||
version: link:../use-aria-button
|
||||
'@react-aria/utils':
|
||||
specifier: ^3.16.0
|
||||
version: 3.16.0(react@18.2.0)
|
||||
'@react-stately/toggle':
|
||||
specifier: ^3.5.1
|
||||
version: 3.5.1(react@18.2.0)
|
||||
devDependencies:
|
||||
'@react-types/button':
|
||||
specifier: ^3.7.2
|
||||
version: 3.7.2(react@18.2.0)
|
||||
'@react-types/shared':
|
||||
specifier: ^3.18.0
|
||||
version: 3.18.0(react@18.2.0)
|
||||
clean-package:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
|
||||
packages/hooks/use-clipboard:
|
||||
devDependencies:
|
||||
clean-package:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user