@duro-app/ui 5.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Table-C70zdSkM.js +1419 -0
- package/dist/Table-C70zdSkM.js.map +1 -0
- package/dist/components/Button/Button.d.ts +11 -2
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/Button.stories.d.ts +1 -0
- package/dist/components/Button/Button.stories.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts +1 -1
- package/dist/components/Combobox/Combobox.d.ts.map +1 -1
- package/dist/components/Combobox/styles.css.d.ts +0 -64
- package/dist/components/Combobox/styles.css.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.d.ts +11 -3
- package/dist/components/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.stories.d.ts +4 -0
- package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
- package/dist/components/Dialog/styles.css.d.ts +1 -0
- package/dist/components/Dialog/styles.css.d.ts.map +1 -1
- package/dist/components/DragDrop/styles.css.d.ts +10 -2
- package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
- package/dist/components/Grid/Grid.d.ts +11 -2
- package/dist/components/Grid/Grid.d.ts.map +1 -1
- package/dist/components/Grid/Grid.stories.d.ts +2 -0
- package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
- package/dist/components/Input/Input.d.ts +6 -1
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/Input/Input.meta.d.ts.map +1 -1
- package/dist/components/Input/Input.stories.d.ts +1 -0
- package/dist/components/Input/Input.stories.d.ts.map +1 -1
- package/dist/components/Input/styles.css.d.ts +18 -0
- package/dist/components/Input/styles.css.d.ts.map +1 -1
- package/dist/components/InputGroup/InputGroup.d.ts +6 -1
- package/dist/components/InputGroup/InputGroup.d.ts.map +1 -1
- package/dist/components/InputGroup/InputGroup.stories.d.ts +1 -0
- package/dist/components/InputGroup/InputGroup.stories.d.ts.map +1 -1
- package/dist/components/Listbox/Listbox.d.ts +75 -0
- package/dist/components/Listbox/Listbox.d.ts.map +1 -0
- package/dist/components/Listbox/Listbox.meta.d.ts +3 -0
- package/dist/components/Listbox/Listbox.meta.d.ts.map +1 -0
- package/dist/components/Listbox/Listbox.stories.d.ts +7 -0
- package/dist/components/Listbox/Listbox.stories.d.ts.map +1 -0
- package/dist/components/Listbox/styles.css.d.ts +75 -0
- package/dist/components/Listbox/styles.css.d.ts.map +1 -0
- package/dist/components/Menu/Menu.d.ts +18 -5
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/Menu.meta.d.ts.map +1 -1
- package/dist/components/Menu/Menu.stories.d.ts +7 -0
- package/dist/components/Menu/Menu.stories.d.ts.map +1 -1
- package/dist/components/Menu/MenuContext.d.ts +3 -0
- package/dist/components/Menu/MenuContext.d.ts.map +1 -1
- package/dist/components/Menu/styles.css.d.ts +24 -15
- package/dist/components/Menu/styles.css.d.ts.map +1 -1
- package/dist/components/Menu/useMenuRoot.d.ts +14 -1
- package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
- package/dist/components/TextLink/TextLink.d.ts +24 -0
- package/dist/components/TextLink/TextLink.d.ts.map +1 -0
- package/dist/components/TextLink/TextLink.meta.d.ts +3 -0
- package/dist/components/TextLink/TextLink.meta.d.ts.map +1 -0
- package/dist/components/TextLink/TextLink.stories.d.ts +9 -0
- package/dist/components/TextLink/TextLink.stories.d.ts.map +1 -0
- package/dist/components/TextLink/styles.css.d.ts +28 -0
- package/dist/components/TextLink/styles.css.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.stories.d.ts +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.stories.d.ts.map +1 -1
- package/dist/components/VisuallyHidden/VisuallyHidden.d.ts +14 -0
- package/dist/components/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
- package/dist/components/VisuallyHidden/VisuallyHidden.meta.d.ts +3 -0
- package/dist/components/VisuallyHidden/VisuallyHidden.meta.d.ts.map +1 -0
- package/dist/components/VisuallyHidden/VisuallyHidden.stories.d.ts +7 -0
- package/dist/components/VisuallyHidden/VisuallyHidden.stories.d.ts.map +1 -0
- package/dist/docs/FlexGrow.stories.d.ts +7 -0
- package/dist/docs/FlexGrow.stories.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2943 -2778
- package/dist/index.js.map +1 -1
- package/dist/index.native.d.ts +1 -0
- package/dist/index.native.d.ts.map +1 -1
- package/dist/styles/visually-hidden.css.d.ts +6 -0
- package/dist/styles/visually-hidden.css.d.ts.map +1 -1
- package/dist/table.js +1 -1
- package/package.json +2 -2
- package/src/components/Button/Button.stories.tsx +41 -0
- package/src/components/Button/Button.tsx +16 -1
- package/src/components/Combobox/Combobox.tsx +25 -79
- package/src/components/Combobox/styles.css.ts +0 -74
- package/src/components/Dialog/Dialog.meta.ts +1 -1
- package/src/components/Dialog/Dialog.stories.tsx +109 -3
- package/src/components/Dialog/Dialog.tsx +63 -9
- package/src/components/Dialog/styles.css.ts +2 -0
- package/src/components/DragDrop/styles.css.ts +16 -3
- package/src/components/Grid/Grid.meta.ts +1 -1
- package/src/components/Grid/Grid.stories.tsx +53 -1
- package/src/components/Grid/Grid.tsx +23 -11
- package/src/components/Input/Input.meta.ts +10 -3
- package/src/components/Input/Input.stories.tsx +52 -2
- package/src/components/Input/Input.tsx +12 -3
- package/src/components/Input/styles.css.ts +29 -0
- package/src/components/InputGroup/InputGroup.stories.tsx +20 -0
- package/src/components/InputGroup/InputGroup.tsx +20 -2
- package/src/components/Listbox/Listbox.meta.ts +52 -0
- package/src/components/Listbox/Listbox.stories.tsx +170 -0
- package/src/components/Listbox/Listbox.tsx +278 -0
- package/src/components/Listbox/styles.css.ts +89 -0
- package/src/components/Menu/Menu.meta.ts +2 -1
- package/src/components/Menu/Menu.stories.tsx +289 -8
- package/src/components/Menu/Menu.tsx +100 -29
- package/src/components/Menu/MenuContext.ts +3 -0
- package/src/components/Menu/styles.css.ts +31 -15
- package/src/components/Menu/useMenuRoot.ts +105 -48
- package/src/components/TextLink/TextLink.meta.ts +33 -0
- package/src/components/TextLink/TextLink.stories.tsx +63 -0
- package/src/components/TextLink/TextLink.tsx +46 -0
- package/src/components/TextLink/styles.css.ts +58 -0
- package/src/components/ThemeProvider/ThemeProvider.stories.tsx +50 -0
- package/src/components/VisuallyHidden/VisuallyHidden.meta.ts +19 -0
- package/src/components/VisuallyHidden/VisuallyHidden.stories.tsx +30 -0
- package/src/components/VisuallyHidden/VisuallyHidden.tsx +22 -0
- package/src/docs/FlexGrow.stories.tsx +83 -0
- package/src/index.native.ts +1 -0
- package/src/index.ts +5 -0
- package/src/styles/visually-hidden.css.ts +8 -0
- package/dist/Table-BkrgH0jb.js +0 -1199
- package/dist/Table-BkrgH0jb.js.map +0 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ReactNode,
|
|
3
|
+
type Ref,
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import {createPortal} from 'react-dom'
|
|
13
|
+
import {html} from 'react-strict-dom'
|
|
14
|
+
import {SPACING_PX} from '@duro-app/tokens/keys'
|
|
15
|
+
import {styles} from './styles.css'
|
|
16
|
+
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
17
|
+
import {computePopoverPosition} from '../Popover/position'
|
|
18
|
+
import {usePopoverLayer} from '../Popover/PopoverLayerContext'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A headless-state popup list of options for an input that keeps focus: a
|
|
22
|
+
* Combobox's text field, or a rich-text editor's typeahead (mentions, slash
|
|
23
|
+
* commands). The anchor element owns focus and the keyboard; it moves the
|
|
24
|
+
* highlight by setting `highlightedId`, and wires itself to the list with
|
|
25
|
+
* `getAnchorProps()` (aria-controls / aria-expanded / aria-activedescendant).
|
|
26
|
+
* A press on an option never takes focus from the anchor.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export type ListboxMaxHeight = 'listMaxH' | 'listMaxHSm'
|
|
30
|
+
|
|
31
|
+
const maxHeightMap = {
|
|
32
|
+
listMaxH: styles.maxHeightListMaxH,
|
|
33
|
+
listMaxHSm: styles.maxHeightListMaxHSm,
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
/** Default option id: the listbox id plus the value, made safe for an id. */
|
|
37
|
+
export function defaultOptionId(listboxId: string, value: string): string {
|
|
38
|
+
return `${listboxId}-option-${value.replace(/[^\w-]/g, (ch) => `_${ch.charCodeAt(0).toString(16)}`)}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ListboxAnchorProps {
|
|
42
|
+
role?: 'combobox'
|
|
43
|
+
'aria-controls': string | undefined
|
|
44
|
+
'aria-expanded': boolean
|
|
45
|
+
'aria-activedescendant': string | undefined
|
|
46
|
+
'aria-autocomplete': 'list'
|
|
47
|
+
'aria-haspopup': 'listbox'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ARIA for the element that keeps focus and drives the list. Spread it on an
|
|
52
|
+
* input, or set each attribute on a contenteditable root.
|
|
53
|
+
*/
|
|
54
|
+
export function getAnchorProps({
|
|
55
|
+
id,
|
|
56
|
+
open,
|
|
57
|
+
highlightedId,
|
|
58
|
+
}: {
|
|
59
|
+
id: string
|
|
60
|
+
open: boolean
|
|
61
|
+
highlightedId: string | null | undefined
|
|
62
|
+
}): ListboxAnchorProps {
|
|
63
|
+
return {
|
|
64
|
+
'aria-controls': open ? id : undefined,
|
|
65
|
+
'aria-expanded': open,
|
|
66
|
+
'aria-activedescendant': open && highlightedId ? highlightedId : undefined,
|
|
67
|
+
'aria-autocomplete': 'list',
|
|
68
|
+
'aria-haspopup': 'listbox',
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ListboxContextValue {
|
|
73
|
+
id: string
|
|
74
|
+
highlightedId: string | null
|
|
75
|
+
onHighlight?: (id: string | null) => void
|
|
76
|
+
onSelect?: (value: string) => void
|
|
77
|
+
getOptionId: (value: string) => string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ListboxContext = createContext<ListboxContextValue | null>(null)
|
|
81
|
+
|
|
82
|
+
function useListbox(part: string) {
|
|
83
|
+
const ctx = useContext(ListboxContext)
|
|
84
|
+
if (!ctx) throw new Error(`Listbox.${part} must be used within Listbox.Root`)
|
|
85
|
+
return ctx
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Root ---
|
|
89
|
+
|
|
90
|
+
interface RootProps {
|
|
91
|
+
/** DOM id of the list; pass the same to getAnchorProps. */
|
|
92
|
+
id: string
|
|
93
|
+
open: boolean
|
|
94
|
+
/** The rect the list is placed against: the input, or the editor's caret. */
|
|
95
|
+
anchor: () => DOMRect | null | undefined
|
|
96
|
+
/** Give the list the anchor's width (a Combobox under its input). */
|
|
97
|
+
matchAnchorWidth?: boolean
|
|
98
|
+
/** Id of the highlighted option, controlled by the anchor's keyboard handling. */
|
|
99
|
+
highlightedId: string | null
|
|
100
|
+
/** Pointer hover asks to move the highlight. */
|
|
101
|
+
onHighlight?: (id: string | null) => void
|
|
102
|
+
/** A press on an option. */
|
|
103
|
+
onSelect?: (value: string) => void
|
|
104
|
+
/** Option value → DOM id. Default: defaultOptionId(id, value). */
|
|
105
|
+
getOptionId?: (value: string) => string
|
|
106
|
+
/** Accessible name of the list. */
|
|
107
|
+
'aria-label'?: string
|
|
108
|
+
/** Size-token key capping the list's height; longer lists scroll. Default: listMaxH. */
|
|
109
|
+
maxHeight?: ListboxMaxHeight
|
|
110
|
+
children: ReactNode
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function Root({
|
|
114
|
+
id,
|
|
115
|
+
open,
|
|
116
|
+
anchor,
|
|
117
|
+
matchAnchorWidth = false,
|
|
118
|
+
highlightedId,
|
|
119
|
+
onHighlight,
|
|
120
|
+
onSelect,
|
|
121
|
+
getOptionId,
|
|
122
|
+
'aria-label': ariaLabel,
|
|
123
|
+
maxHeight = 'listMaxH',
|
|
124
|
+
children,
|
|
125
|
+
}: RootProps) {
|
|
126
|
+
const mount = usePortalMount()
|
|
127
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
128
|
+
const [place, setPlace] = useState<{top: number; left: number; width: number} | null>(null)
|
|
129
|
+
const anchorRef = useRef(anchor)
|
|
130
|
+
anchorRef.current = anchor
|
|
131
|
+
|
|
132
|
+
useLayoutEffect(() => {
|
|
133
|
+
if (!open) {
|
|
134
|
+
setPlace(null)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
const update = () => {
|
|
138
|
+
const rect = anchorRef.current()
|
|
139
|
+
if (!rect) return
|
|
140
|
+
const list = listRef.current
|
|
141
|
+
const next = computePopoverPosition({
|
|
142
|
+
anchor: {top: rect.top, left: rect.left, width: rect.width, height: rect.height},
|
|
143
|
+
popup: {
|
|
144
|
+
width: matchAnchorWidth ? rect.width : (list?.offsetWidth ?? 0),
|
|
145
|
+
height: list?.offsetHeight ?? 0,
|
|
146
|
+
},
|
|
147
|
+
viewport: {width: window.innerWidth, height: window.innerHeight},
|
|
148
|
+
side: 'bottom',
|
|
149
|
+
align: 'start',
|
|
150
|
+
offset: SPACING_PX.xs,
|
|
151
|
+
})
|
|
152
|
+
setPlace((prev) =>
|
|
153
|
+
prev && prev.top === next.top && prev.left === next.left && prev.width === rect.width
|
|
154
|
+
? prev
|
|
155
|
+
: {top: next.top, left: next.left, width: rect.width},
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
update()
|
|
159
|
+
// A second pass once the list has its size, so the flip sees its height.
|
|
160
|
+
const frame = requestAnimationFrame(update)
|
|
161
|
+
window.addEventListener('resize', update)
|
|
162
|
+
window.addEventListener('scroll', update, true)
|
|
163
|
+
return () => {
|
|
164
|
+
cancelAnimationFrame(frame)
|
|
165
|
+
window.removeEventListener('resize', update)
|
|
166
|
+
window.removeEventListener('scroll', update, true)
|
|
167
|
+
}
|
|
168
|
+
}, [open, matchAnchorWidth])
|
|
169
|
+
|
|
170
|
+
// A press inside the list must not move focus off the anchor.
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const list = listRef.current
|
|
173
|
+
if (!list || !open) return
|
|
174
|
+
const keep = (e: MouseEvent) => e.preventDefault()
|
|
175
|
+
list.addEventListener('mousedown', keep)
|
|
176
|
+
return () => list.removeEventListener('mousedown', keep)
|
|
177
|
+
}, [open, place])
|
|
178
|
+
|
|
179
|
+
// Keep the highlighted option in view.
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!open || !highlightedId) return
|
|
182
|
+
const list = listRef.current
|
|
183
|
+
const option = list?.querySelector<HTMLElement>(`[id="${CSS.escape(highlightedId)}"]`)
|
|
184
|
+
option?.scrollIntoView?.({block: 'nearest'})
|
|
185
|
+
}, [open, highlightedId])
|
|
186
|
+
|
|
187
|
+
usePopoverLayer(listRef, {active: open && place !== null})
|
|
188
|
+
|
|
189
|
+
const optionId = useCallback(
|
|
190
|
+
(value: string) => (getOptionId ? getOptionId(value) : defaultOptionId(id, value)),
|
|
191
|
+
[getOptionId, id],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if (!open || !place || typeof document === 'undefined') return null
|
|
195
|
+
|
|
196
|
+
const node = (
|
|
197
|
+
<ListboxContext.Provider
|
|
198
|
+
value={{id, highlightedId, onHighlight, onSelect, getOptionId: optionId}}
|
|
199
|
+
>
|
|
200
|
+
<html.div
|
|
201
|
+
ref={listRef}
|
|
202
|
+
id={id}
|
|
203
|
+
role="listbox"
|
|
204
|
+
aria-label={ariaLabel}
|
|
205
|
+
style={[
|
|
206
|
+
styles.popup,
|
|
207
|
+
maxHeightMap[maxHeight],
|
|
208
|
+
styles.position(place.top, place.left),
|
|
209
|
+
matchAnchorWidth ? styles.width(place.width) : styles.ownWidth,
|
|
210
|
+
]}
|
|
211
|
+
>
|
|
212
|
+
{children}
|
|
213
|
+
</html.div>
|
|
214
|
+
</ListboxContext.Provider>
|
|
215
|
+
)
|
|
216
|
+
return mount ? createPortal(node, mount) : node
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// --- Option ---
|
|
220
|
+
|
|
221
|
+
interface OptionProps {
|
|
222
|
+
value: string
|
|
223
|
+
/** DOM id. Default: Root's getOptionId(value). */
|
|
224
|
+
id?: string
|
|
225
|
+
/** The committed choice (aria-selected). */
|
|
226
|
+
selected?: boolean
|
|
227
|
+
disabled?: boolean
|
|
228
|
+
children: ReactNode
|
|
229
|
+
ref?: Ref<HTMLDivElement>
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function Option({
|
|
233
|
+
value,
|
|
234
|
+
id: idProp,
|
|
235
|
+
selected = false,
|
|
236
|
+
disabled = false,
|
|
237
|
+
children,
|
|
238
|
+
ref,
|
|
239
|
+
}: OptionProps) {
|
|
240
|
+
const {highlightedId, onHighlight, onSelect, getOptionId} = useListbox('Option')
|
|
241
|
+
const id = idProp ?? getOptionId(value)
|
|
242
|
+
const highlighted = highlightedId === id
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<html.div
|
|
246
|
+
ref={ref}
|
|
247
|
+
id={id}
|
|
248
|
+
role="option"
|
|
249
|
+
aria-selected={selected}
|
|
250
|
+
aria-disabled={disabled || undefined}
|
|
251
|
+
onClick={disabled ? undefined : () => onSelect?.(value)}
|
|
252
|
+
onPointerEnter={disabled ? undefined : () => onHighlight?.(id)}
|
|
253
|
+
style={[
|
|
254
|
+
styles.option,
|
|
255
|
+
selected && styles.optionSelected,
|
|
256
|
+
highlighted && styles.optionHighlighted,
|
|
257
|
+
disabled && styles.optionDisabled,
|
|
258
|
+
]}
|
|
259
|
+
>
|
|
260
|
+
{children}
|
|
261
|
+
</html.div>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Empty ---
|
|
266
|
+
|
|
267
|
+
/** Shown in place of options when nothing matches. */
|
|
268
|
+
function Empty({children}: {children: ReactNode}) {
|
|
269
|
+
useListbox('Empty')
|
|
270
|
+
return <html.div style={styles.empty}>{children}</html.div>
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export const Listbox = {
|
|
274
|
+
Root,
|
|
275
|
+
Option,
|
|
276
|
+
Empty,
|
|
277
|
+
getAnchorProps,
|
|
278
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
|
+
import {duration} from '@duro-app/tokens/tokens/motion.css'
|
|
7
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
8
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
9
|
+
|
|
10
|
+
export const styles = css.create({
|
|
11
|
+
popup: {
|
|
12
|
+
// Fixed, with top/left (and width) applied inline from the anchor's rect,
|
|
13
|
+
// so the list escapes any ancestor with overflow: hidden or a transform.
|
|
14
|
+
position: 'fixed',
|
|
15
|
+
top: 0,
|
|
16
|
+
left: 0,
|
|
17
|
+
backgroundColor: colors.bgCard,
|
|
18
|
+
borderWidth: borders.hairline,
|
|
19
|
+
borderStyle: 'solid',
|
|
20
|
+
borderColor: colors.border,
|
|
21
|
+
borderRadius: radii.sm,
|
|
22
|
+
boxShadow: shadows.md,
|
|
23
|
+
paddingTop: spacing.xs,
|
|
24
|
+
paddingBottom: spacing.xs,
|
|
25
|
+
overflowY: 'auto',
|
|
26
|
+
// Above Dialog/Drawer (1000/1001): they portal into the same mount.
|
|
27
|
+
zIndex: 1050,
|
|
28
|
+
// The portal mount is pointer-events: none; the list takes them back.
|
|
29
|
+
pointerEvents: 'auto',
|
|
30
|
+
},
|
|
31
|
+
// A list that does not take the anchor's width (an editor typeahead).
|
|
32
|
+
ownWidth: {
|
|
33
|
+
minWidth: sizes.popupMinW,
|
|
34
|
+
},
|
|
35
|
+
maxHeightListMaxH: {
|
|
36
|
+
maxHeight: sizes.listMaxH,
|
|
37
|
+
},
|
|
38
|
+
maxHeightListMaxHSm: {
|
|
39
|
+
maxHeight: sizes.listMaxHSm,
|
|
40
|
+
},
|
|
41
|
+
position: (top: number, left: number) => ({
|
|
42
|
+
top,
|
|
43
|
+
left,
|
|
44
|
+
}),
|
|
45
|
+
width: (width: number) => ({
|
|
46
|
+
width,
|
|
47
|
+
}),
|
|
48
|
+
option: {
|
|
49
|
+
display: 'flex',
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
paddingTop: spacing.sm,
|
|
52
|
+
paddingBottom: spacing.sm,
|
|
53
|
+
paddingLeft: spacing.md,
|
|
54
|
+
paddingRight: spacing.md,
|
|
55
|
+
fontSize: typography.fontSizeSm,
|
|
56
|
+
fontFamily: typography.fontFamily,
|
|
57
|
+
color: colors.text,
|
|
58
|
+
cursor: 'pointer',
|
|
59
|
+
backgroundColor: 'transparent',
|
|
60
|
+
transitionProperty: 'background-color',
|
|
61
|
+
transitionDuration: duration.fast,
|
|
62
|
+
},
|
|
63
|
+
optionSelected: {
|
|
64
|
+
color: colors.accent,
|
|
65
|
+
fontWeight: typography.fontWeightMedium,
|
|
66
|
+
},
|
|
67
|
+
// The fill is the highlight; the transparent outline is what forced-colors
|
|
68
|
+
// mode paints, where fills are dropped.
|
|
69
|
+
optionHighlighted: {
|
|
70
|
+
backgroundColor: colors.bgCardHover,
|
|
71
|
+
outlineStyle: 'solid',
|
|
72
|
+
outlineWidth: borders.focusRing,
|
|
73
|
+
outlineColor: 'transparent',
|
|
74
|
+
outlineOffset: `calc(-1 * ${borders.focusRing})`,
|
|
75
|
+
},
|
|
76
|
+
optionDisabled: {
|
|
77
|
+
color: colors.textMuted,
|
|
78
|
+
cursor: 'default',
|
|
79
|
+
},
|
|
80
|
+
empty: {
|
|
81
|
+
paddingTop: spacing.sm,
|
|
82
|
+
paddingBottom: spacing.sm,
|
|
83
|
+
paddingLeft: spacing.md,
|
|
84
|
+
paddingRight: spacing.md,
|
|
85
|
+
fontSize: typography.fontSizeSm,
|
|
86
|
+
fontFamily: typography.fontFamily,
|
|
87
|
+
color: colors.textMuted,
|
|
88
|
+
},
|
|
89
|
+
})
|
|
@@ -2,11 +2,12 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button.',
|
|
5
|
+
'Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button; an icon-only trigger takes aria-label, and variant="ghost" suits toolbars. The popup is portalled and placed against the trigger (never clipped by a sticky or overflow ancestor), capped at sizes.listMaxH with scrolling, and takes focus on open (WAI-ARIA menu button); Escape closes only the menu, not an enclosing Popover or Dialog.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Context menu or "more actions" dropdown',
|
|
8
8
|
'Navigation links in a dropdown',
|
|
9
9
|
'Overflow menu for toolbar actions',
|
|
10
|
+
'A toolbar dropdown (align, insert) in an editor — Menu.Trigger variant="ghost"',
|
|
10
11
|
],
|
|
11
12
|
whenNotToUse: [
|
|
12
13
|
'Picking a value — use Select',
|