From a4fd95b7fda5557f831c6c91eb6240dea502c87b Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Tue, 25 Feb 2025 16:47:49 -0700 Subject: [PATCH] dropdown selection done --- .../ui/checkbox/Checkbox.stories.tsx | 2 +- .../ui/dropdown/Dropdown.stories.tsx | 176 ++++++++++++++---- web/src/components/ui/dropdown/Dropdown.tsx | 19 +- .../components/ui/dropdown/DropdownBase.tsx | 47 ++++- .../ui/dropdown/SearchDropdown.stories.tsx | 6 +- 5 files changed, 202 insertions(+), 48 deletions(-) diff --git a/web/src/components/ui/checkbox/Checkbox.stories.tsx b/web/src/components/ui/checkbox/Checkbox.stories.tsx index 12efb235b..987456018 100644 --- a/web/src/components/ui/checkbox/Checkbox.stories.tsx +++ b/web/src/components/ui/checkbox/Checkbox.stories.tsx @@ -86,7 +86,7 @@ export const DisabledChecked: Story = { // Example with event handler export const WithOnChange: Story = { args: { - onCheckedChange: (checked: boolean) => console.log('Checked:', checked) + onCheckedChange: (checked: boolean) => alert(`Checked: ${checked}`) } }; diff --git a/web/src/components/ui/dropdown/Dropdown.stories.tsx b/web/src/components/ui/dropdown/Dropdown.stories.tsx index c4dbb507a..6d68a8d05 100644 --- a/web/src/components/ui/dropdown/Dropdown.stories.tsx +++ b/web/src/components/ui/dropdown/Dropdown.stories.tsx @@ -35,20 +35,20 @@ export const Basic: Story = { { id: '1', label: 'Profile', - onClick: () => console.log('Profile clicked'), + onClick: () => alert('Profile clicked'), loading: false, icon: }, { id: '2', label: 'Settings', - onClick: () => console.log('Settings clicked'), + onClick: () => alert('Settings clicked'), shortcut: '⌘S' }, { id: '3', label: 'Logout', - onClick: () => console.log('Logout clicked'), + onClick: () => alert('Logout clicked'), items: [ { id: '3-1', @@ -75,21 +75,21 @@ export const WithIconsAndShortcuts: Story = { label: 'Profile', icon: '👤', shortcut: '⌘P', - onClick: () => console.log('Profile clicked') + onClick: () => alert('Profile clicked') }, { id: '2', label: 'Settings', icon: '⚙️', shortcut: '⌘S', - onClick: () => console.log('Settings clicked') + onClick: () => alert('Settings clicked') }, { id: '3', label: 'Logout', icon: '🚪', shortcut: '⌘L', - onClick: () => console.log('Logout clicked') + onClick: () => alert('Logout clicked') } ], children: @@ -108,12 +108,12 @@ export const WithNestedItems: Story = { { id: '1-1', label: 'Option 1', - onClick: () => console.log('Option 1 clicked') + onClick: () => alert('Option 1 clicked') }, { id: '1-2', label: 'Option 2', - onClick: () => console.log('Option 2 clicked') + onClick: () => alert('Option 2 clicked') } ] }, @@ -124,12 +124,12 @@ export const WithNestedItems: Story = { { id: '2-1', label: 'Sub Option 1', - onClick: () => console.log('Sub Option 1 clicked') + onClick: () => alert('Sub Option 1 clicked') }, { id: '2-2', label: 'Sub Option 2', - onClick: () => console.log('Sub Option 2 clicked') + onClick: () => alert('Sub Option 2 clicked') } ] } @@ -145,18 +145,18 @@ export const WithDisabledItems: Story = { { id: '1', label: 'Available Option', - onClick: () => console.log('Available clicked') + onClick: () => alert('Available clicked') }, { id: '2', label: 'Disabled Option', disabled: true, - onClick: () => console.log('Should not be called') + onClick: () => alert('Should not be called') }, { id: '3', label: 'Another Available', - onClick: () => console.log('Another clicked') + onClick: () => alert('Another clicked') } ], children: @@ -173,12 +173,12 @@ export const CustomWidth: Story = { { id: '1', label: 'This is a very long menu item that might need wrapping', - onClick: () => console.log('Long item clicked') + onClick: () => alert('Long item clicked') }, { id: '2', label: 'Short item', - onClick: () => console.log('Short item clicked') + onClick: () => alert('Short item clicked') } ], children: @@ -192,29 +192,29 @@ export const WithLoadingItems: Story = { { id: '1', label: 'Normal Item', - onClick: () => console.log('Normal clicked') + onClick: () => alert('Normal clicked') }, { id: '2', label: 'Loading Item', loading: true, - onClick: () => console.log('Loading clicked') + onClick: () => alert('Loading clicked') }, { id: '3', label: 'Another Normal', - onClick: () => console.log('Another clicked') + onClick: () => alert('Another clicked') }, { type: 'divider' }, { id: '4', label: 'Option 4', - onClick: () => console.log('Option 4 clicked') + onClick: () => alert('Option 4 clicked') }, { id: '5', label: 'Option 5', - onClick: () => console.log('Option 5 clicked') + onClick: () => alert('Option 5 clicked') } ], children: @@ -230,17 +230,17 @@ export const WithSelectionSingle: Story = { id: '1', label: 'Option 1', selected: false, - onClick: () => console.log('Option 1 clicked') + onClick: () => alert('Option 1 clicked') }, { id: '2', label: 'Option 2', - onClick: () => console.log('Option 2 clicked') + onClick: () => alert('Option 2 clicked') }, { id: '3', label: 'Option 3 - Selected', - onClick: () => console.log('Option 3 clicked'), + onClick: () => alert('Option 3 clicked'), selected: true } ], @@ -257,32 +257,32 @@ export const WithSelectionMultiple: Story = { id: '1', label: 'Option 1', selected: selectedIds.has('1'), - onClick: () => console.log('Option 1 clicked') + onClick: () => alert('Option 1 clicked') }, { id: '2', label: 'Option 2', selected: selectedIds.has('2'), - onClick: () => console.log('Option 2 clicked') + onClick: () => alert('Option 2 clicked') }, { id: '3', label: 'Option 3', selected: selectedIds.has('3'), - onClick: () => console.log('Option 3 clicked') + onClick: () => alert('Option 3 clicked') }, { type: 'divider' as const }, { id: '4', label: 'Option 4', selected: selectedIds.has('4'), - onClick: () => console.log('Option 4 clicked') + onClick: () => alert('Option 4 clicked') }, { id: '5', label: 'Option 5', selected: selectedIds.has('5'), - onClick: () => console.log('Option 5 clicked') + onClick: () => alert('Option 5 clicked') } ]; @@ -319,14 +319,14 @@ export const WithSecondaryLabel: Story = { id: '1', label: 'Profile Settings', secondaryLabel: 'User preferences', - onClick: () => console.log('Profile clicked'), + onClick: () => alert('Profile clicked'), icon: }, { id: '2', label: 'Storage', secondaryLabel: '45GB used', - onClick: () => console.log('Storage clicked'), + onClick: () => alert('Storage clicked'), icon: }, { type: 'divider' }, @@ -334,7 +334,7 @@ export const WithSecondaryLabel: Story = { id: '3', label: 'Subscription', secondaryLabel: 'Pro Plan', - onClick: () => console.log('Subscription clicked'), + onClick: () => alert('Subscription clicked'), icon: } ], @@ -354,7 +354,7 @@ export const WithSearchHeader: Story = { label: 'Profile Settings', searchLabel: 'profile settings user preferences account', secondaryLabel: 'User preferences', - onClick: () => console.log('Profile clicked'), + onClick: () => alert('Profile clicked'), icon: }, { @@ -362,7 +362,7 @@ export const WithSearchHeader: Story = { label: 'Storage Options', searchLabel: 'storage disk space memory', secondaryLabel: 'Manage storage space', - onClick: () => console.log('Storage clicked'), + onClick: () => alert('Storage clicked'), icon: }, { @@ -370,7 +370,7 @@ export const WithSearchHeader: Story = { label: 'Favorites', searchLabel: 'favorites starred items bookmarks', secondaryLabel: 'View starred items', - onClick: () => console.log('Favorites clicked'), + onClick: () => alert('Favorites clicked'), icon: }, { type: 'divider' }, @@ -378,12 +378,12 @@ export const WithSearchHeader: Story = { id: '4', label: 'Logout', - onClick: () => console.log('Logout clicked') + onClick: () => alert('Logout clicked') }, { id: '5', label: 'Invite User', - onClick: () => console.log('Invite User clicked') + onClick: () => alert('Invite User clicked') } ], children: @@ -405,7 +405,7 @@ export const WithLongText: Story = { label, secondaryLabel, searchLabel: label + ' ' + secondaryLabel, - onClick: () => console.log('Long text clicked'), + onClick: () => alert('Long text clicked'), truncate: true }; }) @@ -413,3 +413,105 @@ export const WithLongText: Story = { children: } }; + +// Example with links +export const WithLinks: Story = { + args: { + menuHeader: 'Navigation Links', + items: [ + { + id: '1', + label: 'Documentation', + link: '/docs', + icon: , + onClick: () => alert('Documentation clicked') + }, + { + id: '2', + label: 'GitHub Repository', + link: 'https://github.com/example/repo', + icon: , + secondaryLabel: 'External Link' + }, + { type: 'divider' }, + { + id: '3', + label: 'Settings Page', + link: '/settings', + icon: + }, + { + id: '4', + label: 'Help Center', + link: '/help', + secondaryLabel: 'Get Support' + } + ], + children: + } +}; + +// Interactive example with links and multiple selection +export const WithLinksAndMultipleSelection: Story = { + render: () => { + const [selectedIds, setSelectedIds] = React.useState>(new Set(['2'])); + + const items: DropdownItems = [ + { + id: '1', + label: 'Documentation Home', + link: '/docs', + selected: selectedIds.has('1'), + icon: , + secondaryLabel: 'Main documentation' + }, + { + id: '2', + label: 'API Reference', + link: '/docs/api', + selected: selectedIds.has('2'), + icon: , + secondaryLabel: 'API documentation' + }, + { type: 'divider' as const }, + { + id: '3', + label: 'Tutorials', + link: '/docs/tutorials', + selected: selectedIds.has('3'), + icon: , + secondaryLabel: 'Learn step by step' + }, + { + id: '4', + label: 'Examples', + link: '/docs/examples', + selected: selectedIds.has('4'), + secondaryLabel: 'Code examples' + } + ]; + + const handleSelect = (itemId: string) => { + setSelectedIds((prev) => { + const newSet = new Set(prev); + if (newSet.has(itemId)) { + newSet.delete(itemId); + } else { + newSet.add(itemId); + } + return newSet; + }); + }; + + return ( + Documentation Sections} + /> + ); + } +}; diff --git a/web/src/components/ui/dropdown/Dropdown.tsx b/web/src/components/ui/dropdown/Dropdown.tsx index 77084da2b..e04166625 100644 --- a/web/src/components/ui/dropdown/Dropdown.tsx +++ b/web/src/components/ui/dropdown/Dropdown.tsx @@ -1,10 +1,11 @@ -import { DropdownMenuLabel, DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; +import { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; import React, { useMemo } from 'react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, //Do I need this? DropdownMenuItem, + DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuShortcut, @@ -13,7 +14,8 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, DropdownMenuCheckboxItemSingle, - DropdownMenuCheckboxItemMultiple + DropdownMenuCheckboxItemMultiple, + DropdownMenuLink } from './DropdownBase'; import { CircleSpinnerLoader } from '../loaders/CircleSpinnerLoader'; import { useMemoizedFn } from 'ahooks'; @@ -35,6 +37,8 @@ export interface DropdownItem { loading?: boolean; selected?: boolean; items?: DropdownItems; + link?: string; + linkIcon?: 'arrow-right' | 'arrow-external' | 'caret-right'; } export interface DropdownDivider { @@ -233,7 +237,9 @@ const DropdownItem: React.FC< onSelect, selectType, secondaryLabel, - truncate + truncate, + link, + linkIcon }) => { const onClickItem = useMemoizedFn((e: React.MouseEvent) => { if (onClick) onClick(); @@ -252,6 +258,13 @@ const DropdownItem: React.FC< {secondaryLabel && {secondaryLabel}} {shortcut && {shortcut}} + {link && ( + + )} ); diff --git a/web/src/components/ui/dropdown/DropdownBase.tsx b/web/src/components/ui/dropdown/DropdownBase.tsx index 1fee44d9b..b7943ea90 100644 --- a/web/src/components/ui/dropdown/DropdownBase.tsx +++ b/web/src/components/ui/dropdown/DropdownBase.tsx @@ -2,10 +2,19 @@ import * as React from 'react'; import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; -import { Check3 as Check, ChevronRight } from '../icons/NucleoIconOutlined'; +import { + ArrowRight, + ArrowUpRight, + CaretRight, + Check3 as Check, + ChevronRight +} from '../icons/NucleoIconOutlined'; import { cn } from '@/lib/classMerge'; import { Checkbox } from '../checkbox/Checkbox'; +import { Button } from '../buttons/Button'; +import { useRouter } from 'next/router'; +import Link from 'next/link'; const DropdownMenu = DropdownMenuPrimitive.Root; @@ -83,6 +92,7 @@ const DropdownMenuItem = React.forwardRef< 'focus:bg-item-hover focus:text-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', inset && 'pl-8', truncate && 'overflow-hidden', + 'group', className )} onClick={(e) => { @@ -100,7 +110,8 @@ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const itemClass = cn( 'focus:bg-item-hover focus:text-foreground', 'relative flex cursor-pointer items-center rounded-sm py-1.5 text-sm transition-colors outline-none select-none', - 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50' + 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50', + 'gap-1.5' ); const DropdownMenuCheckboxItemSingle = React.forwardRef< @@ -180,7 +191,7 @@ const DropdownMenuLabel = React.forwardRef< >(({ className, inset, ...props }, ref) => ( )); @@ -209,6 +220,33 @@ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes = ({ className, link, linkIcon = 'arrow-right', ...props }) => { + const icon = React.useMemo(() => { + if (linkIcon === 'arrow-right') return ; + if (linkIcon === 'arrow-external') return ; + if (linkIcon === 'caret-right') return ; + }, [linkIcon]); + + const isExternal = link?.startsWith('http'); + + return ( + +