mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(dropdown): storybook completed
This commit is contained in:
parent
5f8021a44b
commit
8ab3deff93
@ -54,6 +54,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextui-org/button": "workspace:*",
|
||||
"@nextui-org/avatar": "workspace:*",
|
||||
"@nextui-org/user": "workspace:*",
|
||||
"@nextui-org/shared-icons": "workspace:*",
|
||||
"@react-types/menu": "^3.9.0",
|
||||
"@react-types/shared": "^3.18.0",
|
||||
|
||||
@ -63,6 +63,7 @@ interface Props<T extends object = {}> extends Omit<ItemProps<"li", T>, "childre
|
||||
* <DropdownItem styles={{
|
||||
* base:"base-classes",
|
||||
* title:"label-classes",
|
||||
* wrapper:"wrapper-classes", // title and description wrapper
|
||||
* description:"description-classes",
|
||||
* selectedIcon:"selected-icon-classes",
|
||||
* shortcut:"shortcut-classes",
|
||||
|
||||
@ -12,8 +12,11 @@ export interface DropdownItemProps<T extends object = object> extends UseDropdow
|
||||
const DropdownItem = forwardRef<DropdownItemProps, "li">((props, _) => {
|
||||
const {
|
||||
Component,
|
||||
slots,
|
||||
styles,
|
||||
rendered,
|
||||
shortcut,
|
||||
description,
|
||||
isSelectable,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
@ -23,6 +26,7 @@ const DropdownItem = forwardRef<DropdownItemProps, "li">((props, _) => {
|
||||
disableAnimation,
|
||||
getItemProps,
|
||||
getLabelProps,
|
||||
getDescriptionProps,
|
||||
getKeyboardShortcutProps,
|
||||
getSelectedIconProps,
|
||||
} = useDropdownItem(props);
|
||||
@ -44,7 +48,14 @@ const DropdownItem = forwardRef<DropdownItemProps, "li">((props, _) => {
|
||||
return (
|
||||
<Component {...getItemProps()}>
|
||||
{startContent}
|
||||
<span {...getLabelProps()}>{rendered}</span>
|
||||
{description ? (
|
||||
<div className={slots.wrapper({class: styles?.wrapper})}>
|
||||
<span {...getLabelProps()}>{rendered}</span>
|
||||
<span {...getDescriptionProps()}>{description}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span {...getLabelProps()}>{rendered}</span>
|
||||
)}
|
||||
{endContent}
|
||||
{shortcut && <kbd {...getKeyboardShortcutProps()}>{shortcut}</kbd>}
|
||||
{isSelectable && <span {...getSelectedIconProps()}>{selectedContent}</span>}
|
||||
|
||||
@ -6,6 +6,7 @@ import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {AriaMenuProps} from "@react-types/menu";
|
||||
import {useTreeState} from "@react-stately/tree";
|
||||
|
||||
import DropdownSection from "./dropdown-section";
|
||||
import DropdownItem, {DropdownItemProps} from "./dropdown-item";
|
||||
import {useDropdownContext} from "./dropdown-context";
|
||||
|
||||
@ -65,23 +66,23 @@ const DropdownMenu = forwardRef<DropdownMenuProps, "ul">(
|
||||
<PopoverContent>
|
||||
<Component {...getMenuProps({...menuProps, className}, domRef)}>
|
||||
{[...state.collection].map((item) => {
|
||||
const itemProps = {
|
||||
key: item.key,
|
||||
closeOnSelect,
|
||||
color,
|
||||
disableAnimation,
|
||||
item,
|
||||
state,
|
||||
styles,
|
||||
variant,
|
||||
onAction,
|
||||
...item.props,
|
||||
};
|
||||
|
||||
if (item.type === "section") {
|
||||
return <div>Section</div>;
|
||||
return <DropdownSection {...itemProps} />;
|
||||
}
|
||||
let dropdownItem = (
|
||||
<DropdownItem
|
||||
key={item.key}
|
||||
closeOnSelect={closeOnSelect}
|
||||
color={color}
|
||||
disableAnimation={disableAnimation}
|
||||
item={item}
|
||||
state={state}
|
||||
styles={styles}
|
||||
variant={variant}
|
||||
onAction={onAction}
|
||||
{...item.props}
|
||||
/>
|
||||
);
|
||||
let dropdownItem = <DropdownItem {...itemProps} />;
|
||||
|
||||
if (item.wrapper) {
|
||||
dropdownItem = item.wrapper(dropdownItem);
|
||||
|
||||
126
packages/components/dropdown/src/dropdown-section.tsx
Normal file
126
packages/components/dropdown/src/dropdown-section.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import type {DropdownSectionSlots, SlotsToClasses} from "@nextui-org/theme";
|
||||
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
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 {mergeProps} from "@react-aria/utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
|
||||
import DropdownItem, {DropdownItemProps} from "./dropdown-item";
|
||||
|
||||
export interface DropdownSectionProps<T extends object = object> extends HTMLNextUIProps<"li"> {
|
||||
item: Node<T>;
|
||||
state: TreeState<T>;
|
||||
/**
|
||||
* The dropdown items variant.
|
||||
*/
|
||||
variant?: DropdownItemProps["variant"];
|
||||
/**
|
||||
* The dropdown items color.
|
||||
*/
|
||||
color?: DropdownItemProps["color"];
|
||||
/**
|
||||
* Whether to disable the items animation.
|
||||
* @default false
|
||||
*/
|
||||
disableAnimation?: boolean;
|
||||
/**
|
||||
* Whether the menu should close when the menu item is selected.
|
||||
* @default true
|
||||
*/
|
||||
closeOnSelect?: DropdownItemProps["closeOnSelect"];
|
||||
/**
|
||||
* The dropdown items styles.
|
||||
*/
|
||||
styles?: DropdownItemProps["styles"] & SlotsToClasses<DropdownSectionSlots>;
|
||||
/**
|
||||
* Shows a divider between sections
|
||||
* @default true
|
||||
*/
|
||||
showDivider?: boolean;
|
||||
onAction?: (key: Key) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const DropdownSection = forwardRef<DropdownSectionProps, "li">(
|
||||
(
|
||||
{
|
||||
item,
|
||||
state,
|
||||
as,
|
||||
variant,
|
||||
color,
|
||||
disableAnimation,
|
||||
onAction,
|
||||
closeOnSelect,
|
||||
className,
|
||||
styles: stylesProp = {},
|
||||
showDivider = true,
|
||||
...otherProps
|
||||
},
|
||||
_,
|
||||
) => {
|
||||
const {section, heading, ...styles} = stylesProp;
|
||||
|
||||
const Component = as || "li";
|
||||
const isFirstKey = item.key === state.collection.getFirstKey();
|
||||
|
||||
const slots = useMemo(() => dropdownSection({showDivider: showDivider && !isFirstKey}), [
|
||||
showDivider,
|
||||
isFirstKey,
|
||||
]);
|
||||
|
||||
const baseStyles = clsx(section, className, item.props?.className);
|
||||
|
||||
const {itemProps, headingProps, groupProps} = useMenuSection({
|
||||
heading: item.rendered,
|
||||
"aria-label": item["aria-label"],
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...mergeProps(itemProps, otherProps, item.props)}
|
||||
className={slots.section({class: baseStyles})}
|
||||
>
|
||||
{item.rendered && (
|
||||
<span {...headingProps} className={slots.heading({class: heading})}>
|
||||
{item.rendered}
|
||||
</span>
|
||||
)}
|
||||
<ul {...groupProps}>
|
||||
{[...item.childNodes].map((node) => {
|
||||
let dropdownItem = (
|
||||
<DropdownItem
|
||||
key={node.key}
|
||||
closeOnSelect={closeOnSelect}
|
||||
color={color}
|
||||
disableAnimation={disableAnimation}
|
||||
item={node}
|
||||
state={state}
|
||||
styles={styles}
|
||||
variant={variant}
|
||||
onAction={onAction}
|
||||
{...node.props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (node.wrapper) {
|
||||
dropdownItem = node.wrapper(dropdownItem);
|
||||
}
|
||||
|
||||
return dropdownItem;
|
||||
})}
|
||||
</ul>
|
||||
</Component>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DropdownSection.displayName = "NextUI.DropdownSection";
|
||||
|
||||
export default DropdownSection;
|
||||
@ -35,6 +35,7 @@ export function useDropdownItem<T extends object>(originalProps: UseDropdownItem
|
||||
item,
|
||||
state,
|
||||
shortcut,
|
||||
description,
|
||||
startContent,
|
||||
endContent,
|
||||
isVirtualized,
|
||||
@ -151,6 +152,7 @@ export function useDropdownItem<T extends object>(originalProps: UseDropdownItem
|
||||
isDisabled,
|
||||
rendered,
|
||||
shortcut,
|
||||
description,
|
||||
startContent,
|
||||
endContent,
|
||||
selectedIcon,
|
||||
|
||||
@ -2,6 +2,8 @@ import React from "react";
|
||||
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||
import {dropdown, popover} from "@nextui-org/theme";
|
||||
import {Button} from "@nextui-org/button";
|
||||
import {Avatar} from "@nextui-org/avatar";
|
||||
import {User} from "@nextui-org/user";
|
||||
import {
|
||||
AddNoteBulkIcon,
|
||||
CopyDocumentBulkIcon,
|
||||
@ -12,6 +14,7 @@ import {clsx} from "@nextui-org/shared-utils";
|
||||
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownSection,
|
||||
DropdownTrigger,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
@ -329,6 +332,197 @@ const WithStartContentTemplate: ComponentStory<any> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const WithDescriptionTemplate: ComponentStory<any> = ({
|
||||
color,
|
||||
variant,
|
||||
disableAnimation,
|
||||
...args
|
||||
}) => {
|
||||
const iconClasses = "text-2xl text-secondary pointer-events-none flex-shrink-0";
|
||||
|
||||
return (
|
||||
<Dropdown {...args} disableAnimation={disableAnimation}>
|
||||
<DropdownTrigger>
|
||||
<Button color="secondary" disableAnimation={disableAnimation} variant="flat">
|
||||
Trigger
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Actions" color={color} variant={variant} onAction={alert}>
|
||||
<DropdownItem
|
||||
key="new"
|
||||
description="Create a new file"
|
||||
shortcut="⌘N"
|
||||
startContent={<AddNoteBulkIcon className={iconClasses} />}
|
||||
>
|
||||
New file
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="copy"
|
||||
description="Copy the file link"
|
||||
shortcut="⌘C"
|
||||
startContent={<CopyDocumentBulkIcon className={iconClasses} />}
|
||||
>
|
||||
Copy link
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="edit"
|
||||
description="Allows you to edit the file"
|
||||
shortcut="⌘⇧E"
|
||||
startContent={<EditDocumentBulkIcon className={iconClasses} />}
|
||||
>
|
||||
Edit file
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
showDivider
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
description="Permanently delete the file"
|
||||
shortcut="⌘⇧D"
|
||||
startContent={<DeleteDocumentBulkIcon className={clsx(iconClasses, "!text-danger")} />}
|
||||
>
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
const WithSectionsTemplate: ComponentStory<any> = ({color, variant, disableAnimation, ...args}) => {
|
||||
const iconClasses = "text-2xl text-secondary pointer-events-none flex-shrink-0";
|
||||
|
||||
return (
|
||||
<Dropdown {...args} disableAnimation={disableAnimation}>
|
||||
<DropdownTrigger>
|
||||
<Button color="secondary" disableAnimation={disableAnimation} variant="flat">
|
||||
Trigger
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
aria-label="Actions"
|
||||
closeOnSelect={false}
|
||||
color={color}
|
||||
variant={variant}
|
||||
onAction={alert}
|
||||
>
|
||||
<DropdownSection title="Actions">
|
||||
<DropdownItem
|
||||
key="new"
|
||||
description="Create a new file"
|
||||
shortcut="⌘N"
|
||||
startContent={<AddNoteBulkIcon className={iconClasses} />}
|
||||
>
|
||||
New file
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="copy"
|
||||
description="Copy the file link"
|
||||
shortcut="⌘C"
|
||||
startContent={<CopyDocumentBulkIcon className={iconClasses} />}
|
||||
>
|
||||
Copy link
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="edit"
|
||||
description="Allows you to edit the file"
|
||||
shortcut="⌘⇧E"
|
||||
startContent={<EditDocumentBulkIcon className={iconClasses} />}
|
||||
>
|
||||
Edit file
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection title="Danger zone">
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
description="Permanently delete the file"
|
||||
shortcut="⌘⇧D"
|
||||
startContent={<DeleteDocumentBulkIcon className={clsx(iconClasses, "!text-danger")} />}
|
||||
>
|
||||
Delete file
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomTriggerTemplate: ComponentStory<any> = ({variant, ...args}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-10">
|
||||
<Dropdown {...args} placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Avatar
|
||||
isBordered
|
||||
as="button"
|
||||
color="secondary"
|
||||
size="lg"
|
||||
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Profile Actions" color="secondary" variant={variant}>
|
||||
<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>
|
||||
<Dropdown {...args} placement="bottom-start">
|
||||
<DropdownTrigger>
|
||||
<User
|
||||
as="button"
|
||||
avatarProps={{
|
||||
isBordered: true,
|
||||
color: "primary",
|
||||
size: "lg",
|
||||
src: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||
}}
|
||||
description="@tonyreichert"
|
||||
name="Tony Reichert"
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="User Actions" color="primary" variant={variant}>
|
||||
<DropdownItem key="profile" className="h-14 gap-2">
|
||||
<p className="font-bold">Signed in as</p>
|
||||
<p className="font-bold">@tonyreichert</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
...defaultProps,
|
||||
@ -372,6 +566,29 @@ WithStartContent.args = {
|
||||
color: "secondary",
|
||||
};
|
||||
|
||||
export const WithDescription = WithDescriptionTemplate.bind({});
|
||||
WithDescription.args = {
|
||||
...defaultProps,
|
||||
variant: "flat",
|
||||
color: "secondary",
|
||||
className: "min-w-[240px]",
|
||||
};
|
||||
|
||||
export const WithSections = WithSectionsTemplate.bind({});
|
||||
WithSections.args = {
|
||||
...defaultProps,
|
||||
variant: "flat",
|
||||
color: "secondary",
|
||||
className: "min-w-[240px]",
|
||||
};
|
||||
|
||||
export const WithCustomTrigger = CustomTriggerTemplate.bind({});
|
||||
WithCustomTrigger.args = {
|
||||
...defaultProps,
|
||||
variant: "flat",
|
||||
offset: 14,
|
||||
};
|
||||
|
||||
export const DisableAnimation = WithStartContentTemplate.bind({});
|
||||
DisableAnimation.args = {
|
||||
...defaultProps,
|
||||
|
||||
@ -44,8 +44,9 @@ const dropdownItem = tv({
|
||||
"outline-none",
|
||||
"cursor-pointer",
|
||||
],
|
||||
wrapper: "w-full flex flex-col items-start justify-center",
|
||||
title: "flex-1",
|
||||
description: [],
|
||||
description: ["text-xs", "text-neutral-500", "truncate", "group-hover:text-current"],
|
||||
selectedIcon: ["text-inherit", "w-3", "h-3", "flex-shrink-0"],
|
||||
shortcut: [
|
||||
"px-1",
|
||||
|
||||
45
packages/core/theme/src/components/dropdown-section.ts
Normal file
45
packages/core/theme/src/components/dropdown-section.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type {VariantProps} from "tailwind-variants";
|
||||
|
||||
import {tv} from "tailwind-variants";
|
||||
|
||||
/**
|
||||
* Dropdown wrapper **Tailwind Variants** component
|
||||
*
|
||||
* const { section, heading } = dropdownSection({...})
|
||||
*
|
||||
* @example
|
||||
* <div>
|
||||
* <button className={trigger()} aria-expanded="true/false">your trigger</button>
|
||||
* <div className={section()}>
|
||||
* // dropdown content
|
||||
* <span className={arrow()} data-placement="top/bottom/left/right..." /> // arrow
|
||||
* </div>
|
||||
* </div>
|
||||
*/
|
||||
const dropdownSection = tv({
|
||||
slots: {
|
||||
section: "relative mb-2",
|
||||
heading: "pl-1 text-xs text-neutral-500",
|
||||
},
|
||||
variants: {
|
||||
showDivider: {
|
||||
true: {
|
||||
heading: [
|
||||
"mt-2",
|
||||
"before-content-['']",
|
||||
"before:absolute",
|
||||
"before:-top-1",
|
||||
"before:left-0",
|
||||
"before:right-0",
|
||||
"before:h-px",
|
||||
"before:bg-neutral-200",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type DropdownSectionVariantProps = VariantProps<typeof dropdownSection>;
|
||||
export type DropdownSectionSlots = keyof ReturnType<typeof dropdownSection>;
|
||||
|
||||
export {dropdownSection};
|
||||
@ -31,8 +31,6 @@ const dropdown = tv({
|
||||
],
|
||||
menu: "w-full flex flex-col p-1",
|
||||
trigger: [],
|
||||
section: [],
|
||||
sectionHeading: [],
|
||||
},
|
||||
variants: {
|
||||
disableAnimation: {
|
||||
|
||||
@ -25,3 +25,4 @@ export * from "./circular-progress";
|
||||
export * from "./input";
|
||||
export * from "./dropdown";
|
||||
export * from "./dropdown-item";
|
||||
export * from "./dropdown-section";
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -862,12 +862,18 @@ importers:
|
||||
specifier: ^10.11.2
|
||||
version: 10.11.2(react-dom@18.2.0)(react@18.2.0)
|
||||
devDependencies:
|
||||
'@nextui-org/avatar':
|
||||
specifier: workspace:*
|
||||
version: link:../avatar
|
||||
'@nextui-org/button':
|
||||
specifier: workspace:*
|
||||
version: link:../button
|
||||
'@nextui-org/shared-icons':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-icons
|
||||
'@nextui-org/user':
|
||||
specifier: workspace:*
|
||||
version: link:../user
|
||||
'@react-types/menu':
|
||||
specifier: ^3.9.0
|
||||
version: 3.9.0(react@18.2.0)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user