@duro-app/ui 0.34.8 → 0.34.10
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/components/Button/Button.d.ts +3 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/components/Checkbox/styles.css.d.ts +25 -3
- package/dist/components/Checkbox/styles.css.d.ts.map +1 -1
- package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -1
- package/dist/components/Switch/Switch.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +2407 -2389
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Button/Button.tsx +4 -0
- package/src/components/Checkbox/Checkbox.tsx +51 -8
- package/src/components/Checkbox/styles.css.ts +33 -4
- package/src/components/Combobox/useComboboxRoot.ts +12 -2
- package/src/components/Switch/Switch.tsx +23 -16
- package/src/components/Table/Table.stories.tsx +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@duro-app/tokens": "^0.34.
|
|
57
|
+
"@duro-app/tokens": "^0.34.10"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -13,6 +13,8 @@ interface ButtonProps {
|
|
|
13
13
|
disabled?: boolean
|
|
14
14
|
type?: 'button' | 'submit'
|
|
15
15
|
onClick?: () => void
|
|
16
|
+
/** Accessible name override — e.g. for buttons whose visible text isn't unique. */
|
|
17
|
+
'aria-label'?: string
|
|
16
18
|
children: ReactNode
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -28,6 +30,7 @@ export function Button({
|
|
|
28
30
|
disabled = false,
|
|
29
31
|
type = 'button',
|
|
30
32
|
onClick,
|
|
33
|
+
'aria-label': ariaLabel,
|
|
31
34
|
children,
|
|
32
35
|
}: ButtonProps) {
|
|
33
36
|
return (
|
|
@@ -35,6 +38,7 @@ export function Button({
|
|
|
35
38
|
type={type}
|
|
36
39
|
disabled={disabled}
|
|
37
40
|
onClick={onClick}
|
|
41
|
+
aria-label={ariaLabel}
|
|
38
42
|
style={[
|
|
39
43
|
styles.base,
|
|
40
44
|
isNative && styles.nativeFlex,
|
|
@@ -29,6 +29,7 @@ export function Checkbox({
|
|
|
29
29
|
const [internalChecked, setInternalChecked] = useState(defaultChecked)
|
|
30
30
|
const isChecked = isControlled ? controlledChecked : internalChecked
|
|
31
31
|
|
|
32
|
+
// Web: the real <input type=checkbox> drives toggling, a11y and form submit.
|
|
32
33
|
const handleChange = useCallback(
|
|
33
34
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
34
35
|
if (!isControlled) {
|
|
@@ -39,10 +40,56 @@ export function Checkbox({
|
|
|
39
40
|
[isControlled, onChange],
|
|
40
41
|
)
|
|
41
42
|
|
|
43
|
+
// Native: RSD only makes <button> pressable and has no <input type=checkbox>,
|
|
44
|
+
// so the whole row is a button and we synthesize the change payload that
|
|
45
|
+
// consumers read (e.target.checked).
|
|
46
|
+
const handlePress = useCallback(() => {
|
|
47
|
+
if (disabled) return
|
|
48
|
+
const next = !isChecked
|
|
49
|
+
if (!isControlled) {
|
|
50
|
+
setInternalChecked(next)
|
|
51
|
+
}
|
|
52
|
+
onChange?.({
|
|
53
|
+
target: {checked: next, name, value},
|
|
54
|
+
} as unknown as React.ChangeEvent<HTMLInputElement>)
|
|
55
|
+
}, [disabled, isChecked, isControlled, onChange, name, value])
|
|
56
|
+
|
|
57
|
+
if (isNative) {
|
|
58
|
+
return (
|
|
59
|
+
<html.button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={handlePress}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
role="checkbox"
|
|
64
|
+
aria-checked={isChecked}
|
|
65
|
+
aria-label={!children ? ariaLabel : undefined}
|
|
66
|
+
style={[
|
|
67
|
+
styles.root,
|
|
68
|
+
styles.nativeFlex,
|
|
69
|
+
styles.nativeButton,
|
|
70
|
+
disabled && styles.rootDisabled,
|
|
71
|
+
]}
|
|
72
|
+
>
|
|
73
|
+
<html.div
|
|
74
|
+
style={[
|
|
75
|
+
styles.box,
|
|
76
|
+
styles.nativeFlex,
|
|
77
|
+
isChecked ? styles.boxChecked : styles.boxUnchecked,
|
|
78
|
+
]}
|
|
79
|
+
>
|
|
80
|
+
{/* CSS-border checkmark (rotated right+bottom borders) — no SVG, no
|
|
81
|
+
react-native-svg dep, so nothing is added to the web bundle. */}
|
|
82
|
+
<html.div
|
|
83
|
+
style={[styles.checkmark, isChecked ? styles.checkmarkVisible : styles.checkmarkHidden]}
|
|
84
|
+
/>
|
|
85
|
+
</html.div>
|
|
86
|
+
{children && <html.span style={styles.labelText}>{children}</html.span>}
|
|
87
|
+
</html.button>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
42
91
|
return (
|
|
43
|
-
<html.label
|
|
44
|
-
style={[styles.root, isNative && styles.nativeFlex, disabled && styles.rootDisabled]}
|
|
45
|
-
>
|
|
92
|
+
<html.label style={[styles.root, disabled && styles.rootDisabled]}>
|
|
46
93
|
<html.input
|
|
47
94
|
type="checkbox"
|
|
48
95
|
name={name}
|
|
@@ -55,11 +102,7 @@ export function Checkbox({
|
|
|
55
102
|
style={styles.input}
|
|
56
103
|
/>
|
|
57
104
|
<html.span
|
|
58
|
-
style={[
|
|
59
|
-
styles.box,
|
|
60
|
-
isNative && styles.nativeFlex,
|
|
61
|
-
isChecked ? styles.boxChecked : styles.boxUnchecked,
|
|
62
|
-
]}
|
|
105
|
+
style={[styles.box, isChecked ? styles.boxChecked : styles.boxUnchecked]}
|
|
63
106
|
aria-hidden
|
|
64
107
|
>
|
|
65
108
|
<svg
|
|
@@ -42,10 +42,39 @@ export const styles = css.create({
|
|
|
42
42
|
backgroundColor: colors.accent,
|
|
43
43
|
borderColor: colors.accent,
|
|
44
44
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
// Native-only checkmark: a small box showing just its right + bottom border,
|
|
46
|
+
// rotated 45° into a tick. Pure CSS (RSD translates the transform string to
|
|
47
|
+
// an RN transform), so no SVG / react-native-svg weight reaches web.
|
|
48
|
+
checkmark: {
|
|
49
|
+
width: 5,
|
|
50
|
+
height: 9,
|
|
51
|
+
borderRightWidth: 2,
|
|
52
|
+
borderRightStyle: 'solid',
|
|
53
|
+
borderRightColor: colors.accentContrast,
|
|
54
|
+
borderBottomWidth: 2,
|
|
55
|
+
borderBottomStyle: 'solid',
|
|
56
|
+
borderBottomColor: colors.accentContrast,
|
|
57
|
+
transform: 'rotate(45deg)',
|
|
58
|
+
marginTop: -2,
|
|
59
|
+
},
|
|
60
|
+
checkmarkVisible: {
|
|
61
|
+
opacity: 1,
|
|
62
|
+
},
|
|
63
|
+
checkmarkHidden: {
|
|
64
|
+
opacity: 0,
|
|
65
|
+
},
|
|
66
|
+
// Native label text — RN <Text> doesn't inherit color/size from the button
|
|
67
|
+
// ancestor the way web inherits from <label>, so set it explicitly.
|
|
68
|
+
labelText: {
|
|
69
|
+
fontSize: typography.fontSizeSm,
|
|
70
|
+
color: colors.text,
|
|
71
|
+
},
|
|
72
|
+
// Strip the default button chrome RSD gives <button> on native (border/bg/
|
|
73
|
+
// padding mimic the browser default) so the row matches the web checkbox.
|
|
74
|
+
nativeButton: {
|
|
75
|
+
borderWidth: 0,
|
|
76
|
+
backgroundColor: 'transparent',
|
|
77
|
+
padding: 0,
|
|
49
78
|
},
|
|
50
79
|
input: {
|
|
51
80
|
position: 'absolute',
|
|
@@ -24,7 +24,13 @@ export function useComboboxRoot({
|
|
|
24
24
|
defaultValue ?? null,
|
|
25
25
|
onValueChange,
|
|
26
26
|
)
|
|
27
|
-
|
|
27
|
+
// Seed the visible text from the initially-selected value's label, so a
|
|
28
|
+
// defaultValue/value shows its label on mount — and so the reconcile effect
|
|
29
|
+
// below doesn't see "value set but input empty" and wipe the selection.
|
|
30
|
+
const [internalInputValue, setInternalInputValue] = useState<string>(() => {
|
|
31
|
+
const initial = controlledValue ?? defaultValue
|
|
32
|
+
return initial ? (initialLabels?.[initial] ?? '') : ''
|
|
33
|
+
})
|
|
28
34
|
const inputValue = controlledInputValue !== undefined ? controlledInputValue : internalInputValue
|
|
29
35
|
const setInputValue = useCallback(
|
|
30
36
|
(v: string) => {
|
|
@@ -201,7 +207,11 @@ export function useComboboxRoot({
|
|
|
201
207
|
// - inputValue equals the selected label: nothing to do.
|
|
202
208
|
useEffect(() => {
|
|
203
209
|
if (open) return
|
|
204
|
-
|
|
210
|
+
// Treat an empty value (null OR '') as "no selection" — nothing to
|
|
211
|
+
// reconcile. Without this, a controlled value="" (e.g. a table column
|
|
212
|
+
// filter) fires setValue(null) every render → inline onValueChange →
|
|
213
|
+
// parent state change → re-render → infinite "Maximum update depth" loop.
|
|
214
|
+
if (!value) return
|
|
205
215
|
if (inputValue === '') {
|
|
206
216
|
setValue(null)
|
|
207
217
|
return
|
|
@@ -50,25 +50,32 @@ export function Switch({
|
|
|
50
50
|
<html.label
|
|
51
51
|
style={[styles.root, isNative && styles.nativeFlex, disabled && styles.rootDisabled]}
|
|
52
52
|
>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
type=
|
|
56
|
-
role
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
53
|
+
{!isNative && (
|
|
54
|
+
// Web: hidden real checkbox carries a11y + form submission. RSD has no
|
|
55
|
+
// <input type=checkbox> on native, so it's omitted there and the
|
|
56
|
+
// button below carries the switch role instead.
|
|
57
|
+
<html.input
|
|
58
|
+
ref={inputRef}
|
|
59
|
+
type="checkbox"
|
|
60
|
+
role="switch"
|
|
61
|
+
name={name}
|
|
62
|
+
value={value}
|
|
63
|
+
checked={controlledChecked !== undefined ? controlledChecked : undefined}
|
|
64
|
+
defaultChecked={controlledChecked === undefined ? defaultChecked : undefined}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
aria-checked={checked}
|
|
67
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
68
|
+
onCheckedChange?.(e.target.checked)
|
|
69
|
+
}}
|
|
70
|
+
style={styles.input}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
68
73
|
<html.button
|
|
69
74
|
type="button"
|
|
70
75
|
tabIndex={-1}
|
|
71
|
-
aria-hidden
|
|
76
|
+
aria-hidden={isNative ? undefined : true}
|
|
77
|
+
role={isNative ? 'switch' : undefined}
|
|
78
|
+
aria-checked={isNative ? checked : undefined}
|
|
72
79
|
disabled={disabled}
|
|
73
80
|
onClick={handleClick}
|
|
74
81
|
style={[styles.track, checked ? styles.trackChecked : styles.trackUnchecked]}
|
|
@@ -1128,7 +1128,9 @@ export const SlotPropsOnRoot: Story = {
|
|
|
1128
1128
|
play: async ({canvas}) => {
|
|
1129
1129
|
await expect(canvas.getByRole('table')).toBeInTheDocument()
|
|
1130
1130
|
// Note no Table.HeaderCell label — children are plain strings, so the
|
|
1131
|
-
// label fallback uses extractText.
|
|
1132
|
-
|
|
1131
|
+
// label fallback uses extractText. "Name" appears both in the header and
|
|
1132
|
+
// the SortChip option list, so assert at least one rendered (not getByText,
|
|
1133
|
+
// which throws on multiple matches).
|
|
1134
|
+
await expect(canvas.getAllByText('Name').length).toBeGreaterThan(0)
|
|
1133
1135
|
},
|
|
1134
1136
|
}
|