| @@ -8,8 +8,9 @@ | ||
| 8 | 8 | useLayoutEffect, |
| 9 | 9 | useRef, |
| 10 | 10 | useState |
| 11 | 11 | } from 'react' |
| 12 | +import { KebabIcon } from './icons/KebabIcon' | |
| 12 | 13 | import type { Dispatch, PropsWithChildren, ReactNode, RefObject, SetStateAction } from 'react' |
| 13 | 14 | |
| 14 | 15 | const FOCUSABLE_SELECTOR = [ |
| 15 | 16 | 'button:not(:disabled)', |
| @@ -20,9 +21,9 @@ | ||
| 20 | 21 | '[tabindex]:not([tabindex="-1"])' |
| 21 | 22 | ].join(', ') |
| 22 | 23 | |
| 23 | 24 | interface KebabMenuContextValue { |
| 24 | - closeMenu: () => void | |
| 25 | + closeMenu: VoidFunction | |
| 25 | 26 | } |
| 26 | 27 | |
| 27 | 28 | const KebabMenuContext = createContext<KebabMenuContextValue>({ closeMenu: () => undefined }) |
| 28 | 29 | |
| @@ -28,9 +29,9 @@ | ||
| 28 | 29 | |
| 29 | 30 | export const useKebabMenu = (): KebabMenuContextValue => useContext(KebabMenuContext) |
| 30 | 31 | |
| 31 | 32 | export interface KebabMenuItemProps { |
| 32 | - onSelect?: () => void | |
| 33 | + onSelect?: VoidFunction | |
| 33 | 34 | destructive?: boolean |
| 34 | 35 | disabled?: boolean |
| 35 | 36 | className?: string |
| 36 | 37 | } |
| @@ -79,26 +80,12 @@ | ||
| 79 | 80 | <li className={classnames('kebab-menu-row', className)}> |
| 80 | 81 | {children} |
| 81 | 82 | </li> |
| 82 | 83 | |
| 83 | -const KebabIcon: React.FC = () => | |
| 84 | - <svg | |
| 85 | - width="18" | |
| 86 | - height="18" | |
| 87 | - viewBox="0 0 18 18" | |
| 88 | - fill="none" | |
| 89 | - xmlns="http://www.w3.org/2000/svg" | |
| 90 | - aria-hidden="true" | |
| 91 | - > | |
| 92 | - <circle cx="9" cy="3.5" r="1.6" fill="currentColor" /> | |
| 93 | - <circle cx="9" cy="9" r="1.6" fill="currentColor" /> | |
| 94 | - <circle cx="9" cy="14.5" r="1.6" fill="currentColor" /> | |
| 95 | - </svg> | |
| 96 | - | |
| 97 | 84 | interface PopoverBehaviourOptions { |
| 98 | 85 | isOpen: boolean |
| 99 | 86 | setIsOpen: Dispatch<SetStateAction<boolean>> |
| 100 | - closeMenu: () => void | |
| 87 | + closeMenu: VoidFunction | |
| 101 | 88 | containerRef: RefObject<HTMLDivElement> |
| 102 | 89 | triggerRef: RefObject<HTMLButtonElement> |
| 103 | 90 | popoverRef: RefObject<HTMLUListElement> |
| 104 | 91 | } |
| @@ -139,28 +126,39 @@ | ||
| 139 | 126 | if (!popoverRef.current) { |
| 140 | 127 | return |
| 141 | 128 | } |
| 142 | 129 | |
| 130 | + const active = document.activeElement | |
| 131 | + | |
| 132 | + const isFormInput = active instanceof HTMLElement && | |
| 133 | + (active.isContentEditable || active.matches('input, select, textarea')) | |
| 134 | + | |
| 135 | + if (isFormInput) { | |
| 136 | + return | |
| 137 | + } | |
| 138 | + | |
| 143 | 139 | const focusable = Array.from(popoverRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)) |
| 144 | - const active = document.activeElement | |
| 145 | 140 | const currentIndex = active instanceof HTMLElement ? focusable.indexOf(active) : -1 |
| 146 | 141 | |
| 147 | - if ( | |
| 148 | - 'ArrowDown' === event.key || | |
| 149 | - 'ArrowUp' === event.key || | |
| 150 | - 'Home' === event.key || | |
| 151 | - 'End' === event.key | |
| 152 | - ) { | |
| 153 | - if (0 < focusable.length) { | |
| 154 | - event.preventDefault() | |
| 155 | - const offset = 'ArrowDown' === event.key ? currentIndex + 1 : currentIndex - 1 | |
| 156 | - const target = 'Home' === event.key | |
| 157 | - ? 0 | |
| 158 | - : 'End' === event.key ? focusable.length - 1 : (offset + focusable.length) % focusable.length | |
| 159 | - focusable[target].focus() | |
| 142 | + if (0 < focusable.length && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) { | |
| 143 | + event.preventDefault() | |
| 144 | + | |
| 145 | + const getTarget = () => { | |
| 146 | + switch (event.key) { | |
| 147 | + case 'Home': | |
| 148 | + return 0 | |
| 149 | + | |
| 150 | + case 'End': | |
| 151 | + return focusable.length - 1 | |
| 152 | + | |
| 153 | + default: { | |
| 154 | + const offset = currentIndex + ('ArrowDown' === event.key ? 1 : -1) | |
| 155 | + return (offset + focusable.length) % focusable.length | |
| 156 | + } | |
| 157 | + } | |
| 160 | 158 | } |
| 161 | 159 | |
| 162 | - return | |
| 160 | + focusable[getTarget()].focus() | |
| 163 | 161 | } |
| 164 | 162 | } |
| 165 | 163 | |
| 166 | 164 | const usePopoverDismissal = ({ |