@duro-app/ui 5.11.0 → 5.13.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-Bms2ftt3.js +1694 -0
- package/dist/Table-Bms2ftt3.js.map +1 -0
- package/dist/compiler-runtime-BuugS4D0.js.map +1 -1
- package/dist/components/Alert/Alert.d.ts.map +1 -1
- package/dist/components/AppShell/native.css.d.ts +53 -0
- package/dist/components/AppShell/native.css.d.ts.map +1 -0
- 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 +7 -35
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
- package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
- package/dist/components/Drawer/DrawerSnap.stories.d.ts +20 -0
- package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
- package/dist/components/Drawer/native.css.d.ts +55 -0
- package/dist/components/Drawer/native.css.d.ts.map +1 -0
- package/dist/components/Drawer/parts.css.d.ts +96 -0
- package/dist/components/Drawer/parts.css.d.ts.map +1 -0
- package/dist/components/Drawer/parts.d.ts +56 -0
- package/dist/components/Drawer/parts.d.ts.map +1 -0
- package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
- package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
- package/dist/components/Drawer/styles.css.d.ts +15 -58
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/touchInput.d.ts +35 -0
- package/dist/components/Drawer/touchInput.d.ts.map +1 -0
- package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
- package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
- package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
- package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
- package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
- package/dist/components/Drawer/useSwipeDismiss.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/Menu/Menu.d.ts +7 -1
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/native.css.d.ts +14 -0
- package/dist/components/Menu/native.css.d.ts.map +1 -0
- package/dist/components/Menu/useMenuOpen.d.ts +16 -0
- package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
- package/dist/components/Menu/useMenuRoot.d.ts +3 -6
- package/dist/components/Menu/useMenuRoot.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/Popover/native.css.d.ts +11 -0
- package/dist/components/Popover/native.css.d.ts.map +1 -0
- 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/SideNav/Chevron.d.ts +4 -0
- package/dist/components/SideNav/Chevron.d.ts.map +1 -0
- package/dist/components/SideNav/SideNav.d.ts.map +1 -1
- package/dist/components/SideNav/styles.css.d.ts +6 -0
- package/dist/components/SideNav/styles.css.d.ts.map +1 -1
- package/dist/components/SideNav/types.d.ts +6 -0
- package/dist/components/SideNav/types.d.ts.map +1 -0
- 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/SplitPane/SplitPane.d.ts +38 -0
- package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
- package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
- package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
- package/dist/components/SplitPane/styles.css.d.ts +78 -0
- package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
- 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/Tabs/TabLayout.d.ts +7 -0
- package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/components/Tabs/indicator.d.ts +28 -0
- package/dist/components/Tabs/indicator.d.ts.map +1 -0
- package/dist/components/Tabs/styles.css.d.ts +4 -0
- package/dist/components/Tabs/styles.css.d.ts.map +1 -1
- package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
- package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
- package/dist/components/Tabs/useTabKeys.d.ts +13 -0
- package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
- package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
- package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
- package/dist/components/Tag/Tag.d.ts +2 -24
- package/dist/components/Tag/Tag.d.ts.map +1 -1
- package/dist/components/Tag/styles.css.d.ts +8 -0
- package/dist/components/Tag/styles.css.d.ts.map +1 -1
- package/dist/components/Tag/types.d.ts +39 -0
- package/dist/components/Tag/types.d.ts.map +1 -0
- package/dist/components/Textarea/styles.css.d.ts +1 -0
- package/dist/components/Textarea/styles.css.d.ts.map +1 -1
- package/dist/components/Toast/ToastProvider.d.ts +2 -21
- package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
- package/dist/components/Toast/native.css.d.ts +81 -0
- package/dist/components/Toast/native.css.d.ts.map +1 -0
- package/dist/components/Toast/store.d.ts +47 -0
- package/dist/components/Toast/store.d.ts.map +1 -0
- 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/Tooltip/TooltipHint.d.ts +9 -0
- package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
- package/dist/components/Tree/Tree.d.ts.map +1 -1
- package/dist/components/Tree/styles.css.d.ts +6 -0
- package/dist/components/Tree/styles.css.d.ts.map +1 -1
- package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
- package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.d.ts +33 -0
- package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
- package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
- package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
- package/dist/components/VirtualList/styles.css.d.ts +28 -0
- package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
- 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.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6202 -4794
- package/dist/index.js.map +1 -1
- package/dist/shared/ButtonHost.d.ts +11 -0
- package/dist/shared/ButtonHost.d.ts.map +1 -0
- package/dist/shared/portal/hostRegistry.d.ts +33 -0
- package/dist/shared/portal/hostRegistry.d.ts.map +1 -0
- package/dist/shared/pressableChild.d.ts +14 -0
- package/dist/shared/pressableChild.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/AppShell/AppShell.meta.ts +1 -1
- package/src/components/AppShell/AppShell.native.tsx +174 -0
- package/src/components/AppShell/native.css.ts +56 -0
- package/src/components/Button/Button.tsx +6 -5
- 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.meta.ts +2 -1
- package/src/components/Drawer/Drawer.native.tsx +469 -0
- package/src/components/Drawer/Drawer.tsx +63 -179
- package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
- package/src/components/Drawer/DrawerSnap.stories.tsx +205 -0
- package/src/components/Drawer/native.css.ts +58 -0
- package/src/components/Drawer/parts.css.ts +125 -0
- package/src/components/Drawer/parts.tsx +110 -0
- package/src/components/Drawer/sheetPhysics.ts +103 -0
- package/src/components/Drawer/styles.css.ts +19 -74
- package/src/components/Drawer/touchInput.ts +81 -0
- package/src/components/Drawer/useDrawerRoot.ts +90 -0
- package/src/components/Drawer/useSheetHeights.ts +42 -0
- package/src/components/Drawer/useSwipeDismiss.ts +146 -23
- 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/Menu/Menu.meta.ts +1 -1
- package/src/components/Menu/Menu.native.tsx +268 -0
- package/src/components/Menu/Menu.tsx +11 -2
- package/src/components/Menu/native.css.ts +18 -0
- package/src/components/Menu/useMenuOpen.ts +33 -0
- package/src/components/Menu/useMenuRoot.ts +7 -26
- package/src/components/Meter/styles.css.ts +2 -0
- package/src/components/PageShell/styles.css.ts +4 -0
- package/src/components/Popover/Popover.meta.ts +1 -1
- package/src/components/Popover/Popover.native.tsx +205 -0
- package/src/components/Popover/native.css.ts +13 -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/SideNav/Chevron.native.tsx +20 -0
- package/src/components/SideNav/Chevron.tsx +20 -0
- package/src/components/SideNav/SideNav.meta.ts +1 -1
- package/src/components/SideNav/SideNav.tsx +57 -41
- package/src/components/SideNav/styles.css.ts +8 -0
- package/src/components/SideNav/types.ts +6 -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/SplitPane/SplitPane.meta.ts +40 -0
- package/src/components/SplitPane/SplitPane.native.tsx +47 -0
- package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
- package/src/components/SplitPane/SplitPane.tsx +218 -0
- package/src/components/SplitPane/styles.css.ts +81 -0
- 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/Tabs/TabLayout.native.tsx +9 -0
- package/src/components/Tabs/TabLayout.tsx +6 -0
- package/src/components/Tabs/Tabs.meta.ts +1 -1
- package/src/components/Tabs/Tabs.tsx +38 -136
- package/src/components/Tabs/indicator.ts +36 -0
- package/src/components/Tabs/styles.css.ts +7 -0
- package/src/components/Tabs/useTabIndicator.native.ts +35 -0
- package/src/components/Tabs/useTabIndicator.ts +49 -0
- package/src/components/Tabs/useTabKeys.native.ts +2 -0
- package/src/components/Tabs/useTabKeys.ts +99 -0
- package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
- package/src/components/Tag/RemoveGlyph.tsx +10 -0
- package/src/components/Tag/Tag.meta.ts +1 -1
- package/src/components/Tag/Tag.native.tsx +70 -0
- package/src/components/Tag/Tag.tsx +9 -37
- package/src/components/Tag/styles.css.ts +9 -0
- package/src/components/Tag/types.ts +43 -0
- package/src/components/Textarea/styles.css.ts +2 -0
- package/src/components/ThemeProvider/ThemeProvider.native.tsx +70 -10
- package/src/components/Toast/Toast.meta.ts +1 -1
- package/src/components/Toast/ToastProvider.native.tsx +226 -0
- package/src/components/Toast/ToastProvider.tsx +22 -84
- package/src/components/Toast/native.css.ts +78 -0
- package/src/components/Toast/store.ts +96 -0
- package/src/components/Toggle/Toggle.tsx +5 -3
- package/src/components/Toggle/styles.css.ts +14 -2
- package/src/components/Tooltip/Tooltip.meta.ts +1 -1
- package/src/components/Tooltip/Tooltip.native.tsx +36 -0
- package/src/components/Tooltip/TooltipHint.ts +13 -0
- package/src/components/Tree/Tree.meta.ts +1 -1
- package/src/components/Tree/Tree.tsx +23 -135
- package/src/components/Tree/styles.css.ts +6 -0
- package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
- package/src/components/Tree/useTreeKeyboard.ts +152 -0
- package/src/components/VirtualList/VirtualList.meta.ts +39 -0
- package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
- package/src/components/VirtualList/VirtualList.tsx +103 -0
- package/src/components/VirtualList/styles.css.ts +31 -0
- package/src/components/VirtualTable/styles.css.ts +12 -0
- package/src/hooks/useContainerBelow.native.ts +30 -0
- package/src/index.native.ts +61 -1
- package/src/index.ts +8 -1
- package/src/native-guard/globals.d.ts +10 -0
- package/src/native-guard/react-dom.d.ts +3 -0
- package/src/shared/ButtonHost.native.tsx +70 -0
- package/src/shared/ButtonHost.tsx +12 -0
- package/src/shared/accessibilityFocus.native.ts +23 -0
- package/src/shared/portal/AnchoredOverlay.native.tsx +117 -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/pressableChild.ts +33 -0
- package/src/shared/safeArea.native.ts +22 -0
- package/src/shared/useKeyboardHeight.native.ts +18 -0
- package/src/shared/useOverlayOpenState.ts +128 -0
- package/src/shared/useScreenReaderEnabled.native.ts +19 -0
- package/dist/Table-ColfpgKV.js +0 -1552
- package/dist/Table-ColfpgKV.js.map +0 -1
- 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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {styles} from './styles.css'
|
|
4
|
+
import {Root, useRadioGroup} from './group'
|
|
5
|
+
|
|
6
|
+
interface ItemProps {
|
|
7
|
+
value: string
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
children: ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Native radio item: RSD renders `<input type="radio">` as a text field on
|
|
14
|
+
* native, so the item is a button with the radio role, drawn like the web one.
|
|
15
|
+
*/
|
|
16
|
+
function Item({value, disabled: itemDisabled = false, children}: ItemProps) {
|
|
17
|
+
const {value: groupValue, onSelect, disabled: groupDisabled} = useRadioGroup()
|
|
18
|
+
const isChecked = groupValue === value
|
|
19
|
+
const isDisabled = groupDisabled || itemDisabled
|
|
20
|
+
return (
|
|
21
|
+
<html.button
|
|
22
|
+
type="button"
|
|
23
|
+
role="radio"
|
|
24
|
+
aria-checked={isChecked}
|
|
25
|
+
disabled={isDisabled}
|
|
26
|
+
onClick={() => onSelect(value)}
|
|
27
|
+
style={[styles.item, styles.nativeItem, isDisabled && styles.itemDisabled]}
|
|
28
|
+
>
|
|
29
|
+
{/* div, not span: a native span is a Text, which draws no sized box. */}
|
|
30
|
+
<html.div
|
|
31
|
+
style={[
|
|
32
|
+
styles.circle,
|
|
33
|
+
styles.nativeFlex,
|
|
34
|
+
isChecked ? styles.circleChecked : styles.circleUnchecked,
|
|
35
|
+
]}
|
|
36
|
+
aria-hidden
|
|
37
|
+
>
|
|
38
|
+
{isChecked && <html.div style={styles.dot} />}
|
|
39
|
+
</html.div>
|
|
40
|
+
{children && <html.span>{children}</html.span>}
|
|
41
|
+
</html.button>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const RadioGroup = {
|
|
46
|
+
Root,
|
|
47
|
+
Item,
|
|
48
|
+
}
|
|
@@ -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,20 @@
|
|
|
1
|
+
import Svg, {Path} from 'react-native-svg'
|
|
2
|
+
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
3
|
+
import {useResolvedColors} from '../ThemeProvider/ThemeProvider'
|
|
4
|
+
import type {ChevronProps} from './types'
|
|
5
|
+
|
|
6
|
+
/** The web's chevron, drawn with react-native-svg in the trigger's resolved colour. */
|
|
7
|
+
export function Chevron({color}: ChevronProps) {
|
|
8
|
+
const stroke = useResolvedColors()[color]
|
|
9
|
+
return (
|
|
10
|
+
<Svg width={SIZES_PX.glyphXs} height={SIZES_PX.glyphXs} viewBox="0 0 24 24" fill="none">
|
|
11
|
+
<Path
|
|
12
|
+
d="M9 18l6-6-6-6"
|
|
13
|
+
stroke={stroke}
|
|
14
|
+
strokeWidth={2.5}
|
|
15
|
+
strokeLinecap="round"
|
|
16
|
+
strokeLinejoin="round"
|
|
17
|
+
/>
|
|
18
|
+
</Svg>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
2
|
+
import type {ChevronProps} from './types'
|
|
3
|
+
|
|
4
|
+
/** The group disclosure's chevron, drawn in the trigger's text colour. */
|
|
5
|
+
export function Chevron(_props: ChevronProps) {
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
width={SIZES_PX.glyphXs}
|
|
9
|
+
height={SIZES_PX.glyphXs}
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
strokeWidth={2.5}
|
|
14
|
+
strokeLinecap="round"
|
|
15
|
+
strokeLinejoin="round"
|
|
16
|
+
>
|
|
17
|
+
<path d="M9 18l6-6-6-6" />
|
|
18
|
+
</svg>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Vertical side navigation. `Section` is a labelled block that is always open — the default for primary navigation. `Group` is the same block behind a chevron (a disclosure) — reach for it only for a rare/advanced region, an unbounded data-driven list, or a rail long enough that flat stops reading as an overview; mixing the two in one rail is the healthy shape. `Item`s take an optional `icon` and show an active left-marker. Supports controlled and uncontrolled active item. Compound component — Root is required.',
|
|
5
|
+
'Vertical side navigation. `Section` is a labelled block that is always open — the default for primary navigation. `Group` is the same block behind a chevron (a disclosure) — reach for it only for a rare/advanced region, an unbounded data-driven list, or a rail long enough that flat stops reading as an overview; mixing the two in one rail is the healthy shape. `Item`s take an optional `icon` and show an active left-marker. Supports controlled and uncontrolled active item. Compound component — Root is required. On React Native the same parts render, and a Group knows whether it holds the active item from its Items, not from the DOM.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'App-level sidebar navigation — flat `Section`s, every destination visible',
|
|
8
8
|
'Settings navigation with labelled regions',
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ReactNode,
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useState,
|
|
6
|
+
useCallback,
|
|
7
|
+
useRef,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
} from 'react'
|
|
2
11
|
import {html} from 'react-strict-dom'
|
|
3
12
|
import {styles} from './styles.css'
|
|
4
13
|
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
5
14
|
import {SideNavContext, useSideNav} from './SideNavContext'
|
|
15
|
+
import {isNative} from '../../platform'
|
|
16
|
+
import {Chevron} from './Chevron'
|
|
17
|
+
|
|
18
|
+
/** A Group's own items: each Item inside it registers its value here. */
|
|
19
|
+
const GroupItemsContext = createContext<((value: string) => () => void) | null>(null)
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* SideNav has two ways to chunk a rail, and they are NOT interchangeable.
|
|
@@ -130,39 +144,40 @@ function Group({children, label, groupKey, defaultExpanded}: GroupProps) {
|
|
|
130
144
|
const key = groupKey ?? label
|
|
131
145
|
const {expandedGroups, toggleGroup, expandGroup, activeValue} = useSideNav()
|
|
132
146
|
const isExpanded = expandedGroups.has(key)
|
|
133
|
-
const groupRef = useRef<HTMLDivElement>(null)
|
|
134
147
|
const triggerId = useId()
|
|
135
148
|
const panelId = useId()
|
|
149
|
+
// The values of the Items inside, from props and state rather than a DOM
|
|
150
|
+
// query, so both platforms know whether the active item is in here.
|
|
151
|
+
const [values, setValues] = useState<ReadonlySet<string>>(() => new Set())
|
|
152
|
+
const registerValue = useCallback((value: string) => {
|
|
153
|
+
setValues((prev) => (prev.has(value) ? prev : new Set(prev).add(value)))
|
|
154
|
+
return () =>
|
|
155
|
+
setValues((prev) => {
|
|
156
|
+
if (!prev.has(value)) return prev
|
|
157
|
+
const next = new Set(prev)
|
|
158
|
+
next.delete(value)
|
|
159
|
+
return next
|
|
160
|
+
})
|
|
161
|
+
}, [])
|
|
162
|
+
const hasActiveChild = activeValue !== null && values.has(activeValue)
|
|
136
163
|
|
|
137
164
|
// Auto-expand if this group contains the active item. Uses expandGroup (not
|
|
138
165
|
// toggleGroup) so it never races the defaultExpanded effect into a closed
|
|
139
166
|
// state when the active item lives in a defaultExpanded group.
|
|
140
167
|
useEffect(() => {
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
if (!el) return
|
|
144
|
-
const activeBtn = el.querySelector(`[data-nav-value="${activeValue}"]`)
|
|
145
|
-
if (activeBtn) {
|
|
146
|
-
expandGroup(key)
|
|
147
|
-
}
|
|
148
|
-
}, [activeValue, key, expandedGroups, expandGroup])
|
|
168
|
+
if (hasActiveChild && !expandedGroups.has(key)) expandGroup(key)
|
|
169
|
+
}, [hasActiveChild, key, expandedGroups, expandGroup])
|
|
149
170
|
|
|
150
|
-
// Expand on
|
|
171
|
+
// Expand once, on mount, if defaultExpanded (idempotent add, not a toggle).
|
|
172
|
+
const appliedDefault = useRef(false)
|
|
151
173
|
useEffect(() => {
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
157
|
-
}, [])
|
|
158
|
-
|
|
159
|
-
const hasActiveChild = (() => {
|
|
160
|
-
if (!activeValue || !groupRef.current) return false
|
|
161
|
-
return !!groupRef.current.querySelector(`[data-nav-value="${activeValue}"]`)
|
|
162
|
-
})()
|
|
174
|
+
if (appliedDefault.current) return
|
|
175
|
+
appliedDefault.current = true
|
|
176
|
+
if (defaultExpanded) expandGroup(key)
|
|
177
|
+
}, [defaultExpanded, expandGroup, key])
|
|
163
178
|
|
|
164
179
|
return (
|
|
165
|
-
<html.div
|
|
180
|
+
<html.div style={styles.group}>
|
|
166
181
|
<html.button
|
|
167
182
|
type="button"
|
|
168
183
|
id={triggerId}
|
|
@@ -171,27 +186,26 @@ function Group({children, label, groupKey, defaultExpanded}: GroupProps) {
|
|
|
171
186
|
aria-expanded={isExpanded}
|
|
172
187
|
aria-controls={isExpanded ? panelId : undefined}
|
|
173
188
|
>
|
|
174
|
-
<html.span
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
viewBox="0 0 24 24"
|
|
179
|
-
fill="none"
|
|
180
|
-
stroke="currentColor"
|
|
181
|
-
strokeWidth={2.5}
|
|
182
|
-
strokeLinecap="round"
|
|
183
|
-
strokeLinejoin="round"
|
|
184
|
-
>
|
|
185
|
-
<path d="M9 18l6-6-6-6" />
|
|
186
|
-
</svg>
|
|
189
|
+
<html.span
|
|
190
|
+
style={[styles.chevron, isNative && styles.nativeFlex, isExpanded && styles.chevronOpen]}
|
|
191
|
+
>
|
|
192
|
+
<Chevron color={hasActiveChild ? 'text' : 'textMuted'} />
|
|
187
193
|
</html.span>
|
|
188
|
-
{
|
|
194
|
+
{/* Native refuses bare text beside the chevron's view; the web keeps it bare. */}
|
|
195
|
+
{isNative ? <html.span>{label}</html.span> : label}
|
|
189
196
|
</html.button>
|
|
190
|
-
{
|
|
191
|
-
|
|
197
|
+
{/* Kept mounted while collapsed, so its Items stay registered with the group. */}
|
|
198
|
+
<GroupItemsContext.Provider value={registerValue}>
|
|
199
|
+
<html.div
|
|
200
|
+
id={panelId}
|
|
201
|
+
role="group"
|
|
202
|
+
aria-labelledby={triggerId}
|
|
203
|
+
hidden={isExpanded ? undefined : true}
|
|
204
|
+
style={[styles.items, !isExpanded && styles.itemsHidden]}
|
|
205
|
+
>
|
|
192
206
|
{children}
|
|
193
207
|
</html.div>
|
|
194
|
-
|
|
208
|
+
</GroupItemsContext.Provider>
|
|
195
209
|
</html.div>
|
|
196
210
|
)
|
|
197
211
|
}
|
|
@@ -239,11 +253,13 @@ interface ItemProps {
|
|
|
239
253
|
|
|
240
254
|
function Item({value, children, icon}: ItemProps) {
|
|
241
255
|
const {activeValue, onSelect, registerItem} = useSideNav()
|
|
256
|
+
const registerInGroup = useContext(GroupItemsContext)
|
|
242
257
|
const isActive = activeValue === value
|
|
243
258
|
|
|
244
259
|
useEffect(() => {
|
|
245
260
|
return registerItem(value)
|
|
246
261
|
}, [value, registerItem])
|
|
262
|
+
useEffect(() => registerInGroup?.(value), [value, registerInGroup])
|
|
247
263
|
|
|
248
264
|
return (
|
|
249
265
|
<html.button
|
|
@@ -255,7 +271,7 @@ function Item({value, children, icon}: ItemProps) {
|
|
|
255
271
|
>
|
|
256
272
|
<html.span aria-hidden style={[styles.marker, isActive && styles.markerActive]} />
|
|
257
273
|
{icon ? (
|
|
258
|
-
<html.span aria-hidden style={styles.itemIcon}>
|
|
274
|
+
<html.span aria-hidden style={[styles.itemIcon, isNative && styles.nativeFlex]}>
|
|
259
275
|
{icon}
|
|
260
276
|
</html.span>
|
|
261
277
|
) : null}
|
|
@@ -137,6 +137,14 @@ export const styles = css.create({
|
|
|
137
137
|
transitionDuration: duration.fast,
|
|
138
138
|
transitionTimingFunction: easing.standard,
|
|
139
139
|
},
|
|
140
|
+
// A collapsed group's items stay mounted (they tell the group what it holds), unseen.
|
|
141
|
+
itemsHidden: {
|
|
142
|
+
display: 'none',
|
|
143
|
+
},
|
|
144
|
+
// RSD on native lays out only display: flex, not the web's inline-flex.
|
|
145
|
+
nativeFlex: {
|
|
146
|
+
display: 'flex',
|
|
147
|
+
},
|
|
140
148
|
markerActive: {
|
|
141
149
|
height: sizes.navMarkerH,
|
|
142
150
|
},
|
|
@@ -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
|
}
|