@duro-app/ui 5.2.0 → 5.3.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 → Table-Cm5cCnZD.js} +730 -625
- package/dist/Table-Cm5cCnZD.js.map +1 -0
- package/dist/components/ActionBar/styles.css.d.ts +1 -1
- package/dist/components/ActionBar/styles.css.d.ts.map +1 -1
- package/dist/components/Badge/Badge.d.ts +1 -1
- package/dist/components/Badge/Badge.d.ts.map +1 -1
- package/dist/components/Badge/Badge.stories.d.ts +3 -0
- package/dist/components/Badge/Badge.stories.d.ts.map +1 -1
- package/dist/components/Badge/styles.css.d.ts +4 -0
- package/dist/components/Badge/styles.css.d.ts.map +1 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.d.ts +9 -2
- package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +19 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroupContext.d.ts +13 -0
- package/dist/components/ButtonGroup/ButtonGroupContext.d.ts.map +1 -0
- package/dist/components/ButtonGroup/attached.css.d.ts +34 -0
- package/dist/components/ButtonGroup/attached.css.d.ts.map +1 -0
- package/dist/components/ButtonGroup/styles.css.d.ts +7 -0
- package/dist/components/ButtonGroup/styles.css.d.ts.map +1 -1
- package/dist/components/ButtonGroup/useGroupedControl.d.ts +25 -0
- package/dist/components/ButtonGroup/useGroupedControl.d.ts.map +1 -0
- package/dist/components/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/Dialog/styles.css.d.ts +2 -2
- package/dist/components/Dialog/styles.css.d.ts.map +1 -1
- package/dist/components/DragDrop/DragDrop.d.ts.map +1 -1
- package/dist/components/DragDrop/styles.css.d.ts +1 -1
- package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/styles.css.d.ts +2 -2
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Listbox/styles.css.d.ts +1 -1
- package/dist/components/Listbox/styles.css.d.ts.map +1 -1
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/styles.css.d.ts +1 -1
- package/dist/components/Menu/styles.css.d.ts.map +1 -1
- package/dist/components/Popover/Popover.d.ts +7 -2
- package/dist/components/Popover/Popover.d.ts.map +1 -1
- package/dist/components/Popover/styles.css.d.ts +4 -1
- package/dist/components/Popover/styles.css.d.ts.map +1 -1
- package/dist/components/ScrollArea/styles.css.d.ts +1 -1
- package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.stories.d.ts +2 -0
- package/dist/components/Select/Select.stories.d.ts.map +1 -1
- package/dist/components/Select/styles.css.d.ts +2 -2
- package/dist/components/Select/styles.css.d.ts.map +1 -1
- package/dist/components/Select/useSelectRoot.d.ts.map +1 -1
- package/dist/components/ThemeProvider/Layers.stories.d.ts +24 -0
- package/dist/components/ThemeProvider/Layers.stories.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
- package/dist/components/Toast/styles.css.d.ts +1 -0
- package/dist/components/Toast/styles.css.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/styles.css.d.ts +3 -0
- package/dist/components/Toggle/styles.css.d.ts.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts.map +1 -1
- package/dist/components/ToggleGroup/styles.css.d.ts +5 -0
- package/dist/components/ToggleGroup/styles.css.d.ts.map +1 -1
- package/dist/components/Toolbar/ControlContextBoundary.d.ts +10 -0
- package/dist/components/Toolbar/ControlContextBoundary.d.ts.map +1 -0
- package/dist/components/Toolbar/Toolbar.d.ts +22 -0
- package/dist/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/components/Toolbar/Toolbar.meta.d.ts +3 -0
- package/dist/components/Toolbar/Toolbar.meta.d.ts.map +1 -0
- package/dist/components/Toolbar/Toolbar.stories.d.ts +28 -0
- package/dist/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/components/Toolbar/ToolbarContext.d.ts +10 -0
- package/dist/components/Toolbar/ToolbarContext.d.ts.map +1 -0
- package/dist/components/Toolbar/styles.css.d.ts +16 -0
- package/dist/components/Toolbar/styles.css.d.ts.map +1 -0
- package/dist/components/Tooltip/styles.css.d.ts +1 -1
- package/dist/components/Tooltip/styles.css.d.ts.map +1 -1
- package/dist/components/VirtualTable/styles.css.d.ts +1 -1
- package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3164 -2917
- package/dist/index.js.map +1 -1
- package/dist/shared/ModalContext.d.ts +14 -0
- package/dist/shared/ModalContext.d.ts.map +1 -0
- package/dist/shared/mergeRefs.d.ts +4 -0
- package/dist/shared/mergeRefs.d.ts.map +1 -0
- package/dist/shared/useOrderedRegistry.d.ts +26 -0
- package/dist/shared/useOrderedRegistry.d.ts.map +1 -0
- package/dist/table.js +45 -45
- package/dist/table.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar/styles.css.ts +2 -1
- package/src/components/Badge/Badge.stories.tsx +62 -1
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Badge/styles.css.ts +5 -0
- package/src/components/Button/Button.tsx +7 -1
- package/src/components/ButtonGroup/ButtonGroup.meta.ts +14 -1
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +271 -1
- package/src/components/ButtonGroup/ButtonGroup.tsx +46 -14
- package/src/components/ButtonGroup/ButtonGroupContext.ts +19 -0
- package/src/components/ButtonGroup/attached.css.ts +44 -0
- package/src/components/ButtonGroup/styles.css.ts +8 -0
- package/src/components/ButtonGroup/useGroupedControl.ts +75 -0
- package/src/components/Dialog/Dialog.tsx +5 -1
- package/src/components/Dialog/styles.css.ts +3 -2
- package/src/components/DragDrop/DragDrop.tsx +15 -8
- package/src/components/DragDrop/styles.css.ts +4 -1
- package/src/components/Drawer/Drawer.tsx +5 -1
- package/src/components/Drawer/styles.css.ts +3 -2
- package/src/components/Listbox/styles.css.ts +2 -1
- package/src/components/Menu/Menu.tsx +10 -3
- package/src/components/Menu/styles.css.ts +2 -1
- package/src/components/Popover/Popover.tsx +26 -5
- package/src/components/Popover/styles.css.ts +9 -1
- package/src/components/ScrollArea/styles.css.ts +2 -1
- package/src/components/Select/Select.stories.tsx +20 -1
- package/src/components/Select/Select.tsx +9 -3
- package/src/components/Select/styles.css.ts +3 -2
- package/src/components/Select/useSelectRoot.ts +17 -0
- package/src/components/ThemeProvider/Layers.stories.tsx +379 -0
- package/src/components/ThemeProvider/ThemeProvider.tsx +4 -2
- package/src/components/Toast/styles.css.ts +3 -0
- package/src/components/Toggle/Toggle.tsx +20 -4
- package/src/components/Toggle/styles.css.ts +6 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +16 -4
- package/src/components/ToggleGroup/styles.css.ts +7 -0
- package/src/components/Toolbar/ControlContextBoundary.tsx +16 -0
- package/src/components/Toolbar/Toolbar.meta.ts +48 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +468 -0
- package/src/components/Toolbar/Toolbar.tsx +113 -0
- package/src/components/Toolbar/ToolbarContext.ts +15 -0
- package/src/components/Toolbar/styles.css.ts +17 -0
- package/src/components/Tooltip/styles.css.ts +2 -1
- package/src/components/VirtualTable/styles.css.ts +2 -1
- package/src/index.ts +2 -1
- package/src/shared/ModalContext.ts +18 -0
- package/src/shared/mergeRefs.ts +11 -0
- package/src/shared/useOrderedRegistry.ts +57 -0
- package/dist/Table-C70zdSkM.js.map +0 -1
|
@@ -6,6 +6,7 @@ import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
|
6
6
|
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
7
7
|
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
8
8
|
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
9
|
+
import {layers} from '@duro-app/tokens/tokens/layers.css'
|
|
9
10
|
|
|
10
11
|
export const styles = css.create({
|
|
11
12
|
// --- Backdrop ---
|
|
@@ -13,7 +14,7 @@ export const styles = css.create({
|
|
|
13
14
|
position: 'fixed',
|
|
14
15
|
inset: 0,
|
|
15
16
|
backgroundColor: colors.scrim,
|
|
16
|
-
zIndex:
|
|
17
|
+
zIndex: layers.overlay,
|
|
17
18
|
// Portaled into the ThemeProvider mount, which is pointer-events: none:
|
|
18
19
|
// the backdrop must take clicks back (outside click dismisses).
|
|
19
20
|
pointerEvents: 'auto',
|
|
@@ -41,7 +42,7 @@ export const styles = css.create({
|
|
|
41
42
|
viewport: {
|
|
42
43
|
position: 'fixed',
|
|
43
44
|
inset: 0,
|
|
44
|
-
zIndex:
|
|
45
|
+
zIndex: layers.modal,
|
|
45
46
|
display: 'flex',
|
|
46
47
|
pointerEvents: 'none',
|
|
47
48
|
},
|
|
@@ -6,6 +6,7 @@ import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
|
6
6
|
import {duration} from '@duro-app/tokens/tokens/motion.css'
|
|
7
7
|
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
8
8
|
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
9
|
+
import {layers} from '@duro-app/tokens/tokens/layers.css'
|
|
9
10
|
|
|
10
11
|
export const styles = css.create({
|
|
11
12
|
popup: {
|
|
@@ -24,7 +25,7 @@ export const styles = css.create({
|
|
|
24
25
|
paddingBottom: spacing.xs,
|
|
25
26
|
overflowY: 'auto',
|
|
26
27
|
// Above Dialog/Drawer (1000/1001): they portal into the same mount.
|
|
27
|
-
zIndex:
|
|
28
|
+
zIndex: layers.popup,
|
|
28
29
|
// The portal mount is pointer-events: none; the list takes them back.
|
|
29
30
|
pointerEvents: 'auto',
|
|
30
31
|
},
|
|
@@ -18,6 +18,8 @@ import {devWarnOnce} from '../../shared/devWarnOnce'
|
|
|
18
18
|
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
19
19
|
import {computePopoverPosition} from '../Popover/position'
|
|
20
20
|
import {usePopoverLayer} from '../Popover/PopoverLayerContext'
|
|
21
|
+
import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
|
|
22
|
+
import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
|
|
21
23
|
|
|
22
24
|
// --- Root ---
|
|
23
25
|
interface RootProps {
|
|
@@ -61,14 +63,17 @@ interface TriggerProps {
|
|
|
61
63
|
|
|
62
64
|
function Trigger({children, 'aria-label': ariaLabel, variant = 'default', ref}: TriggerProps) {
|
|
63
65
|
const {open, toggle, menuId, triggerId, triggerRef} = useMenu()
|
|
66
|
+
const grouped = useGroupedControl<HTMLButtonElement>()
|
|
67
|
+
const groupedRef = grouped.ref
|
|
64
68
|
|
|
65
69
|
const setRef = useCallback(
|
|
66
70
|
(el: HTMLButtonElement | null) => {
|
|
67
71
|
triggerRef.current = el
|
|
72
|
+
groupedRef.current = el
|
|
68
73
|
if (typeof ref === 'function') ref(el)
|
|
69
74
|
else if (ref) ref.current = el
|
|
70
75
|
},
|
|
71
|
-
[ref, triggerRef],
|
|
76
|
+
[ref, triggerRef, groupedRef],
|
|
72
77
|
)
|
|
73
78
|
|
|
74
79
|
// TypeScript can't stop a <Button> child; the DOM can tell.
|
|
@@ -85,6 +90,8 @@ function Trigger({children, 'aria-label': ariaLabel, variant = 'default', ref}:
|
|
|
85
90
|
return (
|
|
86
91
|
<html.button
|
|
87
92
|
ref={setRef}
|
|
93
|
+
tabIndex={grouped.tabIndex}
|
|
94
|
+
onFocus={grouped.onFocus}
|
|
88
95
|
id={triggerId}
|
|
89
96
|
type="button"
|
|
90
97
|
onClick={toggle}
|
|
@@ -92,7 +99,7 @@ function Trigger({children, 'aria-label': ariaLabel, variant = 'default', ref}:
|
|
|
92
99
|
aria-expanded={open}
|
|
93
100
|
aria-haspopup="menu"
|
|
94
101
|
aria-controls={open ? menuId : undefined}
|
|
95
|
-
style={[styles.trigger, variant === 'ghost' && styles.triggerGhost]}
|
|
102
|
+
style={[styles.trigger, variant === 'ghost' && styles.triggerGhost, grouped.style]}
|
|
96
103
|
>
|
|
97
104
|
{children}
|
|
98
105
|
</html.button>
|
|
@@ -154,7 +161,7 @@ function Popup({children, align = 'start'}: PopupProps) {
|
|
|
154
161
|
aria-activedescendant={highlightedId ?? undefined}
|
|
155
162
|
style={[styles.popup, styles.popupPosition(coords.top, coords.left)]}
|
|
156
163
|
>
|
|
157
|
-
{children}
|
|
164
|
+
<ControlContextBoundary>{children}</ControlContextBoundary>
|
|
158
165
|
</html.div>,
|
|
159
166
|
mount ?? document.body,
|
|
160
167
|
)
|
|
@@ -6,6 +6,7 @@ import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
|
6
6
|
import {duration} from '@duro-app/tokens/tokens/motion.css'
|
|
7
7
|
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
8
8
|
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
9
|
+
import {layers} from '@duro-app/tokens/tokens/layers.css'
|
|
9
10
|
|
|
10
11
|
export const styles = css.create({
|
|
11
12
|
root: {
|
|
@@ -63,7 +64,7 @@ export const styles = css.create({
|
|
|
63
64
|
overflowY: 'auto',
|
|
64
65
|
// Above Dialog/Drawer (1000/1001), like Select and Combobox: they portal
|
|
65
66
|
// into the same mount, so a menu opened inside a dialog must out-rank it.
|
|
66
|
-
zIndex:
|
|
67
|
+
zIndex: layers.popup,
|
|
67
68
|
// The portal layer is pointer-events: none so clicks fall through it;
|
|
68
69
|
// the popup takes them back.
|
|
69
70
|
pointerEvents: 'auto',
|
|
@@ -19,6 +19,10 @@ import {styles} from './styles.css'
|
|
|
19
19
|
import {computePopoverPosition, type PopoverAlign, type PopoverSide} from './position'
|
|
20
20
|
import {isOutsidePress} from './outside'
|
|
21
21
|
import {PopoverLayerContext, type PopoverLayerContextValue} from './PopoverLayerContext'
|
|
22
|
+
import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
|
|
23
|
+
import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
|
|
24
|
+
import {mergeRefs} from '../../shared/mergeRefs'
|
|
25
|
+
import {useInModal} from '../../shared/ModalContext'
|
|
22
26
|
|
|
23
27
|
// --- Context ---
|
|
24
28
|
|
|
@@ -193,18 +197,30 @@ function Root({
|
|
|
193
197
|
// --- Trigger ---
|
|
194
198
|
// Trigger IS the button (like Menu.Trigger): give it a label, not a Button.
|
|
195
199
|
|
|
196
|
-
|
|
200
|
+
interface TriggerProps {
|
|
201
|
+
children: ReactNode
|
|
202
|
+
/** Accessible name: required when the label is an icon only. */
|
|
203
|
+
'aria-label'?: string
|
|
204
|
+
/** The trigger <button>, e.g. to measure it or return focus to it. */
|
|
205
|
+
ref?: Ref<HTMLButtonElement>
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function Trigger({children, 'aria-label': ariaLabel, ref}: TriggerProps) {
|
|
197
209
|
const {open, setOpen, popoverId, triggerRef} = usePopover('Trigger')
|
|
210
|
+
const grouped = useGroupedControl<HTMLButtonElement>()
|
|
198
211
|
|
|
199
212
|
return (
|
|
200
213
|
<html.button
|
|
201
|
-
ref={triggerRef}
|
|
214
|
+
ref={mergeRefs(triggerRef, grouped.ref, ref)}
|
|
215
|
+
tabIndex={grouped.tabIndex}
|
|
216
|
+
onFocus={grouped.onFocus}
|
|
202
217
|
type="button"
|
|
203
218
|
onClick={() => setOpen(!open)}
|
|
219
|
+
aria-label={ariaLabel}
|
|
204
220
|
aria-haspopup="dialog"
|
|
205
221
|
aria-expanded={open}
|
|
206
222
|
aria-controls={open ? popoverId : undefined}
|
|
207
|
-
style={styles.trigger}
|
|
223
|
+
style={[styles.trigger, grouped.style]}
|
|
208
224
|
>
|
|
209
225
|
{children}
|
|
210
226
|
</html.button>
|
|
@@ -226,6 +242,7 @@ function Popup({children, label}: PopupProps) {
|
|
|
226
242
|
// Inside ThemeProvider's portal layer, like Select/Dialog/Drawer: theme
|
|
227
243
|
// tokens cascade into the popup and it stacks above dialogs.
|
|
228
244
|
const mount = usePortalMount()
|
|
245
|
+
const inModal = useInModal()
|
|
229
246
|
|
|
230
247
|
// Measure before paint on open; re-measure on scroll/resize (capture phase
|
|
231
248
|
// catches any scrolling ancestor) and when Root.reposition() is called.
|
|
@@ -271,9 +288,13 @@ function Popup({children, label}: PopupProps) {
|
|
|
271
288
|
role="dialog"
|
|
272
289
|
aria-label={label}
|
|
273
290
|
tabIndex={-1}
|
|
274
|
-
style={[
|
|
291
|
+
style={[
|
|
292
|
+
styles.popup,
|
|
293
|
+
inModal && styles.popupInModal,
|
|
294
|
+
styles.popupPosition(coords.top, coords.left),
|
|
295
|
+
]}
|
|
275
296
|
>
|
|
276
|
-
{children}
|
|
297
|
+
<ControlContextBoundary>{children}</ControlContextBoundary>
|
|
277
298
|
</html.div>,
|
|
278
299
|
mount ?? document.body,
|
|
279
300
|
)
|
|
@@ -5,6 +5,7 @@ import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
|
5
5
|
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
6
|
import {duration} from '@duro-app/tokens/tokens/motion.css'
|
|
7
7
|
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
8
|
+
import {layers} from '@duro-app/tokens/tokens/layers.css'
|
|
8
9
|
|
|
9
10
|
export const styles = css.create({
|
|
10
11
|
trigger: {
|
|
@@ -30,6 +31,11 @@ export const styles = css.create({
|
|
|
30
31
|
transitionProperty: 'background-color, border-color',
|
|
31
32
|
transitionDuration: duration.fast,
|
|
32
33
|
},
|
|
34
|
+
// Inside a Dialog or Drawer: above that modal (same portal mount), below a
|
|
35
|
+
// Select or Menu opened from inside the Popover.
|
|
36
|
+
popupInModal: {
|
|
37
|
+
zIndex: layers.popover,
|
|
38
|
+
},
|
|
33
39
|
popup: {
|
|
34
40
|
position: 'fixed',
|
|
35
41
|
top: 0,
|
|
@@ -45,7 +51,9 @@ export const styles = css.create({
|
|
|
45
51
|
fontFamily: typography.fontFamily,
|
|
46
52
|
fontSize: typography.fontSizeSm,
|
|
47
53
|
color: colors.text,
|
|
48
|
-
|
|
54
|
+
// Outside a modal: floating chrome, so a Dialog or Drawer opened later
|
|
55
|
+
// (even from inside this Popover) covers it.
|
|
56
|
+
zIndex: layers.floating,
|
|
49
57
|
// The portal layer is pointer-events: none so clicks fall through it;
|
|
50
58
|
// the popup takes them back.
|
|
51
59
|
pointerEvents: 'auto',
|
|
@@ -3,6 +3,7 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
|
3
3
|
import {microSpacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
4
|
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
5
5
|
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
6
|
+
import {layers} from '@duro-app/tokens/tokens/layers.css'
|
|
6
7
|
|
|
7
8
|
export const styles = css.create({
|
|
8
9
|
root: {
|
|
@@ -23,7 +24,7 @@ export const styles = css.create({
|
|
|
23
24
|
},
|
|
24
25
|
scrollbar: {
|
|
25
26
|
position: 'absolute',
|
|
26
|
-
zIndex:
|
|
27
|
+
zIndex: layers.raised,
|
|
27
28
|
display: 'flex',
|
|
28
29
|
touchAction: 'none',
|
|
29
30
|
userSelect: 'none',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
-
import {expect,
|
|
2
|
+
import {expect, screen} from 'storybook/test'
|
|
3
3
|
import {Dialog} from '../Dialog/Dialog'
|
|
4
4
|
import {Select} from './Select'
|
|
5
5
|
|
|
@@ -194,3 +194,22 @@ export const InsideDialog: Story = {
|
|
|
194
194
|
await expect(listbox.closest('[role="dialog"]')).toBeNull()
|
|
195
195
|
},
|
|
196
196
|
}
|
|
197
|
+
|
|
198
|
+
/** Closed, the trigger opens on ArrowDown or ArrowUp, on the selected option (5.3). */
|
|
199
|
+
export const ArrowKeysOpen: Story = {
|
|
200
|
+
render: Default.render,
|
|
201
|
+
play: async ({canvas, userEvent}) => {
|
|
202
|
+
const trigger = canvas.getByRole('combobox')
|
|
203
|
+
trigger.focus()
|
|
204
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
205
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
|
206
|
+
await expect(trigger).toHaveFocus()
|
|
207
|
+
const selected = screen.getByRole('option', {name: 'English'})
|
|
208
|
+
await expect(trigger).toHaveAttribute('aria-activedescendant', selected.id)
|
|
209
|
+
await userEvent.keyboard('{Escape}')
|
|
210
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
|
211
|
+
await userEvent.keyboard('{ArrowUp}')
|
|
212
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
|
213
|
+
await userEvent.keyboard('{Escape}')
|
|
214
|
+
},
|
|
215
|
+
}
|
|
@@ -7,6 +7,9 @@ import {useSelectRoot} from './useSelectRoot'
|
|
|
7
7
|
import {useFieldContext} from '../Field/FieldContext'
|
|
8
8
|
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
9
9
|
import {usePopoverLayer} from '../Popover/PopoverLayerContext'
|
|
10
|
+
import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
|
|
11
|
+
import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
|
|
12
|
+
import {mergeRefs} from '../../shared/mergeRefs'
|
|
10
13
|
|
|
11
14
|
// --- Root ---
|
|
12
15
|
interface RootProps {
|
|
@@ -41,6 +44,7 @@ function Trigger({
|
|
|
41
44
|
}) {
|
|
42
45
|
const {open, toggle, listboxId, highlightedId, triggerRef} = useSelect()
|
|
43
46
|
const localRef = useRef<HTMLButtonElement>(null)
|
|
47
|
+
const grouped = useGroupedControl<HTMLButtonElement>()
|
|
44
48
|
// Inside a <Field.Root>, adopt its controlId so <Field.Label for={controlId}>
|
|
45
49
|
// labels the trigger (a labelable <button>) — no manual aria-label needed; and
|
|
46
50
|
// surface the field's description/error + invalid state, mirroring <Input>.
|
|
@@ -56,7 +60,9 @@ function Trigger({
|
|
|
56
60
|
|
|
57
61
|
return (
|
|
58
62
|
<html.button
|
|
59
|
-
ref={localRef}
|
|
63
|
+
ref={mergeRefs(localRef, grouped.ref)}
|
|
64
|
+
tabIndex={grouped.tabIndex}
|
|
65
|
+
onFocus={grouped.onFocus}
|
|
60
66
|
id={fieldCtx?.controlId}
|
|
61
67
|
type="button"
|
|
62
68
|
role={'combobox' as 'listbox'}
|
|
@@ -68,7 +74,7 @@ function Trigger({
|
|
|
68
74
|
aria-activedescendant={highlightedId ?? undefined}
|
|
69
75
|
aria-describedby={describedBy || undefined}
|
|
70
76
|
aria-invalid={fieldCtx?.invalid || undefined}
|
|
71
|
-
style={styles.trigger}
|
|
77
|
+
style={[styles.trigger, grouped.style]}
|
|
72
78
|
>
|
|
73
79
|
{children}
|
|
74
80
|
</html.button>
|
|
@@ -161,7 +167,7 @@ function Popup({children}: {children: ReactNode}) {
|
|
|
161
167
|
coords && styles.popupPosition(coords.top, coords.left, coords.width),
|
|
162
168
|
]}
|
|
163
169
|
>
|
|
164
|
-
{children}
|
|
170
|
+
<ControlContextBoundary>{children}</ControlContextBoundary>
|
|
165
171
|
</html.div>
|
|
166
172
|
</>
|
|
167
173
|
)
|
|
@@ -6,6 +6,7 @@ import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
|
6
6
|
import {duration} from '@duro-app/tokens/tokens/motion.css'
|
|
7
7
|
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
8
8
|
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
9
|
+
import {layers} from '@duro-app/tokens/tokens/layers.css'
|
|
9
10
|
|
|
10
11
|
export const styles = css.create({
|
|
11
12
|
root: {
|
|
@@ -55,7 +56,7 @@ export const styles = css.create({
|
|
|
55
56
|
bottom: 0,
|
|
56
57
|
// Above Dialog/Drawer (1000/1001): they portal into the same mount, so a
|
|
57
58
|
// popup opened from inside a dialog must out-rank the dialog itself.
|
|
58
|
-
zIndex:
|
|
59
|
+
zIndex: layers.popupBackdrop,
|
|
59
60
|
// The portal mount is pointer-events: none, so re-enable here to catch
|
|
60
61
|
// outside clicks that close the popup.
|
|
61
62
|
pointerEvents: 'auto',
|
|
@@ -76,7 +77,7 @@ export const styles = css.create({
|
|
|
76
77
|
minWidth: sizes.labelMinW,
|
|
77
78
|
maxHeight: sizes.listMaxH,
|
|
78
79
|
overflowY: 'auto',
|
|
79
|
-
zIndex:
|
|
80
|
+
zIndex: layers.popup,
|
|
80
81
|
// Re-enable pointer events: the portal mount is pointer-events: none.
|
|
81
82
|
pointerEvents: 'auto',
|
|
82
83
|
},
|
|
@@ -80,6 +80,23 @@ export function useSelectRoot({
|
|
|
80
80
|
}
|
|
81
81
|
}, [])
|
|
82
82
|
|
|
83
|
+
// Closed: ArrowDown/ArrowUp on the trigger open the listbox (the APG
|
|
84
|
+
// select-only combobox), on the selected option or the first. In a Toolbar
|
|
85
|
+
// this is why a closed Select keeps those keys; Left/Right still move on.
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const root = rootRef.current
|
|
88
|
+
if (!root || open) return
|
|
89
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
90
|
+
if (e.target !== triggerRef.current) return
|
|
91
|
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return
|
|
92
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return
|
|
93
|
+
e.preventDefault()
|
|
94
|
+
toggle()
|
|
95
|
+
}
|
|
96
|
+
root.addEventListener('keydown', handleKeyDown)
|
|
97
|
+
return () => root.removeEventListener('keydown', handleKeyDown)
|
|
98
|
+
}, [open, toggle])
|
|
99
|
+
|
|
83
100
|
// Native keydown for full KeyboardEvent access (preventDefault)
|
|
84
101
|
useEffect(() => {
|
|
85
102
|
const root = rootRef.current
|