feat(navbar): navbar menu implemented

This commit is contained in:
Junior Garcia 2023-04-19 00:35:03 -03:00
parent 5be0df4ab3
commit 20ffb55b23
21 changed files with 995 additions and 86 deletions

View File

@ -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": {

View File

@ -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";

View File

@ -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:

View File

@ -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}

View 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;

View 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;

View 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},
},
},
};

View 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;

View File

@ -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>
);

View File

@ -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>;

View File

@ -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? Well 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,
};

View File

@ -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,

View File

@ -37,7 +37,7 @@ const base: SemanticBaseColors = {
},
dark: {
background: {
DEFAULT: "#000000",
DEFAULT: "#0B0B0C",
},
foreground: {
DEFAULT: "#ECEDEE",

View File

@ -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",

View File

@ -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",
},
},
],
});

View File

@ -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)",

View 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).

View 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"
]
}
}

View 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,
}),
};
}

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.json",
"include": ["src", "index.ts"]
}

49
pnpm-lock.yaml generated
View File

@ -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: