mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(dropdown): tests added
This commit is contained in:
parent
c66e3d7585
commit
548c4b824c
@ -1,19 +1,389 @@
|
||||
import * as React from "react";
|
||||
import {render} from "@testing-library/react";
|
||||
import {act, render} from "@testing-library/react";
|
||||
import {Button} from "@nextui-org/button";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import {Dropdown} from "../src";
|
||||
import {Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, DropdownSection} from "../src";
|
||||
|
||||
describe("Dropdown", () => {
|
||||
it("should render correctly", () => {
|
||||
const wrapper = render(<Dropdown />);
|
||||
it("should render correctly (static)", () => {
|
||||
const wrapper = render(
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button>Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Actions" onAction={alert}>
|
||||
<DropdownItem key="new">New file</DropdownItem>
|
||||
<DropdownItem key="copy">Copy link</DropdownItem>
|
||||
<DropdownItem key="edit">Edit file</DropdownItem>
|
||||
<DropdownItem key="delete" color="danger">
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("ref should be forwarded", () => {
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
it("should render correctly (dynamic)", () => {
|
||||
const menuItems = [
|
||||
{key: "new", name: "New File"},
|
||||
{key: "copy", name: "Copy Link"},
|
||||
{key: "edit", name: "Edit File"},
|
||||
{key: "delete", name: "Delete File"},
|
||||
];
|
||||
|
||||
render(<Dropdown ref={ref} />);
|
||||
expect(ref.current).not.toBeNull();
|
||||
const wrapper = render(
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button>Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Actions" items={menuItems}>
|
||||
{(item: any) => <DropdownItem key={item.key}>{item.name}</DropdownItem>}
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("should render correctly with section (static)", () => {
|
||||
const wrapper = render(
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button>Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Actions" onAction={alert}>
|
||||
<DropdownSection title="Actions">
|
||||
<DropdownItem key="new">New file</DropdownItem>
|
||||
<DropdownItem key="copy">Copy link</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection title="Danger Zone">
|
||||
<DropdownItem key="edit">Edit file</DropdownItem>
|
||||
<DropdownItem key="delete" color="danger">
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("should render correctly with section (dynamic)", () => {
|
||||
const menuItems = [
|
||||
{
|
||||
title: "Actions",
|
||||
children: [
|
||||
{key: "new", name: "New File"},
|
||||
{key: "copy", name: "Copy Link"},
|
||||
{key: "edit", name: "Edit File"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Danger Zone",
|
||||
children: [{key: "delete", name: "Delete File"}],
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = render(
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button>Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Actions" items={menuItems}>
|
||||
{(section: any) => (
|
||||
<DropdownSection
|
||||
aria-label={section.title}
|
||||
items={section.children}
|
||||
title={section.title}
|
||||
>
|
||||
{(item: any) => <DropdownItem key={item.key}>{item.name}</DropdownItem>}
|
||||
</DropdownSection>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("should work with single selection (controlled)", async () => {
|
||||
let onOpenChange = jest.fn();
|
||||
let onSelectionChange = jest.fn();
|
||||
|
||||
const wrapper = render(
|
||||
<Dropdown onOpenChange={onOpenChange}>
|
||||
<DropdownTrigger>
|
||||
<Button data-testid="trigger-test">Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Actions"
|
||||
selectionMode="single"
|
||||
onSelectionChange={onSelectionChange}
|
||||
>
|
||||
<DropdownItem key="new">New file</DropdownItem>
|
||||
<DropdownItem key="copy">Copy link</DropdownItem>
|
||||
<DropdownItem key="edit">Edit file</DropdownItem>
|
||||
<DropdownItem key="delete" color="danger">
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
let triggerButton = wrapper.getByTestId("trigger-test");
|
||||
|
||||
expect(onOpenChange).toBeCalledTimes(0);
|
||||
|
||||
act(() => {
|
||||
triggerButton.click();
|
||||
});
|
||||
|
||||
expect(onOpenChange).toBeCalledTimes(1);
|
||||
|
||||
let menu = wrapper.getByRole("menu");
|
||||
|
||||
expect(menu).toBeTruthy();
|
||||
|
||||
// validates if the menu has the triggerButton id as aria-labelledby
|
||||
expect(menu.getAttribute("aria-labelledby")).toBe(triggerButton.id);
|
||||
|
||||
let menuItems = wrapper.getAllByRole("menuitemradio");
|
||||
|
||||
expect(menuItems.length).toBe(4);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(menuItems[1]);
|
||||
|
||||
expect(onSelectionChange).toBeCalledTimes(1);
|
||||
expect(onOpenChange).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with multiple selection (controlled)", async () => {
|
||||
let onOpenChange = jest.fn();
|
||||
let onSelectionChange = jest.fn();
|
||||
|
||||
const wrapper = render(
|
||||
<Dropdown onOpenChange={onOpenChange}>
|
||||
<DropdownTrigger>
|
||||
<Button data-testid="trigger-test">Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disallowEmptySelection
|
||||
aria-label="Actions"
|
||||
selectionMode="multiple"
|
||||
onSelectionChange={onSelectionChange}
|
||||
>
|
||||
<DropdownItem key="new">New file</DropdownItem>
|
||||
<DropdownItem key="copy">Copy link</DropdownItem>
|
||||
<DropdownItem key="edit">Edit file</DropdownItem>
|
||||
<DropdownItem key="delete" color="danger">
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
let triggerButton = wrapper.getByTestId("trigger-test");
|
||||
|
||||
expect(onOpenChange).toBeCalledTimes(0);
|
||||
|
||||
act(() => {
|
||||
triggerButton.click();
|
||||
});
|
||||
|
||||
expect(onOpenChange).toBeCalledTimes(1);
|
||||
|
||||
let menu = wrapper.getByRole("menu");
|
||||
|
||||
expect(menu).toBeTruthy();
|
||||
|
||||
// validates if the menu has the triggerButton id as aria-labelledby
|
||||
expect(menu.getAttribute("aria-labelledby")).toBe(triggerButton.id);
|
||||
|
||||
let menuItems = wrapper.getAllByRole("menuitemcheckbox");
|
||||
|
||||
expect(menuItems.length).toBe(4);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(menuItems[0]);
|
||||
|
||||
expect(onSelectionChange).toBeCalledTimes(1);
|
||||
expect(onOpenChange).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show checkmarks if selectionMode is single and has a selected item", () => {
|
||||
const wrapper = render(
|
||||
<Dropdown isOpen>
|
||||
<DropdownTrigger>
|
||||
<Button data-testid="trigger-test">Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Actions" selectedKeys={["new"]} selectionMode="single">
|
||||
<DropdownItem key="new">New file</DropdownItem>
|
||||
<DropdownItem key="copy">Copy link</DropdownItem>
|
||||
<DropdownItem key="edit">Edit file</DropdownItem>
|
||||
<DropdownItem key="delete" color="danger">
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
let menuItems = wrapper.getAllByRole("menuitemradio");
|
||||
|
||||
expect(menuItems.length).toBe(4);
|
||||
|
||||
expect(menuItems[0].getAttribute("aria-checked")).toBe("true");
|
||||
expect(menuItems[1].getAttribute("aria-checked")).toBe("false");
|
||||
expect(menuItems[2].getAttribute("aria-checked")).toBe("false");
|
||||
expect(menuItems[3].getAttribute("aria-checked")).toBe("false");
|
||||
|
||||
let svg = menuItems[0].querySelector("svg");
|
||||
|
||||
expect(svg).toBeTruthy();
|
||||
|
||||
expect(svg?.getAttribute("data-selected")).toBe("true");
|
||||
});
|
||||
|
||||
it("should show multiple checkmarks if selectionMode is multiple and has selected items", () => {
|
||||
const wrapper = render(
|
||||
<Dropdown isOpen>
|
||||
<DropdownTrigger>
|
||||
<Button data-testid="trigger-test">Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Actions" selectedKeys={["new", "copy"]} selectionMode="multiple">
|
||||
<DropdownItem key="new">New file</DropdownItem>
|
||||
<DropdownItem key="copy">Copy link</DropdownItem>
|
||||
<DropdownItem key="edit">Edit file</DropdownItem>
|
||||
<DropdownItem key="delete" color="danger">
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
let menuItems = wrapper.getAllByRole("menuitemcheckbox");
|
||||
|
||||
expect(menuItems.length).toBe(4);
|
||||
|
||||
expect(menuItems[0].getAttribute("aria-checked")).toBe("true");
|
||||
expect(menuItems[1].getAttribute("aria-checked")).toBe("true");
|
||||
expect(menuItems[2].getAttribute("aria-checked")).toBe("false");
|
||||
expect(menuItems[3].getAttribute("aria-checked")).toBe("false");
|
||||
|
||||
let checkmark1 = menuItems[0].querySelector("svg");
|
||||
|
||||
expect(checkmark1).toBeTruthy();
|
||||
|
||||
expect(checkmark1?.getAttribute("data-selected")).toBe("true");
|
||||
|
||||
let checkmark2 = menuItems[1].querySelector("svg");
|
||||
|
||||
expect(checkmark2).toBeTruthy();
|
||||
|
||||
expect(checkmark2?.getAttribute("data-selected")).toBe("true");
|
||||
});
|
||||
|
||||
it("should not show checkmarks if selectionMode not defined", () => {
|
||||
const wrapper = render(
|
||||
<Dropdown isOpen>
|
||||
<DropdownTrigger>
|
||||
<Button data-testid="trigger-test">Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Actions" selectedKeys={["new", "copy"]}>
|
||||
<DropdownItem key="new">New file</DropdownItem>
|
||||
<DropdownItem key="copy">Copy link</DropdownItem>
|
||||
<DropdownItem key="edit">Edit file</DropdownItem>
|
||||
<DropdownItem key="delete" color="danger">
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
let menuItems = wrapper.getAllByRole("menuitem");
|
||||
|
||||
expect(menuItems.length).toBe(4);
|
||||
|
||||
expect(menuItems[0].getAttribute("aria-checked")).toBeFalsy();
|
||||
expect(menuItems[1].getAttribute("aria-checked")).toBeFalsy();
|
||||
expect(menuItems[2].getAttribute("aria-checked")).toBeFalsy();
|
||||
expect(menuItems[3].getAttribute("aria-checked")).toBeFalsy();
|
||||
|
||||
let checkmark1 = menuItems[0].querySelector("svg");
|
||||
|
||||
expect(checkmark1).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not open on disabled button", () => {
|
||||
const wrapper = render(
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isDisabled data-testid="trigger-test">
|
||||
Trigger
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Actions">
|
||||
<DropdownItem key="new">New file</DropdownItem>
|
||||
<DropdownItem key="copy">Copy link</DropdownItem>
|
||||
<DropdownItem key="edit">Edit file</DropdownItem>
|
||||
<DropdownItem key="delete" color="danger">
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
let triggerButton = wrapper.getByTestId("trigger-test");
|
||||
|
||||
expect(triggerButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
triggerButton.click();
|
||||
});
|
||||
|
||||
let menu = wrapper.queryByRole("menu");
|
||||
|
||||
expect(menu).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not select on disabled item", () => {
|
||||
const onSelectionChange = jest.fn();
|
||||
const wrapper = render(
|
||||
<Dropdown isOpen>
|
||||
<DropdownTrigger>
|
||||
<Button data-testid="trigger-test">Trigger</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
aria-label="Actions"
|
||||
disabledKeys={["copy"]}
|
||||
selectionMode="single"
|
||||
onSelectionChange={onSelectionChange}
|
||||
>
|
||||
<DropdownItem key="new">New file</DropdownItem>
|
||||
<DropdownItem key="copy">Copy link</DropdownItem>
|
||||
<DropdownItem key="edit">Edit file</DropdownItem>
|
||||
<DropdownItem key="delete" color="danger">
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>,
|
||||
);
|
||||
|
||||
let menuItems = wrapper.getAllByRole("menuitemradio");
|
||||
|
||||
expect(menuItems.length).toBe(4);
|
||||
|
||||
act(() => {
|
||||
menuItems[1].click();
|
||||
});
|
||||
|
||||
expect(onSelectionChange).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,6 +5,8 @@ import {useMenu} from "@react-aria/menu";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {AriaMenuProps} from "@react-types/menu";
|
||||
import {useTreeState} from "@react-stately/tree";
|
||||
import {dropdownMenu} from "@nextui-org/theme";
|
||||
import {useMemo} from "react";
|
||||
|
||||
import DropdownSection from "./dropdown-section";
|
||||
import DropdownItem, {DropdownItemProps} from "./dropdown-item";
|
||||
@ -35,7 +37,7 @@ export interface DropdownMenuProps<T = object>
|
||||
/**
|
||||
* The dropdown items styles.
|
||||
*/
|
||||
styles?: DropdownItemProps["styles"];
|
||||
itemStyles?: DropdownItemProps["styles"];
|
||||
}
|
||||
|
||||
const DropdownMenu = forwardRef<DropdownMenuProps, "ul">(
|
||||
@ -48,7 +50,7 @@ const DropdownMenu = forwardRef<DropdownMenuProps, "ul">(
|
||||
onAction,
|
||||
closeOnSelect,
|
||||
className,
|
||||
styles,
|
||||
itemStyles,
|
||||
...otherProps
|
||||
},
|
||||
ref,
|
||||
@ -62,9 +64,11 @@ const DropdownMenu = forwardRef<DropdownMenuProps, "ul">(
|
||||
const state = useTreeState(otherProps);
|
||||
const {menuProps} = useMenu(otherProps, state, domRef);
|
||||
|
||||
const styles = useMemo(() => dropdownMenu({className}), [className]);
|
||||
|
||||
return (
|
||||
<PopoverContent>
|
||||
<Component {...getMenuProps({...menuProps, className}, domRef)}>
|
||||
<Component {...getMenuProps({...menuProps}, domRef)} className={styles}>
|
||||
{[...state.collection].map((item) => {
|
||||
const itemProps = {
|
||||
key: item.key,
|
||||
@ -73,7 +77,7 @@ const DropdownMenu = forwardRef<DropdownMenuProps, "ul">(
|
||||
disableAnimation,
|
||||
item,
|
||||
state,
|
||||
styles,
|
||||
styles: itemStyles,
|
||||
variant,
|
||||
onAction,
|
||||
...item.props,
|
||||
|
||||
@ -5,7 +5,7 @@ import {dropdownSection} from "@nextui-org/theme";
|
||||
import {Node} from "@react-types/shared";
|
||||
import {TreeState} from "@react-stately/tree";
|
||||
import {useMenuSection} from "@react-aria/menu";
|
||||
import {useMemo, Key} from "react";
|
||||
import {useMemo, Key, useId} from "react";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
|
||||
@ -67,6 +67,8 @@ const DropdownSection = forwardRef<DropdownSectionProps, "li">(
|
||||
) => {
|
||||
const {section, heading, ...styles} = stylesProp;
|
||||
|
||||
const headingId = useId();
|
||||
|
||||
const Component = as || "li";
|
||||
const isFirstKey = item.key === state.collection.getFirstKey();
|
||||
|
||||
@ -88,11 +90,11 @@ const DropdownSection = forwardRef<DropdownSectionProps, "li">(
|
||||
className={slots.section({class: baseStyles})}
|
||||
>
|
||||
{item.rendered && (
|
||||
<span {...headingProps} className={slots.heading({class: heading})}>
|
||||
<span {...headingProps} className={slots.heading({class: heading})} id={headingId}>
|
||||
{item.rendered}
|
||||
</span>
|
||||
)}
|
||||
<ul {...groupProps}>
|
||||
<ul {...groupProps} aria-labelledby={headingId}>
|
||||
{[...item.childNodes].map((node) => {
|
||||
let dropdownItem = (
|
||||
<DropdownItem
|
||||
|
||||
@ -7,7 +7,13 @@ export function DropdownSelectedIcon(props: DropdownSelectedIconProps) {
|
||||
const {isSelected, disableAnimation, ...otherProps} = props;
|
||||
|
||||
return (
|
||||
<svg aria-hidden="true" role="presentation" viewBox="0 0 17 18" {...otherProps}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-selected={isSelected}
|
||||
role="presentation"
|
||||
viewBox="0 0 17 18"
|
||||
{...otherProps}
|
||||
>
|
||||
<polyline
|
||||
fill="none"
|
||||
points="1 9 7 14 15 4"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type {DropdownItemBaseProps} from "./base/dropdown-item-base";
|
||||
|
||||
import {useMemo, useRef, useCallback} from "react";
|
||||
import {useMemo, useRef, useCallback, useId} from "react";
|
||||
import {dropdownItem} from "@nextui-org/theme";
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
@ -58,11 +58,13 @@ export function useDropdownItem<T extends object>(originalProps: UseDropdownItem
|
||||
|
||||
const {rendered, key} = item;
|
||||
|
||||
const isSelected = state.selectionManager.isSelected(key);
|
||||
const isFocused = state.selectionManager.focusedKey === item.key;
|
||||
const isDisabled = state.disabledKeys.has(key) || originalProps.isDisabled;
|
||||
const isSelectable = state.selectionManager.selectionMode !== "none";
|
||||
|
||||
const labelId = useId();
|
||||
const descriptionId = useId();
|
||||
const keyboardId = useId();
|
||||
|
||||
const {pressProps} = usePress({
|
||||
ref: domRef,
|
||||
isDisabled,
|
||||
@ -73,11 +75,17 @@ export function useDropdownItem<T extends object>(originalProps: UseDropdownItem
|
||||
autoFocus,
|
||||
});
|
||||
|
||||
const {menuItemProps, labelProps, descriptionProps, keyboardShortcutProps} = useMenuItem(
|
||||
const {
|
||||
isFocused,
|
||||
isSelected,
|
||||
menuItemProps,
|
||||
labelProps,
|
||||
descriptionProps,
|
||||
keyboardShortcutProps,
|
||||
} = useMenuItem(
|
||||
{
|
||||
key,
|
||||
onClose,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
"aria-label": props["aria-label"],
|
||||
closeOnSelect,
|
||||
@ -111,22 +119,27 @@ export function useDropdownItem<T extends object>(originalProps: UseDropdownItem
|
||||
props,
|
||||
),
|
||||
"data-focused": dataAttr(isFocused),
|
||||
"aria-labelledby": labelId,
|
||||
"aria-describedby": [descriptionId, keyboardId].filter(Boolean).join(" ") || undefined,
|
||||
className: slots.base({class: clsx(baseStyles, props.className)}),
|
||||
onClick: chain(pressProps.onClick, onClick),
|
||||
});
|
||||
|
||||
const getLabelProps: PropGetter = (props = {}) => ({
|
||||
...mergeProps(labelProps, props),
|
||||
id: labelId,
|
||||
className: slots.title({class: styles?.title}),
|
||||
});
|
||||
|
||||
const getDescriptionProps: PropGetter = (props = {}) => ({
|
||||
...mergeProps(descriptionProps, props),
|
||||
id: descriptionId,
|
||||
className: slots.description({class: styles?.description}),
|
||||
});
|
||||
|
||||
const getKeyboardShortcutProps: PropGetter = (props = {}) => ({
|
||||
...mergeProps(keyboardShortcutProps, props),
|
||||
id: keyboardId,
|
||||
className: slots.shortcut({class: styles?.shortcut}),
|
||||
});
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import type {DropdownVariantProps, SlotsToClasses, DropdownSlots} from "@nextui-org/theme";
|
||||
|
||||
import {Ref, useId} from "react";
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
import {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||
import {useMenuTriggerState} from "@react-stately/menu";
|
||||
import {MenuTriggerType} from "@react-types/menu";
|
||||
import {useMenuTrigger} from "@react-aria/menu";
|
||||
@ -11,7 +9,7 @@ import {PopoverProps} from "@nextui-org/popover";
|
||||
import {useMemo, useRef} from "react";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
|
||||
interface Props
|
||||
export interface UseDropdownProps
|
||||
extends HTMLNextUIProps<"div", Omit<PopoverProps, "children" | "color" | "variant">> {
|
||||
/**
|
||||
* Type of overlay that is opened by the trigger.
|
||||
@ -36,33 +34,12 @@ interface Props
|
||||
* @default true
|
||||
*/
|
||||
closeOnSelect?: boolean;
|
||||
/**
|
||||
* Classname or List of classes to change the styles of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* <Dropdown styles={{
|
||||
* trigger: "trigger-classes",
|
||||
* base: "base-classes", // popover wrapper
|
||||
* menu: "menu-classes",// items wrapper
|
||||
* section: "section-classes",
|
||||
* sectionHeading: "sectionHeading-classes",
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
styles?: SlotsToClasses<DropdownSlots>;
|
||||
}
|
||||
|
||||
export type UseDropdownProps = Props & DropdownVariantProps;
|
||||
|
||||
export function useDropdown(originalProps: UseDropdownProps) {
|
||||
const [props, variantProps] = mapPropsVariants(originalProps, dropdown.variantKeys);
|
||||
|
||||
export function useDropdown(props: UseDropdownProps) {
|
||||
const {
|
||||
as,
|
||||
triggerRef: triggerRefProp,
|
||||
className,
|
||||
isOpen,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
@ -71,7 +48,9 @@ export function useDropdown(originalProps: UseDropdownProps) {
|
||||
placement = "bottom",
|
||||
isDisabled = false,
|
||||
closeOnSelect = true,
|
||||
styles,
|
||||
styles: stylesProp,
|
||||
disableAnimation = false,
|
||||
className,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@ -83,8 +62,7 @@ export function useDropdown(originalProps: UseDropdownProps) {
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const triggerId = useId();
|
||||
|
||||
const disableAnimation = originalProps.disableAnimation ?? false;
|
||||
const menuId = useId();
|
||||
|
||||
const state = useMenuTriggerState({trigger, isOpen, defaultOpen, onOpenChange});
|
||||
|
||||
@ -94,16 +72,14 @@ export function useDropdown(originalProps: UseDropdownProps) {
|
||||
menuTriggerRef,
|
||||
);
|
||||
|
||||
const slots = useMemo(
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
dropdown({
|
||||
...variantProps,
|
||||
className,
|
||||
}),
|
||||
[...Object.values(variantProps)],
|
||||
[className],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(styles?.base, className);
|
||||
|
||||
const getPopoverProps: PropGetter = (props = {}) => ({
|
||||
state,
|
||||
placement,
|
||||
@ -112,7 +88,12 @@ export function useDropdown(originalProps: UseDropdownProps) {
|
||||
scrollRef: menuRef,
|
||||
triggerRef: menuTriggerRef,
|
||||
...mergeProps(otherProps, props),
|
||||
className: slots.base({class: clsx(baseStyles, props.className)}),
|
||||
styles: {
|
||||
...stylesProp,
|
||||
...props.styles,
|
||||
base: clsx(styles, stylesProp?.base, props.className),
|
||||
arrow: clsx("border border-neutral-100", stylesProp?.arrow),
|
||||
},
|
||||
});
|
||||
|
||||
const getMenuTriggerProps: PropGetter = (
|
||||
@ -127,13 +108,15 @@ export function useDropdown(originalProps: UseDropdownProps) {
|
||||
...mergeProps(otherMenuTriggerProps, props),
|
||||
id: triggerId,
|
||||
ref: mergeRefs(_ref, triggerRef),
|
||||
"aria-controls": menuId,
|
||||
};
|
||||
};
|
||||
|
||||
const getMenuProps: PropGetter = (props = {}, _ref: Ref<any> | null | undefined = null) => ({
|
||||
...mergeProps(menuProps, props),
|
||||
id: menuId,
|
||||
ref: mergeRefs(_ref, menuRef),
|
||||
className: slots.menu({class: clsx(styles?.menu, props.className)}),
|
||||
"aria-labelledby": triggerId,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -456,6 +456,7 @@ const CustomTriggerTemplate: ComponentStory<any> = ({variant, ...args}) => {
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
className="transition-transform"
|
||||
color="secondary"
|
||||
size="lg"
|
||||
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
|
||||
@ -493,6 +494,7 @@ const CustomTriggerTemplate: ComponentStory<any> = ({variant, ...args}) => {
|
||||
size: "lg",
|
||||
src: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
}}
|
||||
className="transition-transform"
|
||||
description="@tonyreichert"
|
||||
name="Tony Reichert"
|
||||
/>
|
||||
|
||||
13
packages/core/theme/src/components/dropdown-menu.ts
Normal file
13
packages/core/theme/src/components/dropdown-menu.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
/**
|
||||
* Dropdown Menu wrapper **Tailwind Variants** component
|
||||
*
|
||||
* const styles = dropdownMenu({...})
|
||||
|
||||
*/
|
||||
const dropdownMenu = tv({
|
||||
base: "w-full flex flex-col p-1",
|
||||
});
|
||||
|
||||
export {dropdownMenu};
|
||||
@ -1,11 +1,9 @@
|
||||
import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
/**
|
||||
* Dropdown wrapper **Tailwind Variants** component
|
||||
*
|
||||
* const { base, trigger, arrow } = dropdown({...})
|
||||
* const { base, menu } = dropdown({...})
|
||||
*
|
||||
* @example
|
||||
* <div>
|
||||
@ -17,33 +15,17 @@ import {tv} from "tailwind-variants";
|
||||
* </div>
|
||||
*/
|
||||
const dropdown = tv({
|
||||
slots: {
|
||||
base: [
|
||||
"w-full",
|
||||
"p-1",
|
||||
"min-w-[200px]",
|
||||
"shadow",
|
||||
"shadow-lg",
|
||||
"bg-white",
|
||||
"dark:bg-content1",
|
||||
"border",
|
||||
"border-neutral-100",
|
||||
],
|
||||
menu: "w-full flex flex-col p-1",
|
||||
trigger: [],
|
||||
},
|
||||
variants: {
|
||||
disableAnimation: {
|
||||
true: "transition-none",
|
||||
},
|
||||
},
|
||||
|
||||
defaultVariants: {
|
||||
disableAnimation: false,
|
||||
},
|
||||
base: [
|
||||
"w-full",
|
||||
"p-1",
|
||||
"min-w-[200px]",
|
||||
"shadow",
|
||||
"shadow-lg",
|
||||
"bg-white",
|
||||
"dark:bg-content1",
|
||||
"border",
|
||||
"border-neutral-100",
|
||||
],
|
||||
});
|
||||
|
||||
export type DropdownVariantProps = VariantProps<typeof dropdown>;
|
||||
export type DropdownSlots = keyof ReturnType<typeof dropdown>;
|
||||
|
||||
export {dropdown};
|
||||
|
||||
@ -26,3 +26,4 @@ export * from "./input";
|
||||
export * from "./dropdown";
|
||||
export * from "./dropdown-item";
|
||||
export * from "./dropdown-section";
|
||||
export * from "./dropdown-menu";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user