@duro-app/ui 5.11.0 → 5.12.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-ColfpgKV.js.map +1 -1
- package/dist/compiler-runtime-BuugS4D0.js.map +1 -1
- package/dist/components/Alert/Alert.d.ts.map +1 -1
- package/dist/components/Button/Button.d.ts +2 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/styles.css.d.ts +1 -0
- package/dist/components/Button/styles.css.d.ts.map +1 -1
- package/dist/components/Callout/Callout.d.ts.map +1 -1
- package/dist/components/Callout/styles.css.d.ts +3 -0
- package/dist/components/Callout/styles.css.d.ts.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.d.ts +2 -17
- 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/native.css.d.ts +82 -0
- package/dist/components/Dialog/native.css.d.ts.map +1 -0
- package/dist/components/Dialog/parts.css.d.ts +82 -0
- package/dist/components/Dialog/parts.css.d.ts.map +1 -0
- package/dist/components/Dialog/parts.d.ts +35 -0
- package/dist/components/Dialog/parts.d.ts.map +1 -0
- package/dist/components/Dialog/styles.css.d.ts +1 -74
- package/dist/components/Dialog/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 -0
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Grid/styles.css.d.ts +1 -0
- package/dist/components/Grid/styles.css.d.ts.map +1 -1
- package/dist/components/Icon/Icon.d.ts +3 -9
- package/dist/components/Icon/Icon.d.ts.map +1 -1
- package/dist/components/Icon/geometry.d.ts +60 -0
- package/dist/components/Icon/geometry.d.ts.map +1 -0
- package/dist/components/Icon/types.d.ts +15 -0
- package/dist/components/Icon/types.d.ts.map +1 -0
- package/dist/components/Input/Input.d.ts +5 -1
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/Input/styles.css.d.ts +1 -0
- package/dist/components/Input/styles.css.d.ts.map +1 -1
- package/dist/components/LinkButton/styles.css.d.ts +1 -0
- package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
- package/dist/components/Meter/styles.css.d.ts +1 -0
- package/dist/components/Meter/styles.css.d.ts.map +1 -1
- package/dist/components/PageShell/styles.css.d.ts +2 -0
- package/dist/components/PageShell/styles.css.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +1 -17
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts +3 -0
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -1
- package/dist/components/RadioGroup/group.d.ts +31 -0
- package/dist/components/RadioGroup/group.d.ts.map +1 -0
- package/dist/components/RadioGroup/styles.css.d.ts +10 -0
- package/dist/components/RadioGroup/styles.css.d.ts.map +1 -1
- package/dist/components/ScrollArea/styles.css.d.ts +1 -0
- package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
- package/dist/components/Spinner/Spinner.d.ts +2 -6
- package/dist/components/Spinner/Spinner.d.ts.map +1 -1
- package/dist/components/Spinner/Spinner.types.d.ts +6 -0
- package/dist/components/Spinner/Spinner.types.d.ts.map +1 -0
- package/dist/components/Spinner/ring.css.d.ts +39 -0
- package/dist/components/Spinner/ring.css.d.ts.map +1 -0
- package/dist/components/Spinner/styles.css.d.ts +1 -28
- package/dist/components/Spinner/styles.css.d.ts.map +1 -1
- package/dist/components/StatusIcon/StatusIcon.d.ts.map +1 -1
- package/dist/components/StatusIcon/styles.css.d.ts +3 -0
- package/dist/components/StatusIcon/styles.css.d.ts.map +1 -1
- package/dist/components/Switch/Switch.d.ts +2 -18
- package/dist/components/Switch/Switch.d.ts.map +1 -1
- package/dist/components/Switch/Switch.types.d.ts +18 -0
- package/dist/components/Switch/Switch.types.d.ts.map +1 -0
- package/dist/components/TableOfContents/styles.css.d.ts +1 -0
- package/dist/components/TableOfContents/styles.css.d.ts.map +1 -1
- package/dist/components/Textarea/styles.css.d.ts +1 -0
- package/dist/components/Textarea/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 +11 -2
- package/dist/components/Toggle/styles.css.d.ts.map +1 -1
- package/dist/components/VirtualTable/styles.css.d.ts +6 -0
- package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +4299 -3566
- package/dist/index.js.map +1 -1
- package/dist/shared/portal/hostRegistry.d.ts +33 -0
- package/dist/shared/portal/hostRegistry.d.ts.map +1 -0
- package/dist/shared/useOverlayOpenState.d.ts +40 -0
- package/dist/shared/useOverlayOpenState.d.ts.map +1 -0
- package/dist/table.js +141 -135
- package/dist/table.js.map +1 -1
- package/package.json +34 -4
- package/src/components/Alert/Alert.tsx +14 -2
- package/src/components/Button/Button.tsx +3 -2
- package/src/components/Button/styles.css.ts +2 -0
- package/src/components/Callout/Callout.tsx +17 -3
- package/src/components/Callout/styles.css.ts +5 -0
- package/src/components/Checkbox/Checkbox.native.tsx +67 -0
- package/src/components/Checkbox/Checkbox.tsx +1 -50
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +3 -0
- package/src/components/DetailPanel/DetailPanel.tsx +9 -45
- package/src/components/Dialog/Dialog.native.tsx +296 -0
- package/src/components/Dialog/Dialog.stories.tsx +71 -0
- package/src/components/Dialog/Dialog.tsx +41 -130
- package/src/components/Dialog/native.css.ts +78 -0
- package/src/components/Dialog/parts.css.ts +101 -0
- package/src/components/Dialog/parts.tsx +61 -0
- package/src/components/Dialog/styles.css.ts +2 -89
- package/src/components/Drawer/Drawer.tsx +20 -49
- package/src/components/Drawer/styles.css.ts +4 -0
- package/src/components/Grid/styles.css.ts +2 -0
- package/src/components/Icon/Icon.native.tsx +54 -0
- package/src/components/Icon/Icon.tsx +19 -430
- package/src/components/Icon/geometry.ts +456 -0
- package/src/components/Icon/types.ts +74 -0
- package/src/components/Input/Input.tsx +8 -0
- package/src/components/Input/styles.css.ts +2 -0
- package/src/components/LinkButton/styles.css.ts +2 -0
- package/src/components/Meter/styles.css.ts +2 -0
- package/src/components/PageShell/styles.css.ts +4 -0
- package/src/components/RadioGroup/RadioGroup.meta.ts +1 -1
- package/src/components/RadioGroup/RadioGroup.native.tsx +48 -0
- package/src/components/RadioGroup/RadioGroup.stories.tsx +64 -1
- package/src/components/RadioGroup/RadioGroup.tsx +8 -83
- package/src/components/RadioGroup/group.tsx +85 -0
- package/src/components/RadioGroup/styles.css.ts +13 -0
- package/src/components/ScrollArea/styles.css.ts +2 -0
- package/src/components/Spinner/Spinner.native.tsx +37 -0
- package/src/components/Spinner/Spinner.tsx +6 -15
- package/src/components/Spinner/Spinner.types.ts +6 -0
- package/src/components/Spinner/ring.css.ts +45 -0
- package/src/components/Spinner/styles.css.ts +2 -32
- package/src/components/StatusIcon/StatusIcon.tsx +13 -2
- package/src/components/StatusIcon/styles.css.ts +5 -0
- package/src/components/Switch/Switch.native.tsx +45 -0
- package/src/components/Switch/Switch.tsx +22 -44
- package/src/components/Switch/Switch.types.ts +18 -0
- package/src/components/TableOfContents/styles.css.ts +2 -0
- package/src/components/Textarea/styles.css.ts +2 -0
- package/src/components/ThemeProvider/ThemeProvider.native.tsx +70 -10
- package/src/components/Toggle/Toggle.tsx +2 -0
- package/src/components/Toggle/styles.css.ts +14 -2
- package/src/components/VirtualTable/styles.css.ts +12 -0
- package/src/index.native.ts +28 -1
- package/src/native-guard/globals.d.ts +10 -0
- package/src/native-guard/react-dom.d.ts +3 -0
- package/src/shared/accessibilityFocus.native.ts +23 -0
- package/src/shared/portal/OverlayModal.native.tsx +51 -0
- package/src/shared/portal/PortalHost.native.tsx +102 -0
- package/src/shared/portal/hostRegistry.ts +76 -0
- package/src/shared/portal/themeScope.native.ts +18 -0
- package/src/shared/safeArea.native.ts +22 -0
- package/src/shared/useOverlayOpenState.ts +128 -0
- package/dist/components/ThemeProvider/ThemeProvider.native.d.ts +0 -10
- package/dist/components/ThemeProvider/ThemeProvider.native.d.ts.map +0 -1
- package/dist/index.native.d.ts +0 -33
- package/dist/index.native.d.ts.map +0 -1
- package/dist/platform.native.d.ts +0 -2
- package/dist/platform.native.d.ts.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {useState} from 'react'
|
|
2
2
|
import type {Meta, StoryObj} from '@storybook/react'
|
|
3
3
|
import {expect, fn} from 'storybook/test'
|
|
4
|
-
import {
|
|
4
|
+
import {html} from 'react-strict-dom'
|
|
5
5
|
import {RadioGroup} from './RadioGroup'
|
|
6
6
|
|
|
7
7
|
const meta: Meta = {
|
|
@@ -81,3 +81,66 @@ export const Controlled: Story = {
|
|
|
81
81
|
await expect(radios[0]).not.toBeChecked()
|
|
82
82
|
},
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
const onPick = fn()
|
|
86
|
+
|
|
87
|
+
export const OneChangePerPick: Story = {
|
|
88
|
+
render: () => (
|
|
89
|
+
<RadioGroup.Root defaultValue="apple" onValueChange={onPick} aria-label="Fruit">
|
|
90
|
+
<RadioGroup.Item value="apple">Apple</RadioGroup.Item>
|
|
91
|
+
<RadioGroup.Item value="banana">Banana</RadioGroup.Item>
|
|
92
|
+
<RadioGroup.Item value="cherry">Cherry</RadioGroup.Item>
|
|
93
|
+
</RadioGroup.Root>
|
|
94
|
+
),
|
|
95
|
+
play: async ({canvas, userEvent}) => {
|
|
96
|
+
onPick.mockClear()
|
|
97
|
+
// Clicking the visible label text…
|
|
98
|
+
await userEvent.click(canvas.getByText('Banana'))
|
|
99
|
+
await expect(onPick).toHaveBeenCalledTimes(1)
|
|
100
|
+
await expect(onPick).toHaveBeenLastCalledWith('banana')
|
|
101
|
+
// …and clicking the radio itself each report the pick exactly once.
|
|
102
|
+
await userEvent.click(canvas.getByRole('radio', {name: 'Cherry'}))
|
|
103
|
+
await expect(onPick).toHaveBeenCalledTimes(2)
|
|
104
|
+
await expect(onPick).toHaveBeenLastCalledWith('cherry')
|
|
105
|
+
// Re-picking the selected item is not a change.
|
|
106
|
+
await userEvent.click(canvas.getByText('Cherry'))
|
|
107
|
+
await expect(onPick).toHaveBeenCalledTimes(2)
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const SubmitsWithForm: Story = {
|
|
112
|
+
render: () => (
|
|
113
|
+
<html.form aria-label="Order">
|
|
114
|
+
<RadioGroup.Root name="fruit" defaultValue="apple" aria-label="Fruit">
|
|
115
|
+
<RadioGroup.Item value="apple">Apple</RadioGroup.Item>
|
|
116
|
+
<RadioGroup.Item value="banana">Banana</RadioGroup.Item>
|
|
117
|
+
</RadioGroup.Root>
|
|
118
|
+
</html.form>
|
|
119
|
+
),
|
|
120
|
+
play: async ({canvas, userEvent}) => {
|
|
121
|
+
const form = canvas.getByRole('form', {name: 'Order'}) as HTMLFormElement
|
|
122
|
+
await expect(new FormData(form).get('fruit')).toBe('apple')
|
|
123
|
+
await userEvent.click(canvas.getByText('Banana'))
|
|
124
|
+
await expect(new FormData(form).get('fruit')).toBe('banana')
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const ArrowKeysMoveSelection: Story = {
|
|
129
|
+
render: () => (
|
|
130
|
+
<RadioGroup.Root defaultValue="sm" aria-label="Size">
|
|
131
|
+
<RadioGroup.Item value="sm">Small</RadioGroup.Item>
|
|
132
|
+
<RadioGroup.Item value="md">Medium</RadioGroup.Item>
|
|
133
|
+
<RadioGroup.Item value="lg">Large</RadioGroup.Item>
|
|
134
|
+
</RadioGroup.Root>
|
|
135
|
+
),
|
|
136
|
+
play: async ({canvas, userEvent}) => {
|
|
137
|
+
const group = canvas.getByRole('radiogroup', {name: 'Size'})
|
|
138
|
+
await expect(group).toBeInTheDocument()
|
|
139
|
+
// Arrow keys move the selection within the group, as native radios do.
|
|
140
|
+
const [sm, md] = canvas.getAllByRole('radio')
|
|
141
|
+
sm.focus()
|
|
142
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
143
|
+
await expect(md).toBeChecked()
|
|
144
|
+
await expect(md).toHaveFocus()
|
|
145
|
+
},
|
|
146
|
+
}
|
|
@@ -1,78 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
|
-
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
4
3
|
import {styles} from './styles.css'
|
|
5
|
-
import {useFieldGroupLabelling} from '../Field/FieldContext'
|
|
6
4
|
import {visuallyHidden} from '../../styles/visually-hidden.css'
|
|
7
|
-
|
|
8
|
-
// --- Context ---
|
|
9
|
-
|
|
10
|
-
interface RadioGroupContextValue {
|
|
11
|
-
value: string
|
|
12
|
-
onSelect: (value: string) => void
|
|
13
|
-
disabled: boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const RadioGroupContext = createContext<RadioGroupContextValue | null>(null)
|
|
17
|
-
|
|
18
|
-
function useRadioGroup() {
|
|
19
|
-
const ctx = useContext(RadioGroupContext)
|
|
20
|
-
if (!ctx) {
|
|
21
|
-
throw new Error('RadioGroup.Item must be used within RadioGroup.Root')
|
|
22
|
-
}
|
|
23
|
-
return ctx
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// --- Root ---
|
|
27
|
-
|
|
28
|
-
interface RootProps {
|
|
29
|
-
value?: string
|
|
30
|
-
defaultValue?: string
|
|
31
|
-
onValueChange?: (value: string) => void
|
|
32
|
-
orientation?: 'horizontal' | 'vertical'
|
|
33
|
-
disabled?: boolean
|
|
34
|
-
/** Accessible name when the group is not inside a Field.Root (inside one,
|
|
35
|
-
* the Field.Label names it). */
|
|
36
|
-
'aria-label'?: string
|
|
37
|
-
/** id of the element that names the group, when not inside a Field.Root. */
|
|
38
|
-
'aria-labelledby'?: string
|
|
39
|
-
children: ReactNode
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function Root({
|
|
43
|
-
value: controlledValue,
|
|
44
|
-
defaultValue = '',
|
|
45
|
-
onValueChange,
|
|
46
|
-
orientation = 'vertical',
|
|
47
|
-
disabled = false,
|
|
48
|
-
children,
|
|
49
|
-
...labelling
|
|
50
|
-
}: RootProps) {
|
|
51
|
-
const a11y = useFieldGroupLabelling(labelling)
|
|
52
|
-
const [value, setValue] = useControllableValue(controlledValue, defaultValue, onValueChange)
|
|
53
|
-
|
|
54
|
-
const onSelect = useCallback(
|
|
55
|
-
(itemValue: string) => {
|
|
56
|
-
if (!disabled) {
|
|
57
|
-
setValue(itemValue)
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
[disabled, setValue],
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<RadioGroupContext.Provider value={{value, onSelect, disabled}}>
|
|
65
|
-
<html.div
|
|
66
|
-
role="radiogroup"
|
|
67
|
-
aria-orientation={orientation}
|
|
68
|
-
{...a11y}
|
|
69
|
-
style={[styles.root, orientation === 'horizontal' && styles.rootHorizontal]}
|
|
70
|
-
>
|
|
71
|
-
{children}
|
|
72
|
-
</html.div>
|
|
73
|
-
</RadioGroupContext.Provider>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
5
|
+
import {Root, useRadioGroup} from './group'
|
|
76
6
|
|
|
77
7
|
// --- Item ---
|
|
78
8
|
|
|
@@ -83,20 +13,18 @@ interface ItemProps {
|
|
|
83
13
|
}
|
|
84
14
|
|
|
85
15
|
function Item({value, disabled: itemDisabled = false, children}: ItemProps) {
|
|
86
|
-
const {value: groupValue, onSelect, disabled: groupDisabled} = useRadioGroup()
|
|
16
|
+
const {value: groupValue, name, onSelect, disabled: groupDisabled} = useRadioGroup()
|
|
87
17
|
const isChecked = groupValue === value
|
|
88
18
|
const isDisabled = groupDisabled || itemDisabled
|
|
89
19
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
20
|
+
// The native radio is the only source of a pick: a click on the label
|
|
21
|
+
// activates it, and onChange fires once, only when the selection changes.
|
|
22
|
+
// (A label onClick as well would report every pick twice.)
|
|
96
23
|
return (
|
|
97
|
-
<html.label style={[styles.item, isDisabled && styles.itemDisabled]}
|
|
24
|
+
<html.label style={[styles.item, isDisabled && styles.itemDisabled]}>
|
|
98
25
|
<html.input
|
|
99
26
|
type="radio"
|
|
27
|
+
name={name}
|
|
100
28
|
value={value}
|
|
101
29
|
checked={isChecked}
|
|
102
30
|
disabled={isDisabled}
|
|
@@ -114,9 +42,6 @@ function Item({value, disabled: itemDisabled = false, children}: ItemProps) {
|
|
|
114
42
|
)
|
|
115
43
|
}
|
|
116
44
|
|
|
117
|
-
// inside a Field.Root, the label names this group (see Field's `group`)
|
|
118
|
-
Root.isFieldGroup = true as const
|
|
119
|
-
|
|
120
45
|
export const RadioGroup = {
|
|
121
46
|
Root,
|
|
122
47
|
Item,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {type ReactNode, createContext, useContext, useCallback, useId} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
4
|
+
import {styles} from './styles.css'
|
|
5
|
+
import {useFieldGroupLabelling} from '../Field/FieldContext'
|
|
6
|
+
|
|
7
|
+
// --- Context ---
|
|
8
|
+
|
|
9
|
+
interface RadioGroupContextValue {
|
|
10
|
+
value: string
|
|
11
|
+
name: string
|
|
12
|
+
onSelect: (value: string) => void
|
|
13
|
+
disabled: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const RadioGroupContext = createContext<RadioGroupContextValue | null>(null)
|
|
17
|
+
|
|
18
|
+
export function useRadioGroup() {
|
|
19
|
+
const ctx = useContext(RadioGroupContext)
|
|
20
|
+
if (!ctx) {
|
|
21
|
+
throw new Error('RadioGroup.Item must be used within RadioGroup.Root')
|
|
22
|
+
}
|
|
23
|
+
return ctx
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Root ---
|
|
27
|
+
|
|
28
|
+
interface RootProps {
|
|
29
|
+
value?: string
|
|
30
|
+
defaultValue?: string
|
|
31
|
+
onValueChange?: (value: string) => void
|
|
32
|
+
orientation?: 'horizontal' | 'vertical'
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
/** Form field name: every item's radio carries it, so the selection is
|
|
35
|
+
* submitted with a native form. Generated when omitted (the radios still
|
|
36
|
+
* form one group). */
|
|
37
|
+
name?: string
|
|
38
|
+
/** Accessible name when the group is not inside a Field.Root (inside one,
|
|
39
|
+
* the Field.Label names it). */
|
|
40
|
+
'aria-label'?: string
|
|
41
|
+
/** id of the element that names the group, when not inside a Field.Root. */
|
|
42
|
+
'aria-labelledby'?: string
|
|
43
|
+
children: ReactNode
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function Root({
|
|
47
|
+
value: controlledValue,
|
|
48
|
+
defaultValue = '',
|
|
49
|
+
onValueChange,
|
|
50
|
+
orientation = 'vertical',
|
|
51
|
+
disabled = false,
|
|
52
|
+
name: nameProp,
|
|
53
|
+
children,
|
|
54
|
+
...labelling
|
|
55
|
+
}: RootProps) {
|
|
56
|
+
const a11y = useFieldGroupLabelling(labelling)
|
|
57
|
+
const generatedName = useId()
|
|
58
|
+
const name = nameProp ?? generatedName
|
|
59
|
+
const [value, setValue] = useControllableValue(controlledValue, defaultValue, onValueChange)
|
|
60
|
+
|
|
61
|
+
const onSelect = useCallback(
|
|
62
|
+
(itemValue: string) => {
|
|
63
|
+
if (!disabled) {
|
|
64
|
+
setValue(itemValue)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[disabled, setValue],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<RadioGroupContext.Provider value={{value, name, onSelect, disabled}}>
|
|
72
|
+
<html.div
|
|
73
|
+
role="radiogroup"
|
|
74
|
+
aria-orientation={orientation}
|
|
75
|
+
{...a11y}
|
|
76
|
+
style={[styles.root, orientation === 'horizontal' && styles.rootHorizontal]}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</html.div>
|
|
80
|
+
</RadioGroupContext.Provider>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// inside a Field.Root, the label names this group (see Field's `group`)
|
|
85
|
+
Root.isFieldGroup = true as const
|
|
@@ -24,6 +24,14 @@ export const styles = css.create({
|
|
|
24
24
|
color: colors.text,
|
|
25
25
|
lineHeight: typography.lineHeight,
|
|
26
26
|
},
|
|
27
|
+
// Native draws the item as a button: no button chrome, a flex row.
|
|
28
|
+
nativeItem: {
|
|
29
|
+
display: 'flex',
|
|
30
|
+
flexDirection: 'row',
|
|
31
|
+
borderWidth: 0,
|
|
32
|
+
backgroundColor: 'transparent',
|
|
33
|
+
padding: 0,
|
|
34
|
+
},
|
|
27
35
|
itemDisabled: {
|
|
28
36
|
opacity: 0.5,
|
|
29
37
|
cursor: 'not-allowed',
|
|
@@ -63,4 +71,9 @@ export const styles = css.create({
|
|
|
63
71
|
input: {
|
|
64
72
|
opacity: 0,
|
|
65
73
|
},
|
|
74
|
+
// RSD-native only honors display:'flex' (not 'inline-flex'). Layered on
|
|
75
|
+
// native only.
|
|
76
|
+
nativeFlex: {
|
|
77
|
+
display: 'flex',
|
|
78
|
+
},
|
|
66
79
|
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {useEffect} from 'react'
|
|
2
|
+
import Animated, {
|
|
3
|
+
Easing,
|
|
4
|
+
cancelAnimation,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withRepeat,
|
|
8
|
+
withTiming,
|
|
9
|
+
} from 'react-native-reanimated'
|
|
10
|
+
import {html} from 'react-strict-dom'
|
|
11
|
+
import {ring, TURN_MS} from './ring.css'
|
|
12
|
+
import {visuallyHidden} from '../../styles/visually-hidden.css'
|
|
13
|
+
import type {SpinnerProps} from './Spinner.types'
|
|
14
|
+
|
|
15
|
+
export type {SpinnerProps, SpinnerSize} from './Spinner.types'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Native Spinner: the web's ring, turned by Reanimated on the UI thread (RSD
|
|
19
|
+
* has no CSS animations on native), so a busy JS thread does not stall it.
|
|
20
|
+
*/
|
|
21
|
+
export function Spinner({size = 'md', label = 'Loading'}: SpinnerProps) {
|
|
22
|
+
const turn = useSharedValue(0)
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
turn.value = withRepeat(withTiming(360, {duration: TURN_MS, easing: Easing.linear}), -1)
|
|
25
|
+
return () => cancelAnimation(turn)
|
|
26
|
+
}, [turn])
|
|
27
|
+
const turning = useAnimatedStyle(() => ({transform: [{rotate: `${turn.value}deg`}]}))
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<html.div role="status" style={[ring.root, ring.nativeRoot]}>
|
|
31
|
+
<Animated.View style={turning}>
|
|
32
|
+
<html.div style={[ring.ring, ring[size]]} aria-hidden />
|
|
33
|
+
</Animated.View>
|
|
34
|
+
<html.span style={[visuallyHidden.base, ring.srOnly]}>{label}</html.span>
|
|
35
|
+
</html.div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
import {html} from 'react-strict-dom'
|
|
2
|
+
import {ring} from './ring.css'
|
|
2
3
|
import {styles} from './styles.css'
|
|
3
4
|
import {visuallyHidden} from '../../styles/visually-hidden.css'
|
|
5
|
+
import type {SpinnerProps} from './Spinner.types'
|
|
4
6
|
|
|
5
|
-
export type SpinnerSize
|
|
6
|
-
|
|
7
|
-
interface SpinnerProps {
|
|
8
|
-
size?: SpinnerSize
|
|
9
|
-
label?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const sizeMap = {
|
|
13
|
-
sm: styles.sm,
|
|
14
|
-
md: styles.md,
|
|
15
|
-
lg: styles.lg,
|
|
16
|
-
} as const
|
|
7
|
+
export type {SpinnerProps, SpinnerSize} from './Spinner.types'
|
|
17
8
|
|
|
18
9
|
export function Spinner({size = 'md', label = 'Loading'}: SpinnerProps) {
|
|
19
10
|
return (
|
|
20
|
-
<html.div role="status" style={
|
|
21
|
-
<html.div style={[
|
|
22
|
-
<html.span style={[visuallyHidden.base,
|
|
11
|
+
<html.div role="status" style={ring.root}>
|
|
12
|
+
<html.div style={[ring.ring, ring[size], styles.spinning]} aria-hidden />
|
|
13
|
+
<html.span style={[visuallyHidden.base, ring.srOnly]}>{label}</html.span>
|
|
23
14
|
</html.div>
|
|
24
15
|
)
|
|
25
16
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
4
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
5
|
+
|
|
6
|
+
/** The ring both platforms draw; each turns it its own way. */
|
|
7
|
+
export const ring = css.create({
|
|
8
|
+
root: {
|
|
9
|
+
display: 'inline-flex',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
justifyContent: 'center',
|
|
12
|
+
},
|
|
13
|
+
// RSD-native only honors display:'flex' (not 'inline-flex').
|
|
14
|
+
nativeRoot: {
|
|
15
|
+
display: 'flex',
|
|
16
|
+
},
|
|
17
|
+
ring: {
|
|
18
|
+
borderRadius: '50%',
|
|
19
|
+
borderStyle: 'solid',
|
|
20
|
+
borderColor: colors.border,
|
|
21
|
+
borderTopColor: colors.accent,
|
|
22
|
+
},
|
|
23
|
+
sm: {
|
|
24
|
+
width: sizes.spinnerSm,
|
|
25
|
+
height: sizes.spinnerSm,
|
|
26
|
+
borderWidth: borders.strong,
|
|
27
|
+
},
|
|
28
|
+
md: {
|
|
29
|
+
width: sizes.spinnerMd,
|
|
30
|
+
height: sizes.spinnerMd,
|
|
31
|
+
borderWidth: borders.strong,
|
|
32
|
+
},
|
|
33
|
+
lg: {
|
|
34
|
+
width: sizes.spinnerLg,
|
|
35
|
+
height: sizes.spinnerLg,
|
|
36
|
+
borderWidth: borders.accent,
|
|
37
|
+
},
|
|
38
|
+
// Layered on visuallyHidden.base.
|
|
39
|
+
srOnly: {
|
|
40
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/** One turn, in ms: the web animation's 0.6s. */
|
|
45
|
+
export const TURN_MS = 600
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import {css} from 'react-strict-dom'
|
|
2
|
-
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
-
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
4
2
|
import {easing} from '@duro-app/tokens/tokens/motion.css'
|
|
5
|
-
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
6
3
|
|
|
7
4
|
const spin = css.keyframes({
|
|
8
5
|
'0%': {transform: 'rotate(0deg)'},
|
|
@@ -10,38 +7,11 @@ const spin = css.keyframes({
|
|
|
10
7
|
})
|
|
11
8
|
|
|
12
9
|
export const styles = css.create({
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
alignItems: 'center',
|
|
16
|
-
justifyContent: 'center',
|
|
17
|
-
},
|
|
18
|
-
spinner: {
|
|
19
|
-
borderRadius: '50%',
|
|
20
|
-
borderStyle: 'solid',
|
|
21
|
-
borderColor: colors.border,
|
|
22
|
-
borderTopColor: colors.accent,
|
|
10
|
+
// Web turns the ring with a CSS animation (one turn per TURN_MS).
|
|
11
|
+
spinning: {
|
|
23
12
|
animationName: spin,
|
|
24
13
|
animationDuration: '0.6s',
|
|
25
14
|
animationTimingFunction: easing.linear,
|
|
26
15
|
animationIterationCount: 'infinite',
|
|
27
16
|
},
|
|
28
|
-
sm: {
|
|
29
|
-
width: sizes.spinnerSm,
|
|
30
|
-
height: sizes.spinnerSm,
|
|
31
|
-
borderWidth: borders.strong,
|
|
32
|
-
},
|
|
33
|
-
md: {
|
|
34
|
-
width: sizes.spinnerMd,
|
|
35
|
-
height: sizes.spinnerMd,
|
|
36
|
-
borderWidth: borders.strong,
|
|
37
|
-
},
|
|
38
|
-
lg: {
|
|
39
|
-
width: sizes.spinnerLg,
|
|
40
|
-
height: sizes.spinnerLg,
|
|
41
|
-
borderWidth: borders.accent,
|
|
42
|
-
},
|
|
43
|
-
// Layered on visuallyHidden.base.
|
|
44
|
-
srOnly: {
|
|
45
|
-
clip: 'rect(0, 0, 0, 0)',
|
|
46
|
-
},
|
|
47
17
|
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {html} from 'react-strict-dom'
|
|
2
2
|
import {Icon} from '../Icon'
|
|
3
3
|
import type {IconName, IconSize} from '../Icon'
|
|
4
|
+
import type {ColorToken} from '@duro-app/tokens/keys'
|
|
5
|
+
import {isNative} from '../../platform'
|
|
4
6
|
import {styles} from './styles.css'
|
|
5
7
|
|
|
6
8
|
export type StatusIconName = Extract<
|
|
@@ -9,6 +11,15 @@ export type StatusIconName = Extract<
|
|
|
9
11
|
>
|
|
10
12
|
export type StatusIconVariant = 'error' | 'success' | 'warning' | 'info' | 'muted'
|
|
11
13
|
|
|
14
|
+
// On native an Icon cannot inherit the variant's colour, so it is passed.
|
|
15
|
+
const iconColor = {
|
|
16
|
+
error: 'error',
|
|
17
|
+
success: 'success',
|
|
18
|
+
warning: 'warning',
|
|
19
|
+
info: 'info',
|
|
20
|
+
muted: 'textMuted',
|
|
21
|
+
} as const satisfies Record<StatusIconVariant, ColorToken>
|
|
22
|
+
|
|
12
23
|
interface StatusIconProps {
|
|
13
24
|
name: StatusIconName
|
|
14
25
|
size?: IconSize
|
|
@@ -17,8 +28,8 @@ interface StatusIconProps {
|
|
|
17
28
|
|
|
18
29
|
export function StatusIcon({name, size = 'xxl', variant = 'muted'}: StatusIconProps) {
|
|
19
30
|
return (
|
|
20
|
-
<html.div style={[styles.root, styles[variant]]}>
|
|
21
|
-
<Icon name={name} size={size} />
|
|
31
|
+
<html.div style={[styles.root, isNative && styles.nativeFlex, styles[variant]]}>
|
|
32
|
+
<Icon name={name} size={size} color={isNative ? iconColor[variant] : undefined} />
|
|
22
33
|
</html.div>
|
|
23
34
|
)
|
|
24
35
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {useCallback} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
4
|
+
import {styles} from './styles.css'
|
|
5
|
+
import type {SwitchProps} from './Switch.types'
|
|
6
|
+
|
|
7
|
+
export type {SwitchProps} from './Switch.types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Native Switch: RSD has no `<input type="checkbox">` on native, so the track
|
|
11
|
+
* button carries the switch role. `name` and `value` have no form to submit
|
|
12
|
+
* to here.
|
|
13
|
+
*/
|
|
14
|
+
export function Switch({
|
|
15
|
+
checked: controlledChecked,
|
|
16
|
+
defaultChecked = false,
|
|
17
|
+
onCheckedChange,
|
|
18
|
+
disabled = false,
|
|
19
|
+
children,
|
|
20
|
+
}: SwitchProps) {
|
|
21
|
+
const [checked, setChecked] = useControllableValue(
|
|
22
|
+
controlledChecked,
|
|
23
|
+
defaultChecked,
|
|
24
|
+
onCheckedChange,
|
|
25
|
+
)
|
|
26
|
+
const handleClick = useCallback(() => {
|
|
27
|
+
if (!disabled) setChecked(!checked)
|
|
28
|
+
}, [disabled, checked, setChecked])
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<html.label style={[styles.root, styles.nativeFlex, disabled && styles.rootDisabled]}>
|
|
32
|
+
<html.button
|
|
33
|
+
type="button"
|
|
34
|
+
role="switch"
|
|
35
|
+
aria-checked={checked}
|
|
36
|
+
disabled={disabled}
|
|
37
|
+
onClick={handleClick}
|
|
38
|
+
style={[styles.track, checked ? styles.trackChecked : styles.trackUnchecked]}
|
|
39
|
+
>
|
|
40
|
+
<html.span style={[styles.thumb, checked && styles.thumbChecked]} />
|
|
41
|
+
</html.button>
|
|
42
|
+
{children && <html.span>{children}</html.span>}
|
|
43
|
+
</html.label>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -1,26 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {useCallback, useRef} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
|
-
import {isNative} from '../../platform'
|
|
4
3
|
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
5
4
|
import {styles} from './styles.css'
|
|
6
5
|
import {visuallyHidden} from '../../styles/visually-hidden.css'
|
|
6
|
+
import type {SwitchProps} from './Switch.types'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
/** Controlled checked state. */
|
|
10
|
-
checked?: boolean
|
|
11
|
-
/** Initial checked state (uncontrolled). */
|
|
12
|
-
defaultChecked?: boolean
|
|
13
|
-
/** Callback fired when the checked state changes. */
|
|
14
|
-
onCheckedChange?: (checked: boolean) => void
|
|
15
|
-
/** Prevents user interaction. */
|
|
16
|
-
disabled?: boolean
|
|
17
|
-
/** Form field name for the hidden input. */
|
|
18
|
-
name?: string
|
|
19
|
-
/** Value submitted when checked. */
|
|
20
|
-
value?: string
|
|
21
|
-
/** Label content rendered beside the switch. */
|
|
22
|
-
children?: ReactNode
|
|
23
|
-
}
|
|
8
|
+
export type {SwitchProps} from './Switch.types'
|
|
24
9
|
|
|
25
10
|
export function Switch({
|
|
26
11
|
checked: controlledChecked,
|
|
@@ -48,35 +33,28 @@ export function Switch({
|
|
|
48
33
|
}, [disabled, checked, setChecked])
|
|
49
34
|
|
|
50
35
|
return (
|
|
51
|
-
<html.label
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
onCheckedChange?.(e.target.checked)
|
|
70
|
-
}}
|
|
71
|
-
style={[visuallyHidden.base, styles.input]}
|
|
72
|
-
/>
|
|
73
|
-
)}
|
|
36
|
+
<html.label style={[styles.root, disabled && styles.rootDisabled]}>
|
|
37
|
+
{/* The hidden real checkbox carries a11y and form submission (native:
|
|
38
|
+
Switch.native.tsx, whose button carries the switch role). */}
|
|
39
|
+
<html.input
|
|
40
|
+
ref={inputRef}
|
|
41
|
+
type="checkbox"
|
|
42
|
+
role="switch"
|
|
43
|
+
name={name}
|
|
44
|
+
value={value}
|
|
45
|
+
checked={controlledChecked !== undefined ? controlledChecked : undefined}
|
|
46
|
+
defaultChecked={controlledChecked === undefined ? defaultChecked : undefined}
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
aria-checked={checked}
|
|
49
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
50
|
+
onCheckedChange?.(e.target.checked)
|
|
51
|
+
}}
|
|
52
|
+
style={[visuallyHidden.base, styles.input]}
|
|
53
|
+
/>
|
|
74
54
|
<html.button
|
|
75
55
|
type="button"
|
|
76
56
|
tabIndex={-1}
|
|
77
|
-
aria-hidden
|
|
78
|
-
role={isNative ? 'switch' : undefined}
|
|
79
|
-
aria-checked={isNative ? checked : undefined}
|
|
57
|
+
aria-hidden
|
|
80
58
|
disabled={disabled}
|
|
81
59
|
onClick={handleClick}
|
|
82
60
|
style={[styles.track, checked ? styles.trackChecked : styles.trackUnchecked]}
|