From ad3efaa7d550901cdcb6bf91669df053dcfd14af Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Mon, 3 Mar 2025 19:53:59 -0700 Subject: [PATCH] better dropdown handling --- .../StatusBadgeButton.tsx | 7 +- .../ui/dropdown/Dropdown.stories.tsx | 19 +- web/src/components/ui/dropdown/Dropdown.tsx | 176 ++++++++++-------- 3 files changed, 116 insertions(+), 86 deletions(-) diff --git a/web/src/components/features/metrics/StatusBadgeIndicator/StatusBadgeButton.tsx b/web/src/components/features/metrics/StatusBadgeIndicator/StatusBadgeButton.tsx index e243f78d8..2d1c595e6 100644 --- a/web/src/components/features/metrics/StatusBadgeIndicator/StatusBadgeButton.tsx +++ b/web/src/components/features/metrics/StatusBadgeIndicator/StatusBadgeButton.tsx @@ -71,17 +71,12 @@ export const StatusBadgeButton: React.FC<{ !isAdmin && status === VerificationStatus.notRequested && !isOpenDropdown; const buttonText = Array.isArray(id) ? 'Status' : ''; - console.log(showButtonTooltip); - return ( { - console.log(v); - setIsOpenDropdown(v); - }}> + onOpenChange={setIsOpenDropdown}> } }; + +export const WithNumberedItemsWithFilter: Story = { + args: { + ...{ ...WithNumberedItemsNoFilter.args }, + menuHeader: 'Search items...' + } +}; diff --git a/web/src/components/ui/dropdown/Dropdown.tsx b/web/src/components/ui/dropdown/Dropdown.tsx index af141d052..587948efe 100644 --- a/web/src/components/ui/dropdown/Dropdown.tsx +++ b/web/src/components/ui/dropdown/Dropdown.tsx @@ -130,6 +130,26 @@ export const _Dropdown = ({ const dropdownItems = selectType === 'multiple' ? unselectedItems : filteredItems; + const onSelectItem = useMemoizedFn((index: number) => { + const correctIndex = dropdownItems.filter((item) => (item as DropdownItem).value); + const item = correctIndex[index] as DropdownItem; + + if (item) { + const disabled = (item as DropdownItem).disabled; + if (!disabled && onSelect) { + onSelect(item.value); + // Close the dropdown if closeOnSelect is true + if (closeOnSelect) { + const dropdownTrigger = document.querySelector('[data-state="open"][role="menu"]'); + if (dropdownTrigger) { + const closeEvent = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }); + dropdownTrigger.dispatchEvent(closeEvent); + } + } + } + } + }); + return ( ({ menuHeader={menuHeader} onChange={handleSearchChange} text={searchText} + onSelectItem={onSelectItem} showIndex={showIndex} /> @@ -171,6 +192,7 @@ export const _Dropdown = ({ index={hotkeyIndex} selectType={selectType} onSelect={onSelect} + onSelectItem={onSelectItem} closeOnSelect={closeOnSelect} showIndex={showIndex} /> @@ -191,6 +213,7 @@ export const _Dropdown = ({ index={hotkeyIndex} selectType={selectType} onSelect={onSelect} + onSelectItem={onSelectItem} closeOnSelect={closeOnSelect} key={dropdownItemKey(item, hotkeyIndex)} showIndex={showIndex} @@ -216,6 +239,7 @@ const DropdownItemSelector = React.memo( item, index, onSelect, + onSelectItem, closeOnSelect, selectType, showIndex @@ -223,6 +247,7 @@ const DropdownItemSelector = React.memo( item: DropdownItems[number]; index: number; onSelect?: (value: any) => void; // Using any here to resolve the type mismatch + onSelectItem: (index: number) => void; closeOnSelect: boolean; showIndex: boolean; selectType: DropdownProps['selectType']; @@ -240,6 +265,7 @@ const DropdownItemSelector = React.memo( {...(item as DropdownItem)} closeOnSelect={closeOnSelect} onSelect={onSelect} + onSelectItem={onSelectItem} selectType={selectType} index={index} showIndex={showIndex} @@ -255,13 +281,14 @@ const DropdownItem = ({ shortcut, onClick, icon, - disabled, + disabled = false, loading, selected, index, items, closeOnSelect, onSelect, + onSelectItem, selectType, secondaryLabel, truncate, @@ -269,6 +296,7 @@ const DropdownItem = ({ linkIcon }: DropdownItem & { onSelect?: (value: T) => void; + onSelectItem: (index: number) => void; closeOnSelect: boolean; index: number; showIndex: boolean; @@ -278,26 +306,12 @@ const DropdownItem = ({ if (onClick) onClick(); if (onSelect) onSelect(value as T); }); + const enabledHotKeys = showIndex && !disabled && !!onSelectItem; // Add hotkey support when showIndex is true - useHotkeys( - showIndex ? `${index}` : '', - (e) => { - e.preventDefault(); - if (!disabled) { - onClickItem(e as unknown as React.MouseEvent); - // Close the dropdown if closeOnSelect is true - if (closeOnSelect) { - const dropdownTrigger = document.querySelector('[data-state="open"][role="menu"]'); - if (dropdownTrigger) { - const closeEvent = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }); - dropdownTrigger.dispatchEvent(closeEvent); - } - } - } - }, - { enabled: showIndex && !disabled } - ); + useHotkeys(showIndex ? `${index}` : '', (e) => onSelectItem(index), { + enabled: enabledHotKeys + }); const isSubItem = items && items.length > 0; const isSelectable = !!selectType && selectType !== 'none'; @@ -341,6 +355,7 @@ const DropdownItem = ({ items={items} closeOnSelect={closeOnSelect} onSelect={onSelect} + onSelectItem={onSelectItem} showIndex={showIndex} selectType={selectType}> {renderContent()} @@ -390,6 +405,7 @@ interface DropdownSubMenuWrapperProps { closeOnSelect: boolean; showIndex: boolean; onSelect?: (value: T) => void; + onSelectItem: (index: number) => void; selectType: DropdownProps['selectType']; } @@ -398,6 +414,7 @@ const DropdownSubMenuWrapper = ({ children, closeOnSelect, onSelect, + onSelectItem, selectType, showIndex }: DropdownSubMenuWrapperProps) => { @@ -412,6 +429,7 @@ const DropdownSubMenuWrapper = ({ item={item} index={index} onSelect={onSelect} + onSelectItem={onSelectItem} closeOnSelect={closeOnSelect} selectType={selectType} showIndex={showIndex} @@ -423,72 +441,80 @@ const DropdownSubMenuWrapper = ({ ); }; -const DropdownMenuHeaderSelector = React.memo( - ({ - menuHeader, - onChange, - text, - showIndex - }: { - menuHeader: NonNullable['menuHeader']>; - onChange: (text: string) => void; - text: string; - showIndex: boolean; - }) => { - if (typeof menuHeader === 'string') { - return ( - - ); - } - return menuHeader; +const DropdownMenuHeaderSelector = ({ + menuHeader, + onChange, + onSelectItem, + text, + showIndex +}: { + menuHeader: NonNullable['menuHeader']>; + onSelectItem: (index: number) => void; + onChange: (text: string) => void; + text: string; + showIndex: boolean; +}) => { + if (typeof menuHeader === 'string') { + return ( + + ); } -); - + return menuHeader; +}; DropdownMenuHeaderSelector.displayName = 'DropdownMenuHeaderSelector'; -interface DropdownMenuHeaderSearchProps { +interface DropdownMenuHeaderSearchProps { text: string; onChange: (text: string) => void; + onSelectItem: (index: number) => void; placeholder?: string; showIndex: boolean; } -const DropdownMenuHeaderSearch = React.memo( - ({ text, onChange, showIndex, placeholder }: DropdownMenuHeaderSearchProps) => { - const onChangePreflight = useMemoizedFn((e: React.ChangeEvent) => { - e.stopPropagation(); +const DropdownMenuHeaderSearch = ({ + text, + onChange, + onSelectItem, + showIndex, + placeholder +}: DropdownMenuHeaderSearchProps) => { + const onChangePreflight = useMemoizedFn((e: React.ChangeEvent) => { + e.stopPropagation(); + e.preventDefault(); + onChange(e.target.value); + }); + + const onKeyDownPreflight = useMemoizedFn((e: React.KeyboardEvent) => { + const isFirstCharacter = (e.target as HTMLInputElement).value.length === 0; + + // Only prevent default for digit shortcuts when showIndex is true + if (showIndex && isFirstCharacter && /^Digit[0-9]$/.test(e.code)) { e.preventDefault(); - onChange(e.target.value); - }); + const index = parseInt(e.key); + onSelectItem?.(index); + } else { + e.stopPropagation(); + } + }); - const onKeyDownPreflight = useMemoizedFn((e: React.KeyboardEvent) => { - if (showIndex && !isNaN(Number(e.currentTarget.value))) { - const isCurrentValueNumber = e.code.includes('Digit'); - if (isCurrentValueNumber) { - e.preventDefault(); - e.stopPropagation(); - } - } - }); - - return ( -
- -
- ); - } -); + return ( +
+ +
+ ); +}; DropdownMenuHeaderSearch.displayName = 'DropdownMenuHeaderSearch';