mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(navbar): open/close animation implemented in framer, tests added
This commit is contained in:
parent
20ffb55b23
commit
adf1bf6caa
@ -1,7 +1,15 @@
|
||||
import * as React from "react";
|
||||
import {render} from "@testing-library/react";
|
||||
import {act, render} from "@testing-library/react";
|
||||
|
||||
import {Navbar} from "../src";
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarItem,
|
||||
NavbarContent,
|
||||
NavbarMenuToggle,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
} from "../src";
|
||||
|
||||
describe("Navbar", () => {
|
||||
it("should render correctly", () => {
|
||||
@ -16,4 +24,108 @@ describe("Navbar", () => {
|
||||
render(<Navbar ref={ref} />);
|
||||
expect(ref.current).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should render correctly with brand", () => {
|
||||
const wrapper = render(
|
||||
<Navbar>
|
||||
<NavbarBrand data-testid="navbar-test">ACME</NavbarBrand>
|
||||
</Navbar>,
|
||||
);
|
||||
|
||||
expect(wrapper.getByTestId("navbar-test")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render correctly content children", () => {
|
||||
const wrapper = render(
|
||||
<Navbar>
|
||||
<NavbarContent data-testid="navbar-content-test">
|
||||
<NavbarItem>Dashboard</NavbarItem>
|
||||
<NavbarItem>Team</NavbarItem>
|
||||
<NavbarItem>Deployments</NavbarItem>
|
||||
<NavbarItem>Activity</NavbarItem>
|
||||
<NavbarItem>Settings</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>,
|
||||
);
|
||||
|
||||
const navbarContent = wrapper.getByTestId("navbar-content-test");
|
||||
|
||||
expect(navbarContent.children.length).toBe(5);
|
||||
});
|
||||
|
||||
it("should render correctly with menu", () => {
|
||||
const items = ["item1", "item2", "item3", "item4", "item5"];
|
||||
|
||||
const wrapper = render(
|
||||
<Navbar data-testid="navbar-test">
|
||||
<NavbarMenuToggle data-testid="navbar-toggle-test" />
|
||||
<NavbarContent data-testid="navbar-content-test">
|
||||
<NavbarItem>Dashboard</NavbarItem>
|
||||
<NavbarItem>Team</NavbarItem>
|
||||
<NavbarItem>Deployments</NavbarItem>
|
||||
<NavbarItem>Activity</NavbarItem>
|
||||
<NavbarItem>Settings</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarMenu data-testid="navbar-menu-test">
|
||||
{items.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>{item}</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>,
|
||||
);
|
||||
|
||||
const toggle = wrapper.getByTestId("navbar-toggle-test");
|
||||
|
||||
act(() => {
|
||||
toggle.click();
|
||||
});
|
||||
|
||||
const menu = wrapper.getByTestId("navbar-menu-test");
|
||||
|
||||
expect(menu.children.length).toBe(items.length);
|
||||
});
|
||||
|
||||
it("should call on onChange when toggle is clicked", () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = render(
|
||||
<Navbar data-testid="navbar-test">
|
||||
<NavbarMenuToggle data-testid="navbar-toggle-test" onChange={onChange} />
|
||||
<NavbarContent data-testid="navbar-content-test">
|
||||
<NavbarItem>Dashboard</NavbarItem>
|
||||
<NavbarItem>Team</NavbarItem>
|
||||
<NavbarItem>Deployments</NavbarItem>
|
||||
<NavbarItem>Activity</NavbarItem>
|
||||
<NavbarItem>Settings</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>,
|
||||
);
|
||||
|
||||
const toggle = wrapper.getByTestId("navbar-toggle-test");
|
||||
|
||||
act(() => {
|
||||
toggle.click();
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render correctly with custom toggle icon", () => {
|
||||
const wrapper = render(
|
||||
<Navbar data-testid="navbar-test">
|
||||
<NavbarMenuToggle data-testid="navbar-toggle-test" icon={<span>test</span>} />
|
||||
<NavbarContent data-testid="navbar-content-test">
|
||||
<NavbarItem>Dashboard</NavbarItem>
|
||||
<NavbarItem>Team</NavbarItem>
|
||||
<NavbarItem>Deployments</NavbarItem>
|
||||
<NavbarItem>Activity</NavbarItem>
|
||||
<NavbarItem>Settings</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>,
|
||||
);
|
||||
|
||||
const toggle = wrapper.getByTestId("navbar-toggle-test");
|
||||
|
||||
expect(toggle).toHaveTextContent("test");
|
||||
});
|
||||
});
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/theme": "workspace:*",
|
||||
"@nextui-org/framer-transitions": "workspace:*",
|
||||
"@nextui-org/use-aria-toggle-button": "workspace:*",
|
||||
"@nextui-org/use-scroll-position": "workspace:*",
|
||||
"@react-aria/focus": "^3.12.0",
|
||||
|
||||
@ -49,7 +49,7 @@ const NavbarMenuToggle = forwardRef<NavbarMenuToggleProps, "button">((props, ref
|
||||
const Component = as || "button";
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {slots, classNames, setIsMenuOpen} = useNavbarContext();
|
||||
const {slots, classNames, isMenuOpen, setIsMenuOpen} = useNavbarContext();
|
||||
|
||||
const handleChange = (isOpen: boolean) => {
|
||||
onChange?.(isOpen);
|
||||
@ -66,7 +66,7 @@ const NavbarMenuToggle = forwardRef<NavbarMenuToggleProps, "button">((props, ref
|
||||
|
||||
const child = useMemo(() => {
|
||||
if (typeof icon === "function") {
|
||||
return icon({isOpen: state.isSelected});
|
||||
return icon({isOpen: isMenuOpen});
|
||||
}
|
||||
|
||||
return icon || <span className={slots.toggleIcon({class: classNames?.toggleIcon})} />;
|
||||
@ -78,7 +78,7 @@ const NavbarMenuToggle = forwardRef<NavbarMenuToggleProps, "button">((props, ref
|
||||
}
|
||||
|
||||
return state.isSelected ? "close navigation menu" : "open navigation menu";
|
||||
}, [srOnlyTextProp, state.isSelected]);
|
||||
}, [srOnlyTextProp, isMenuOpen]);
|
||||
|
||||
return (
|
||||
<Component
|
||||
@ -86,7 +86,7 @@ const NavbarMenuToggle = forwardRef<NavbarMenuToggleProps, "button">((props, ref
|
||||
className={slots.toggle?.({class: toggleStyles})}
|
||||
data-focus-visible={dataAttr(isFocusVisible)}
|
||||
data-hover={dataAttr(isHovered)}
|
||||
data-open={dataAttr(state.isSelected)}
|
||||
data-open={dataAttr(isMenuOpen)}
|
||||
data-pressed={dataAttr(isPressed)}
|
||||
{...mergeProps(buttonProps, focusProps, hoverProps, otherProps)}
|
||||
>
|
||||
|
||||
17
packages/components/navbar/src/navbar-transitions.ts
Normal file
17
packages/components/navbar/src/navbar-transitions.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {TRANSITION_EASINGS} from "@nextui-org/framer-transitions";
|
||||
import {Variants} from "framer-motion";
|
||||
|
||||
export const variants: Variants = {
|
||||
visible: {
|
||||
y: 0,
|
||||
transition: {
|
||||
ease: TRANSITION_EASINGS.easeOut,
|
||||
},
|
||||
},
|
||||
hidden: {
|
||||
y: "-100%",
|
||||
transition: {
|
||||
ease: TRANSITION_EASINGS.easeIn,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -1,6 +1,9 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {pickChildren} from "@nextui-org/shared-utils";
|
||||
import {motion} from "framer-motion";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
|
||||
import {variants} from "./navbar-transitions";
|
||||
import {UseNavbarProps, useNavbar} from "./use-navbar";
|
||||
import {NavbarProvider} from "./navbar-context";
|
||||
import NavbarMenu from "./navbar-menu";
|
||||
@ -18,12 +21,27 @@ const Navbar = forwardRef<NavbarProps, "div">((props, ref) => {
|
||||
|
||||
const [childrenWithoutMenu, menu] = pickChildren(children, NavbarMenu);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<header {...context.getWrapperProps()}>{childrenWithoutMenu}</header>
|
||||
{menu}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<NavbarProvider value={context}>
|
||||
<Component {...context.getBaseProps()}>
|
||||
<header {...context.getWrapperProps()}>{childrenWithoutMenu}</header>
|
||||
{menu}
|
||||
</Component>
|
||||
{context.shouldHideOnScroll ? (
|
||||
<motion.nav
|
||||
animate={context.isHidden ? "hidden" : "visible"}
|
||||
initial={false}
|
||||
variants={variants}
|
||||
{...mergeProps(context.getBaseProps(), context.motionProps)}
|
||||
>
|
||||
{content}
|
||||
</motion.nav>
|
||||
) : (
|
||||
<Component {...context.getBaseProps()}>{content}</Component>
|
||||
)}
|
||||
</NavbarProvider>
|
||||
);
|
||||
});
|
||||
|
||||
@ -4,10 +4,11 @@ import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system"
|
||||
import {navbar} from "@nextui-org/theme";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx, dataAttr, ReactRef} from "@nextui-org/shared-utils";
|
||||
import {useMemo, useState} from "react";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {mergeProps, useResizeObserver} from "@react-aria/utils";
|
||||
import {useScrollPosition} from "@nextui-org/use-scroll-position";
|
||||
import {useControlledState} from "@react-stately/utils";
|
||||
import {HTMLMotionProps} from "framer-motion";
|
||||
|
||||
export interface UseNavbarProps extends HTMLNextUIProps<"nav", NavbarVariantProps> {
|
||||
/**
|
||||
@ -45,6 +46,11 @@ export interface UseNavbarProps extends HTMLNextUIProps<"nav", NavbarVariantProp
|
||||
* @default false
|
||||
*/
|
||||
disableScrollHandler?: boolean;
|
||||
/**
|
||||
* The props to modify the framer motion animation. Use the `variants` API to create your own animation.
|
||||
* This motion is only available if the `shouldHideOnScroll` prop is set to `true`.
|
||||
*/
|
||||
motionProps?: HTMLMotionProps<"nav">;
|
||||
/**
|
||||
* The event handler for the menu open state.
|
||||
* @param isOpen boolean
|
||||
@ -56,6 +62,7 @@ export interface UseNavbarProps extends HTMLNextUIProps<"nav", NavbarVariantProp
|
||||
* it only works if `disableScrollHandler` is set to `false` or `shouldHideOnScroll` is set to `true`.
|
||||
*/
|
||||
onScrollPositionChange?: (scrollPosition: number) => void;
|
||||
|
||||
/**
|
||||
* Classname or List of classes to change the classNames of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
@ -90,6 +97,7 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
isMenuOpen: isMenuOpenProp,
|
||||
isMenuDefaultOpen = false,
|
||||
onMenuOpenChange = () => {},
|
||||
motionProps,
|
||||
className,
|
||||
classNames,
|
||||
...otherProps
|
||||
@ -99,6 +107,9 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const prevWidth = useRef(0);
|
||||
const navHeight = useRef(0);
|
||||
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useControlledState<boolean | undefined>(
|
||||
@ -107,6 +118,34 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
onMenuOpenChange,
|
||||
);
|
||||
|
||||
const updateWidth = () => {
|
||||
if (domRef.current) {
|
||||
const width = domRef.current.offsetWidth;
|
||||
|
||||
if (width !== prevWidth.current) {
|
||||
prevWidth.current = width;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useResizeObserver({
|
||||
ref: domRef,
|
||||
onResize: () => {
|
||||
const currentWidth = domRef.current?.offsetWidth;
|
||||
|
||||
if (currentWidth !== prevWidth.current) {
|
||||
updateWidth();
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateWidth();
|
||||
|
||||
navHeight.current = domRef.current?.offsetHeight || 0;
|
||||
}, []);
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
navbar({
|
||||
@ -125,7 +164,7 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
onScrollPositionChange?.(currPos.y);
|
||||
if (shouldHideOnScroll) {
|
||||
setIsHidden((prev) => {
|
||||
const next = currPos.y > prevPos.y;
|
||||
const next = currPos.y > prevPos.y && currPos.y > navHeight.current;
|
||||
|
||||
return next !== prev ? next : prev;
|
||||
});
|
||||
@ -135,7 +174,7 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
|
||||
const getBaseProps: PropGetter = (props = {}) => ({
|
||||
...mergeProps(otherProps, props),
|
||||
"data-hide": dataAttr(isHidden),
|
||||
"data-hidden": dataAttr(isHidden),
|
||||
"data-menu-open": dataAttr(isMenuOpen),
|
||||
ref: domRef,
|
||||
className: slots.base({class: clsx(baseStyles, props?.className)}),
|
||||
@ -153,9 +192,12 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
|
||||
return {
|
||||
Component,
|
||||
motionProps,
|
||||
isHidden,
|
||||
slots,
|
||||
domRef,
|
||||
classNames,
|
||||
shouldHideOnScroll,
|
||||
isMenuOpen,
|
||||
setIsMenuOpen,
|
||||
getBaseProps,
|
||||
|
||||
@ -3,9 +3,20 @@ import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||
import {navbar} from "@nextui-org/theme";
|
||||
import {Link} from "@nextui-org/link";
|
||||
import {Button} from "@nextui-org/button";
|
||||
import {Avatar} from "@nextui-org/avatar";
|
||||
import {Input} from "@nextui-org/input";
|
||||
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 {
|
||||
ChevronDown,
|
||||
Lock,
|
||||
Activity,
|
||||
Flash,
|
||||
Server,
|
||||
TagUser,
|
||||
Scale,
|
||||
SearchIcon,
|
||||
} from "@nextui-org/shared-icons";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
@ -223,7 +234,7 @@ const WithDropdownTemplate: ComponentStory<typeof Navbar> = (args: NavbarProps)
|
||||
|
||||
return (
|
||||
<App>
|
||||
<Navbar isBordered position="sticky" {...args}>
|
||||
<Navbar {...args}>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold hidden sm:block text-inherit">ACME</p>
|
||||
@ -317,6 +328,212 @@ const WithDropdownTemplate: ComponentStory<typeof Navbar> = (args: NavbarProps)
|
||||
);
|
||||
};
|
||||
|
||||
const WithAvatarUserTemplate: ComponentStory<typeof Navbar> = (args: NavbarProps) => {
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Logout",
|
||||
];
|
||||
|
||||
return (
|
||||
<App>
|
||||
<Navbar {...args}>
|
||||
<NavbarMenuToggle className="sm:hidden" />
|
||||
<NavbarMenu>
|
||||
{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>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden gap-3 md:flex">
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Dashboard
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive as={Link} color="secondary" href="#">
|
||||
Team
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Deployments
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Activity
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Settings
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent>
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<NavbarItem>
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
className="transition-transform"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
|
||||
/>
|
||||
</NavbarItem>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Profile Actions" color="secondary">
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p className="font-semibold">Signed in as</p>
|
||||
<p className="font-semibold">zoey@example.com</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem key="settings" showDivider>
|
||||
My Settings
|
||||
</DropdownItem>
|
||||
<DropdownItem key="team_settings">Team Settings</DropdownItem>
|
||||
<DropdownItem key="analytics" showDivider>
|
||||
Analytics
|
||||
</DropdownItem>
|
||||
<DropdownItem key="system">System</DropdownItem>
|
||||
<DropdownItem key="configurations">Configurations</DropdownItem>
|
||||
<DropdownItem key="help_and_feedback" showDivider>
|
||||
Help & Feedback
|
||||
</DropdownItem>
|
||||
<DropdownItem key="logout" showDivider color="danger">
|
||||
Log Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
</App>
|
||||
);
|
||||
};
|
||||
|
||||
const WithSearchInputTemplate: ComponentStory<typeof Navbar> = (args: NavbarProps) => {
|
||||
const menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Logout",
|
||||
];
|
||||
|
||||
return (
|
||||
<App>
|
||||
<Navbar {...args}>
|
||||
<NavbarMenuToggle className="sm:hidden" />
|
||||
<NavbarMenu>
|
||||
{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>
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarContent className="hidden gap-3 md:flex">
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Dashboard
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive as={Link} color="secondary" href="#">
|
||||
Team
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Deployments
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Activity
|
||||
</NavbarItem>
|
||||
<NavbarItem as={Link} color="foreground" href="#">
|
||||
Settings
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
<NavbarContent>
|
||||
<NavbarItem className="hidden md:flex">
|
||||
<Input
|
||||
classNames={{
|
||||
input: "text-base",
|
||||
}}
|
||||
placeholder="Search..."
|
||||
size="xs"
|
||||
startContent={<SearchIcon className="text-base pointer-events-none flex-shrink-0" />}
|
||||
onClear={() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("clear");
|
||||
}}
|
||||
/>
|
||||
</NavbarItem>
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<NavbarItem>
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
className="transition-transform"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
|
||||
/>
|
||||
</NavbarItem>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Profile Actions" color="secondary">
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p className="font-semibold">Signed in as</p>
|
||||
<p className="font-semibold">zoey@example.com</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem key="settings" showDivider>
|
||||
My Settings
|
||||
</DropdownItem>
|
||||
<DropdownItem key="team_settings">Team Settings</DropdownItem>
|
||||
<DropdownItem key="analytics" showDivider>
|
||||
Analytics
|
||||
</DropdownItem>
|
||||
<DropdownItem key="system">System</DropdownItem>
|
||||
<DropdownItem key="configurations">Configurations</DropdownItem>
|
||||
<DropdownItem key="help_and_feedback" showDivider>
|
||||
Help & Feedback
|
||||
</DropdownItem>
|
||||
<DropdownItem key="logout" showDivider color="danger">
|
||||
Log Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
</App>
|
||||
);
|
||||
};
|
||||
|
||||
export const Static = Template.bind({});
|
||||
Static.args = {
|
||||
...defaultProps,
|
||||
@ -348,7 +565,16 @@ WithMenu.args = {
|
||||
};
|
||||
|
||||
export const WithDropdown = WithDropdownTemplate.bind({});
|
||||
|
||||
WithDropdown.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const WithAvatarUser = WithAvatarUserTemplate.bind({});
|
||||
WithAvatarUser.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const WithSearchInput = WithSearchInputTemplate.bind({});
|
||||
WithSearchInput.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type {PopoverVariantProps, SlotsToClasses, PopoverSlots} from "@nextui-org/theme";
|
||||
import type {HTMLMotionProps} from "framer-motion";
|
||||
import type {RefObject, Ref} from "react";
|
||||
import type {OverlayPlacement} from "@nextui-org/aria-utils";
|
||||
|
||||
import {OverlayPlacement, toOverlayPlacement} from "@nextui-org/aria-utils";
|
||||
import {OverlayTriggerState, useOverlayTriggerState} from "@react-stately/overlays";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {
|
||||
@ -13,7 +13,11 @@ import {
|
||||
} from "@react-aria/overlays";
|
||||
import {OverlayTriggerProps} from "@react-types/overlays";
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
import {toReactAriaPlacement, getArrowPlacement} from "@nextui-org/aria-utils";
|
||||
import {
|
||||
toReactAriaPlacement,
|
||||
getArrowPlacement,
|
||||
getShouldUseAxisPlacement,
|
||||
} from "@nextui-org/aria-utils";
|
||||
import {popover} from "@nextui-org/theme";
|
||||
import {mergeProps, mergeRefs} from "@react-aria/utils";
|
||||
import {createDOMRef} from "@nextui-org/dom-utils";
|
||||
@ -148,7 +152,7 @@ export function usePopover(originalProps: UsePopoverProps) {
|
||||
isDisabled: !state.isOpen || !shouldBlockScroll,
|
||||
});
|
||||
|
||||
const {popoverProps, underlayProps, arrowProps, placement} = useReactAriaPopover(
|
||||
const {popoverProps, underlayProps, arrowProps, placement: ariaPlacement} = useReactAriaPopover(
|
||||
{
|
||||
triggerRef,
|
||||
popoverRef,
|
||||
@ -195,6 +199,11 @@ export function usePopover(originalProps: UsePopoverProps) {
|
||||
},
|
||||
});
|
||||
|
||||
const placement = useMemo(
|
||||
() => (getShouldUseAxisPlacement(ariaPlacement, placementProp) ? ariaPlacement : placementProp),
|
||||
[ariaPlacement, placementProp],
|
||||
);
|
||||
|
||||
const getTriggerProps = useCallback<PropGetter>(
|
||||
(props = {}, _ref: Ref<any> | null | undefined = null) => {
|
||||
return {
|
||||
@ -221,10 +230,10 @@ export function usePopover(originalProps: UsePopoverProps) {
|
||||
const getArrowProps = useCallback<PropGetter>(
|
||||
() => ({
|
||||
className: slots.arrow({class: classNames?.arrow}),
|
||||
"data-placement": getArrowPlacement(placement, placementProp),
|
||||
"data-placement": getArrowPlacement(ariaPlacement, placementProp),
|
||||
...arrowProps,
|
||||
}),
|
||||
[arrowProps, placement, placementProp, slots, classNames],
|
||||
[arrowProps, ariaPlacement, placementProp, slots, classNames],
|
||||
);
|
||||
|
||||
return {
|
||||
@ -233,7 +242,7 @@ export function usePopover(originalProps: UsePopoverProps) {
|
||||
classNames,
|
||||
showArrow,
|
||||
triggerRef,
|
||||
placement: placement ? toOverlayPlacement(placement) : placementProp,
|
||||
placement,
|
||||
isOpen: state.isOpen,
|
||||
onClose: state.close,
|
||||
disableAnimation,
|
||||
|
||||
@ -49,7 +49,7 @@ const navbar = tv({
|
||||
slots: {
|
||||
base: [
|
||||
"relative",
|
||||
"z-20",
|
||||
"z-50",
|
||||
"w-full",
|
||||
"h-auto",
|
||||
"flex",
|
||||
@ -144,6 +144,7 @@ const navbar = tv({
|
||||
"data-[active=true]:font-semibold",
|
||||
],
|
||||
menu: [
|
||||
"z-50",
|
||||
"hidden",
|
||||
"px-6",
|
||||
"pt-4",
|
||||
@ -200,10 +201,10 @@ const navbar = tv({
|
||||
"sticky",
|
||||
"top-0",
|
||||
"inset-x-0",
|
||||
"transition-transform",
|
||||
"!duration-400",
|
||||
"translate-y-0",
|
||||
"data-[hide=true]:-translate-y-full",
|
||||
// "transition-transform",
|
||||
// "!duration-400",
|
||||
// "translate-y-0",
|
||||
// "data-[hide=true]:-translate-y-full",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -90,6 +90,21 @@ export const toOverlayPlacement = (placement: PlacementAxis) => {
|
||||
return mapPositions[placement];
|
||||
};
|
||||
|
||||
export const getShouldUseAxisPlacement = (
|
||||
axisPlacement: PlacementAxis,
|
||||
overlayPlacement: OverlayPlacement,
|
||||
) => {
|
||||
if (overlayPlacement.includes("-")) {
|
||||
const [position] = overlayPlacement.split("-");
|
||||
|
||||
if (position.includes(axisPlacement)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getArrowPlacement = (dynamicPlacement: PlacementAxis, placement: OverlayPlacement) => {
|
||||
if (placement.includes("-")) {
|
||||
const [, position] = placement.split("-");
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -1082,6 +1082,9 @@ importers:
|
||||
'@nextui-org/dom-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/dom-utils
|
||||
'@nextui-org/framer-transitions':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/framer-transitions
|
||||
'@nextui-org/shared-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-utils
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user