@duro-app/ui 5.1.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-Cm5cCnZD.js +1524 -0
- 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 +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/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/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 +3 -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 +11 -3
- 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/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/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/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/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/ThemeProvider/ThemeProvider.stories.d.ts +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.stories.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/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 +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3768 -3356
- 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/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/styles/visually-hidden.css.d.ts +6 -0
- package/dist/styles/visually-hidden.css.d.ts.map +1 -1
- 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.stories.tsx +41 -0
- package/src/components/Button/Button.tsx +22 -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/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 +68 -10
- package/src/components/Dialog/styles.css.ts +5 -2
- package/src/components/DragDrop/DragDrop.tsx +15 -8
- package/src/components/DragDrop/styles.css.ts +20 -4
- package/src/components/Drawer/Drawer.tsx +5 -1
- package/src/components/Drawer/styles.css.ts +3 -2
- 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 +90 -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 +107 -29
- package/src/components/Menu/MenuContext.ts +3 -0
- package/src/components/Menu/styles.css.ts +32 -15
- package/src/components/Menu/useMenuRoot.ts +105 -48
- 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/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/Layers.stories.tsx +379 -0
- package/src/components/ThemeProvider/ThemeProvider.stories.tsx +50 -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/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 +7 -1
- package/src/shared/ModalContext.ts +18 -0
- package/src/shared/mergeRefs.ts +11 -0
- package/src/shared/useOrderedRegistry.ts +57 -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
|
@@ -1,48 +1,93 @@
|
|
|
1
|
-
import {useState, useCallback, useRef, useId, useEffect} from 'react'
|
|
1
|
+
import {useState, useCallback, useRef, useId, useEffect, useLayoutEffect} from 'react'
|
|
2
2
|
import type {MenuContextValue} from './MenuContext'
|
|
3
|
+
import {isOutsidePress} from '../Popover/outside'
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
|
|
5
|
+
export interface MenuRootOptions {
|
|
6
|
+
open?: boolean
|
|
7
|
+
defaultOpen?: boolean
|
|
8
|
+
onOpenChange?: (open: boolean) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The menu-button pattern (WAI-ARIA APG): on open, focus moves to the
|
|
13
|
+
* `role="menu"` popup, which carries `aria-activedescendant`; arrows move the
|
|
14
|
+
* highlight; Escape closes and focus returns to the trigger. The keydown
|
|
15
|
+
* handler sits on the popup itself, never on `document`, so keys typed
|
|
16
|
+
* elsewhere (an editor) are never intercepted, and Escape stops there so an
|
|
17
|
+
* enclosing Popover or Dialog stays open.
|
|
18
|
+
*/
|
|
19
|
+
export function useMenuRoot({open: openProp, defaultOpen = false, onOpenChange}: MenuRootOptions) {
|
|
20
|
+
const [inner, setInner] = useState(defaultOpen)
|
|
21
|
+
const controlled = openProp !== undefined
|
|
22
|
+
const open = controlled ? openProp : inner
|
|
6
23
|
const [highlightedId, setHighlightedId] = useState<string | null>(null)
|
|
24
|
+
// Read by the keydown handler, so activating an item is not a side effect
|
|
25
|
+
// inside a state updater (React may run updaters twice).
|
|
26
|
+
const highlightedRef = useRef(highlightedId)
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
highlightedRef.current = highlightedId
|
|
29
|
+
}, [highlightedId])
|
|
7
30
|
const menuId = useId()
|
|
31
|
+
const triggerId = useId()
|
|
8
32
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
9
33
|
const triggerRef = useRef<HTMLButtonElement | null>(null)
|
|
34
|
+
const popupRef = useRef<HTMLDivElement | null>(null)
|
|
10
35
|
const itemsRef = useRef(new Map<string, HTMLElement>())
|
|
11
36
|
const orderRef = useRef<string[]>([])
|
|
12
|
-
const
|
|
37
|
+
const openRef = useRef(open)
|
|
38
|
+
openRef.current = open
|
|
39
|
+
const onOpenChangeRef = useRef(onOpenChange)
|
|
40
|
+
onOpenChangeRef.current = onOpenChange
|
|
13
41
|
|
|
14
|
-
const
|
|
42
|
+
const setOpen = useCallback(
|
|
43
|
+
(next: boolean) => {
|
|
44
|
+
if (next === openRef.current) return
|
|
45
|
+
if (!controlled) setInner(next)
|
|
46
|
+
onOpenChangeRef.current?.(next)
|
|
47
|
+
},
|
|
48
|
+
[controlled],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Closes and leaves focus where it is (a press elsewhere put it there).
|
|
52
|
+
const dismiss = useCallback(() => {
|
|
53
|
+
if (!openRef.current) return
|
|
15
54
|
setOpen(false)
|
|
16
55
|
setHighlightedId(null)
|
|
17
|
-
|
|
56
|
+
}, [setOpen])
|
|
57
|
+
|
|
58
|
+
// Closes and puts focus back on the trigger.
|
|
59
|
+
const close = useCallback(() => {
|
|
60
|
+
if (!openRef.current) return
|
|
61
|
+
dismiss()
|
|
18
62
|
triggerRef.current?.focus()
|
|
19
|
-
}, [])
|
|
63
|
+
}, [dismiss])
|
|
20
64
|
|
|
21
65
|
const toggle = useCallback(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} else {
|
|
26
|
-
setHighlightedId(null)
|
|
27
|
-
needsInitialHighlightRef.current = false
|
|
28
|
-
}
|
|
29
|
-
return !prev
|
|
30
|
-
})
|
|
31
|
-
}, [])
|
|
66
|
+
if (openRef.current) close()
|
|
67
|
+
else setOpen(true)
|
|
68
|
+
}, [close, setOpen])
|
|
32
69
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
70
|
+
// On open: focus the menu and highlight the first item. Child effects (item
|
|
71
|
+
// registration) run before this parent effect, so orderRef is populated.
|
|
72
|
+
const wasOpenRef = useRef(false)
|
|
36
73
|
useEffect(() => {
|
|
37
|
-
if (open &&
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
74
|
+
if (open && !wasOpenRef.current) {
|
|
75
|
+
popupRef.current?.focus({preventScroll: true})
|
|
76
|
+
const first = orderRef.current[0]
|
|
77
|
+
if (first) setHighlightedId(first)
|
|
78
|
+
} else if (!open && wasOpenRef.current) {
|
|
79
|
+
setHighlightedId(null)
|
|
43
80
|
}
|
|
81
|
+
wasOpenRef.current = open
|
|
44
82
|
}, [open])
|
|
45
83
|
|
|
84
|
+
// Keep the highlighted item in view when the popup scrolls.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!highlightedId) return
|
|
87
|
+
const el = itemsRef.current.get(highlightedId)
|
|
88
|
+
el?.scrollIntoView?.({block: 'nearest'})
|
|
89
|
+
}, [highlightedId])
|
|
90
|
+
|
|
46
91
|
const registerItem = useCallback((id: string, element: HTMLElement) => {
|
|
47
92
|
itemsRef.current.set(id, element)
|
|
48
93
|
const map = itemsRef.current
|
|
@@ -60,12 +105,24 @@ export function useMenuRoot() {
|
|
|
60
105
|
}
|
|
61
106
|
}, [])
|
|
62
107
|
|
|
63
|
-
// Native keydown for full KeyboardEvent access
|
|
108
|
+
// Native keydown on the popup element, for full KeyboardEvent access.
|
|
64
109
|
useEffect(() => {
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
110
|
+
const popup = popupRef.current
|
|
111
|
+
if (!popup || !open) return
|
|
67
112
|
|
|
68
113
|
function handleKeyDown(e: KeyboardEvent) {
|
|
114
|
+
if (e.key === 'Escape') {
|
|
115
|
+
e.preventDefault()
|
|
116
|
+
e.stopPropagation()
|
|
117
|
+
close()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (e.key === 'Tab') {
|
|
121
|
+
// Back on the trigger before the browser moves focus, so Tab and
|
|
122
|
+
// Shift+Tab continue from where the menu was opened.
|
|
123
|
+
close()
|
|
124
|
+
return
|
|
125
|
+
}
|
|
69
126
|
const order = orderRef.current
|
|
70
127
|
if (order.length === 0) return
|
|
71
128
|
|
|
@@ -99,41 +156,41 @@ export function useMenuRoot() {
|
|
|
99
156
|
case 'Enter':
|
|
100
157
|
case ' ': {
|
|
101
158
|
e.preventDefault()
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
if (prev) {
|
|
105
|
-
const el = items.get(prev)
|
|
106
|
-
el?.click()
|
|
107
|
-
}
|
|
108
|
-
return prev
|
|
109
|
-
})
|
|
110
|
-
break
|
|
111
|
-
}
|
|
112
|
-
case 'Escape': {
|
|
113
|
-
e.preventDefault()
|
|
114
|
-
close()
|
|
115
|
-
break
|
|
116
|
-
}
|
|
117
|
-
case 'Tab': {
|
|
118
|
-
close()
|
|
159
|
+
const current = highlightedRef.current
|
|
160
|
+
if (current) itemsRef.current.get(current)?.click()
|
|
119
161
|
break
|
|
120
162
|
}
|
|
121
163
|
}
|
|
122
164
|
}
|
|
123
165
|
|
|
124
|
-
|
|
125
|
-
return () =>
|
|
166
|
+
popup.addEventListener('keydown', handleKeyDown)
|
|
167
|
+
return () => popup.removeEventListener('keydown', handleKeyDown)
|
|
126
168
|
}, [open, close])
|
|
127
169
|
|
|
170
|
+
// A press outside the trigger and the popup closes the menu, and leaves
|
|
171
|
+
// focus where the press put it.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!open) return
|
|
174
|
+
function onPointerDown(e: PointerEvent) {
|
|
175
|
+
if (isOutsidePress(e.target as Node | null, [popupRef.current, triggerRef.current])) {
|
|
176
|
+
dismiss()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
180
|
+
return () => document.removeEventListener('pointerdown', onPointerDown, true)
|
|
181
|
+
}, [open, dismiss])
|
|
182
|
+
|
|
128
183
|
const ctx: MenuContextValue = {
|
|
129
184
|
open,
|
|
130
185
|
toggle,
|
|
131
186
|
close,
|
|
132
187
|
menuId,
|
|
188
|
+
triggerId,
|
|
133
189
|
highlightedId,
|
|
134
190
|
setHighlightedId,
|
|
135
191
|
registerItem,
|
|
136
192
|
triggerRef,
|
|
193
|
+
popupRef,
|
|
137
194
|
}
|
|
138
195
|
|
|
139
196
|
return {ctx, rootRef}
|
|
@@ -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
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Inline hyperlink for running text and standalone text links ("View all", "Edit profile"). Always underlined, so colour is never the only cue: default is accent text with an accent underline; subtle keeps the surrounding text colour with a muted underline and turns accent on hover. target="_blank" defaults rel to noopener noreferrer.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'A link inside a sentence or paragraph',
|
|
8
|
+
'A standalone text link under a list or card ("View all requests")',
|
|
9
|
+
'A quiet link in dense UI (variant="subtle")',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'A link that should look like a button — use LinkButton',
|
|
13
|
+
'An action that does not navigate — use Button (variant="link" for a text-styled action)',
|
|
14
|
+
'A link inside a dropdown — use Menu.LinkItem',
|
|
15
|
+
],
|
|
16
|
+
relatedTo: [
|
|
17
|
+
{
|
|
18
|
+
component: 'LinkButton',
|
|
19
|
+
kind: 'contrast',
|
|
20
|
+
relationship:
|
|
21
|
+
'LinkButton is a hyperlink styled as a button; TextLink is a hyperlink styled as text',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
component: 'Button',
|
|
25
|
+
kind: 'contrast',
|
|
26
|
+
relationship: 'Button runs an action; TextLink navigates',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
example: `<Text>
|
|
30
|
+
Read the <TextLink href="/docs/migrate">migration guide</TextLink> before you change seats, or{' '}
|
|
31
|
+
<TextLink href="/plans" variant="subtle">compare plans</TextLink> first.
|
|
32
|
+
</Text>`,
|
|
33
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {expect} from 'storybook/test'
|
|
3
|
+
import {TextLink} from './TextLink'
|
|
4
|
+
import {Text} from '../Text/Text'
|
|
5
|
+
import {Inline} from '../Inline/Inline'
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof TextLink> = {
|
|
8
|
+
title: 'Components/TextLink',
|
|
9
|
+
component: TextLink,
|
|
10
|
+
args: {href: '#guide', children: 'migration guide'},
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default meta
|
|
14
|
+
type Story = StoryObj<typeof TextLink>
|
|
15
|
+
|
|
16
|
+
export const InRunningText: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Text>
|
|
19
|
+
Your workspace moved to the new plan. Read the{' '}
|
|
20
|
+
<TextLink href="#guide">migration guide</TextLink> before you change seats, or{' '}
|
|
21
|
+
<TextLink href="#plans" variant="subtle">
|
|
22
|
+
compare plans
|
|
23
|
+
</TextLink>{' '}
|
|
24
|
+
first.
|
|
25
|
+
</Text>
|
|
26
|
+
),
|
|
27
|
+
play: async ({canvas}) => {
|
|
28
|
+
for (const name of ['migration guide', 'compare plans']) {
|
|
29
|
+
const link = canvas.getByRole('link', {name})
|
|
30
|
+
// Underlined in both variants: colour is never the only cue.
|
|
31
|
+
await expect(getComputedStyle(link).textDecorationLine).toContain('underline')
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Standalone: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<Inline gap="lg">
|
|
39
|
+
<TextLink href="#all">View all requests</TextLink>
|
|
40
|
+
<TextLink href="#profile" variant="subtle">
|
|
41
|
+
Edit profile
|
|
42
|
+
</TextLink>
|
|
43
|
+
<TextLink href="https://example.com/docs" target="_blank">
|
|
44
|
+
Documentation
|
|
45
|
+
</TextLink>
|
|
46
|
+
</Inline>
|
|
47
|
+
),
|
|
48
|
+
play: async ({canvas}) => {
|
|
49
|
+
const external = canvas.getByRole('link', {name: 'Documentation'})
|
|
50
|
+
await expect(external).toHaveAttribute('target', '_blank')
|
|
51
|
+
await expect(external).toHaveAttribute('rel', 'noopener noreferrer')
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const KeyboardFocus: Story = {
|
|
56
|
+
render: () => <TextLink href="#billing">Billing settings</TextLink>,
|
|
57
|
+
play: async ({canvas, userEvent}) => {
|
|
58
|
+
await userEvent.tab()
|
|
59
|
+
const link = canvas.getByRole('link', {name: 'Billing settings'})
|
|
60
|
+
await expect(link).toHaveFocus()
|
|
61
|
+
await expect(getComputedStyle(link).outlineStyle).toBe('solid')
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {styles} from './styles.css'
|
|
4
|
+
|
|
5
|
+
export type TextLinkVariant = 'default' | 'subtle'
|
|
6
|
+
|
|
7
|
+
interface TextLinkProps {
|
|
8
|
+
href: string
|
|
9
|
+
/** `_blank` opens a new tab; `rel` then defaults to `noopener noreferrer`. */
|
|
10
|
+
target?: '_blank' | '_self'
|
|
11
|
+
rel?: string
|
|
12
|
+
/**
|
|
13
|
+
* `default`: accent text with an accent underline. `subtle`: the
|
|
14
|
+
* surrounding text colour with a muted underline, accent on hover. Both
|
|
15
|
+
* stay underlined, so colour is never the only cue in running text.
|
|
16
|
+
*/
|
|
17
|
+
variant?: TextLinkVariant
|
|
18
|
+
/** Accessible name, when the visible text alone does not say where it goes. */
|
|
19
|
+
'aria-label'?: string
|
|
20
|
+
children: ReactNode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An inline hyperlink in running text or a standalone "View all" link. For a
|
|
25
|
+
* link that looks like a button, use LinkButton.
|
|
26
|
+
*/
|
|
27
|
+
export function TextLink({
|
|
28
|
+
href,
|
|
29
|
+
target,
|
|
30
|
+
rel,
|
|
31
|
+
variant = 'default',
|
|
32
|
+
'aria-label': ariaLabel,
|
|
33
|
+
children,
|
|
34
|
+
}: TextLinkProps) {
|
|
35
|
+
return (
|
|
36
|
+
<html.a
|
|
37
|
+
href={href}
|
|
38
|
+
target={target}
|
|
39
|
+
rel={rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined)}
|
|
40
|
+
aria-label={ariaLabel}
|
|
41
|
+
style={[styles.base, styles[variant]]}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</html.a>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
5
|
+
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
6
|
+
|
|
7
|
+
// Direction "Underlined" (docs/mockups/text-link/Underlined.dc.html): the
|
|
8
|
+
// underline is always there, so colour is never the only cue (WCAG F73).
|
|
9
|
+
export const styles = css.create({
|
|
10
|
+
base: {
|
|
11
|
+
fontFamily: 'inherit',
|
|
12
|
+
fontSize: 'inherit',
|
|
13
|
+
lineHeight: 'inherit',
|
|
14
|
+
cursor: 'pointer',
|
|
15
|
+
textDecorationLine: 'underline',
|
|
16
|
+
textDecorationStyle: 'solid',
|
|
17
|
+
borderRadius: radii.xs,
|
|
18
|
+
transitionProperty: 'color, text-decoration-color',
|
|
19
|
+
transitionDuration: duration.fast,
|
|
20
|
+
transitionTimingFunction: easing.standard,
|
|
21
|
+
outlineWidth: {
|
|
22
|
+
default: 0,
|
|
23
|
+
':focus-visible': borders.focusRing,
|
|
24
|
+
},
|
|
25
|
+
outlineStyle: {
|
|
26
|
+
default: 'none',
|
|
27
|
+
':focus-visible': 'solid',
|
|
28
|
+
},
|
|
29
|
+
outlineColor: {
|
|
30
|
+
default: 'transparent',
|
|
31
|
+
':focus-visible': colors.accent,
|
|
32
|
+
},
|
|
33
|
+
outlineOffset: {
|
|
34
|
+
default: 0,
|
|
35
|
+
':focus-visible': borders.focusOffset,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
default: {
|
|
39
|
+
color: {
|
|
40
|
+
default: colors.accent,
|
|
41
|
+
':hover': colors.accentHover,
|
|
42
|
+
},
|
|
43
|
+
textDecorationColor: {
|
|
44
|
+
default: colors.accent,
|
|
45
|
+
':hover': colors.accentHover,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
subtle: {
|
|
49
|
+
color: {
|
|
50
|
+
default: 'inherit',
|
|
51
|
+
':hover': colors.accent,
|
|
52
|
+
},
|
|
53
|
+
textDecorationColor: {
|
|
54
|
+
default: colors.textMuted,
|
|
55
|
+
':hover': colors.accent,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
})
|