PluginProbe
Code Snippets / trunk
Code Snippets vtrunk
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
← All changes | js/components/common/KebabMenu.tsx +30 -32 3.10.0-beta.1 → trunk View file →
@@ -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 = ({