feat(dropdown): storybook completed

This commit is contained in:
Junior Garcia 2023-04-08 19:55:49 -03:00
parent 5f8021a44b
commit 8ab3deff93
12 changed files with 430 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

@ -31,8 +31,6 @@ const dropdown = tv({
],
menu: "w-full flex flex-col p-1",
trigger: [],
section: [],
sectionHeading: [],
},
variants: {
disableAnimation: {

View File

@ -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
View File

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