@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,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]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
|
|
3
|
+
export interface SwitchProps {
|
|
4
|
+
/** Controlled checked state. */
|
|
5
|
+
checked?: boolean
|
|
6
|
+
/** Initial checked state (uncontrolled). */
|
|
7
|
+
defaultChecked?: boolean
|
|
8
|
+
/** Callback fired when the checked state changes. */
|
|
9
|
+
onCheckedChange?: (checked: boolean) => void
|
|
10
|
+
/** Prevents user interaction. */
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
/** Form field name for the hidden input. */
|
|
13
|
+
name?: string
|
|
14
|
+
/** Value submitted when checked. */
|
|
15
|
+
value?: string
|
|
16
|
+
/** Label content rendered beside the switch. */
|
|
17
|
+
children?: ReactNode
|
|
18
|
+
}
|
|
@@ -94,6 +94,8 @@ export const styles = css.create({
|
|
|
94
94
|
justifyContent: 'space-between',
|
|
95
95
|
gap: spacing.sm,
|
|
96
96
|
width: '100%',
|
|
97
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
98
|
+
boxSizing: 'border-box',
|
|
97
99
|
minHeight: sizes.touchTarget,
|
|
98
100
|
paddingTop: spacing.sm,
|
|
99
101
|
paddingBottom: spacing.sm,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {View} from 'react-native'
|
|
3
|
+
import {useReportTabLayout} from './indicator'
|
|
4
|
+
|
|
5
|
+
/** Native: reports the tab's box to its List, which places the sliding indicator. */
|
|
6
|
+
export function TabLayout({value, children}: {value: string; children: ReactNode}) {
|
|
7
|
+
const report = useReportTabLayout()
|
|
8
|
+
return <View onLayout={(e) => report?.(value, e.nativeEvent.layout)}>{children}</View>
|
|
9
|
+
}
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Tabbed interface with keyboard navigation. Supports horizontal and vertical orientation. Compound component — Root is required.',
|
|
5
|
+
'Tabbed interface with keyboard navigation. Supports horizontal and vertical orientation. Compound component — Root is required. On React Native the indicator follows each tab layout; there is no arrow-key navigation there, each tab is its own screen-reader stop.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Switching between content panels in the same view',
|
|
8
8
|
'Settings pages with multiple sections',
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {type ReactNode, useRef, useId, useEffect
|
|
1
|
+
import {type ReactNode, useRef, useId, useEffect} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {styles} from './styles.css'
|
|
4
|
+
import {isNative} from '../../platform'
|
|
4
5
|
import {TabsContext, useTabs} from './TabsContext'
|
|
5
6
|
import {useTabsRoot} from './useTabsRoot'
|
|
7
|
+
import {useTabIndicator} from './useTabIndicator'
|
|
8
|
+
import {useTabKeys} from './useTabKeys'
|
|
9
|
+
import {TabLayoutContext} from './indicator'
|
|
10
|
+
import {TabLayout} from './TabLayout'
|
|
6
11
|
|
|
7
12
|
// --- Root ---
|
|
8
13
|
|
|
@@ -41,120 +46,14 @@ interface ListProps {
|
|
|
41
46
|
function List({children}: ListProps) {
|
|
42
47
|
const {orientation, activeValue, onSelect, tabsRef, orderRef, setIndicatorActive} = useTabs()
|
|
43
48
|
const listRef = useRef<HTMLDivElement>(null)
|
|
44
|
-
const [indicator, setIndicator] = useState<{offset: number; size: number} | null>(null)
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
useLayoutEffect(() => {
|
|
54
|
-
const list = listRef.current
|
|
55
|
-
if (!list || !activeValue) {
|
|
56
|
-
setIndicator(null)
|
|
57
|
-
setIndicatorActive(false)
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
const el = list.querySelector<HTMLElement>(`[data-tab-value="${CSS.escape(activeValue)}"]`)
|
|
61
|
-
if (!el) {
|
|
62
|
-
setIndicator(null)
|
|
63
|
-
setIndicatorActive(false)
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
const measure = () => {
|
|
67
|
-
setIndicator(
|
|
68
|
-
orientation === 'horizontal'
|
|
69
|
-
? {offset: el.offsetLeft, size: el.offsetWidth}
|
|
70
|
-
: {offset: el.offsetTop, size: el.offsetHeight},
|
|
71
|
-
)
|
|
72
|
-
setIndicatorActive(true)
|
|
73
|
-
}
|
|
74
|
-
measure()
|
|
75
|
-
const ro = new ResizeObserver(measure)
|
|
76
|
-
ro.observe(el)
|
|
77
|
-
ro.observe(list)
|
|
78
|
-
return () => ro.disconnect()
|
|
79
|
-
}, [activeValue, orientation, setIndicatorActive])
|
|
80
|
-
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
const el = listRef.current
|
|
83
|
-
if (!el) return
|
|
84
|
-
const listEl = el
|
|
85
|
-
|
|
86
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
87
|
-
const order = orderRef.current
|
|
88
|
-
const tabs = tabsRef.current
|
|
89
|
-
if (order.length === 0) return
|
|
90
|
-
|
|
91
|
-
const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'
|
|
92
|
-
const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
|
|
93
|
-
|
|
94
|
-
let targetValue: string | null = null
|
|
95
|
-
|
|
96
|
-
switch (e.key) {
|
|
97
|
-
case nextKey: {
|
|
98
|
-
e.preventDefault()
|
|
99
|
-
const currentIdx = activeValue ? order.indexOf(activeValue) : -1
|
|
100
|
-
for (let i = 1; i <= order.length; i++) {
|
|
101
|
-
const idx = (currentIdx + i) % order.length
|
|
102
|
-
const val = order[idx]
|
|
103
|
-
if (!tabs.get(val)) {
|
|
104
|
-
targetValue = val
|
|
105
|
-
break
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
break
|
|
109
|
-
}
|
|
110
|
-
case prevKey: {
|
|
111
|
-
e.preventDefault()
|
|
112
|
-
const currentIdx = activeValue ? order.indexOf(activeValue) : 0
|
|
113
|
-
for (let i = 1; i <= order.length; i++) {
|
|
114
|
-
const idx = (currentIdx - i + order.length) % order.length
|
|
115
|
-
const val = order[idx]
|
|
116
|
-
if (!tabs.get(val)) {
|
|
117
|
-
targetValue = val
|
|
118
|
-
break
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
break
|
|
122
|
-
}
|
|
123
|
-
case 'Home': {
|
|
124
|
-
e.preventDefault()
|
|
125
|
-
for (const val of order) {
|
|
126
|
-
if (!tabs.get(val)) {
|
|
127
|
-
targetValue = val
|
|
128
|
-
break
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
break
|
|
132
|
-
}
|
|
133
|
-
case 'End': {
|
|
134
|
-
e.preventDefault()
|
|
135
|
-
for (let i = order.length - 1; i >= 0; i--) {
|
|
136
|
-
if (!tabs.get(order[i])) {
|
|
137
|
-
targetValue = order[i]
|
|
138
|
-
break
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
break
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (targetValue) {
|
|
146
|
-
onSelect(targetValue)
|
|
147
|
-
// Focus the newly activated tab button
|
|
148
|
-
const tabEl = listEl.querySelector(
|
|
149
|
-
`[data-tab-value="${targetValue}"]`,
|
|
150
|
-
) as HTMLElement | null
|
|
151
|
-
tabEl?.focus()
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
el.addEventListener('keydown', handleKeyDown)
|
|
156
|
-
return () => el.removeEventListener('keydown', handleKeyDown)
|
|
157
|
-
}, [orientation, activeValue, onSelect, tabsRef, orderRef])
|
|
50
|
+
const {indicator, reportTabLayout} = useTabIndicator({
|
|
51
|
+
listRef,
|
|
52
|
+
activeValue,
|
|
53
|
+
orientation,
|
|
54
|
+
setIndicatorActive,
|
|
55
|
+
})
|
|
56
|
+
useTabKeys({listRef, orientation, activeValue, onSelect, tabsRef, orderRef})
|
|
158
57
|
|
|
159
58
|
return (
|
|
160
59
|
<html.div
|
|
@@ -163,7 +62,7 @@ function List({children}: ListProps) {
|
|
|
163
62
|
aria-orientation={orientation}
|
|
164
63
|
style={[styles.list, orientation === 'vertical' && styles.listVertical]}
|
|
165
64
|
>
|
|
166
|
-
{children}
|
|
65
|
+
<TabLayoutContext.Provider value={reportTabLayout}>{children}</TabLayoutContext.Provider>
|
|
167
66
|
{indicator && (
|
|
168
67
|
<html.div
|
|
169
68
|
aria-hidden
|
|
@@ -205,27 +104,30 @@ function Tab({value, disabled = false, children}: TabProps) {
|
|
|
205
104
|
}
|
|
206
105
|
|
|
207
106
|
return (
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
107
|
+
<TabLayout value={value}>
|
|
108
|
+
<html.button
|
|
109
|
+
type="button"
|
|
110
|
+
role="tab"
|
|
111
|
+
id={tabId}
|
|
112
|
+
aria-selected={isActive}
|
|
113
|
+
aria-controls={panelId}
|
|
114
|
+
aria-disabled={disabled || undefined}
|
|
115
|
+
data-tab-value={value}
|
|
116
|
+
tabIndex={isActive ? 0 : -1}
|
|
117
|
+
onClick={handleClick}
|
|
118
|
+
style={[
|
|
119
|
+
styles.tab,
|
|
120
|
+
isNative && styles.nativeFlex,
|
|
121
|
+
orientation === 'vertical' && styles.tabVertical,
|
|
122
|
+
isActive &&
|
|
123
|
+
(orientation === 'vertical' ? styles.tabActiveVertical : styles.tabActiveHorizontal),
|
|
124
|
+
isActive && indicatorActive && styles.tabIndicatorActive,
|
|
125
|
+
disabled && styles.tabDisabled,
|
|
126
|
+
]}
|
|
127
|
+
>
|
|
128
|
+
{children}
|
|
129
|
+
</html.button>
|
|
130
|
+
</TabLayout>
|
|
229
131
|
)
|
|
230
132
|
}
|
|
231
133
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {createContext, useContext} from 'react'
|
|
2
|
+
import type {Orientation} from '../../shared/types'
|
|
3
|
+
|
|
4
|
+
/** Where the sliding indicator sits along the list, in px. */
|
|
5
|
+
export interface IndicatorBox {
|
|
6
|
+
offset: number
|
|
7
|
+
size: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A tab's box inside its list, as native `onLayout` reports it. */
|
|
11
|
+
export interface TabBox {
|
|
12
|
+
x: number
|
|
13
|
+
y: number
|
|
14
|
+
width: number
|
|
15
|
+
height: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Native: each Tab reports its box here, so the List places the indicator. */
|
|
19
|
+
export const TabLayoutContext = createContext<((value: string, box: TabBox) => void) | null>(null)
|
|
20
|
+
|
|
21
|
+
export function useReportTabLayout() {
|
|
22
|
+
return useContext(TabLayoutContext)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TabIndicatorInput {
|
|
26
|
+
listRef: React.RefObject<HTMLDivElement | null>
|
|
27
|
+
activeValue: string | null
|
|
28
|
+
orientation: Orientation
|
|
29
|
+
setIndicatorActive: (active: boolean) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TabIndicator {
|
|
33
|
+
indicator: IndicatorBox | null
|
|
34
|
+
/** Native only: what the Tabs report their layout to. */
|
|
35
|
+
reportTabLayout: ((value: string, box: TabBox) => void) | null
|
|
36
|
+
}
|
|
@@ -32,6 +32,13 @@ export const styles = css.create({
|
|
|
32
32
|
borderRightColor: colors.border,
|
|
33
33
|
gap: 0,
|
|
34
34
|
},
|
|
35
|
+
// RSD on native lays out only display: flex, not the web's inline-flex,
|
|
36
|
+
// and its Animated colour transition ends on a near-black colour: native
|
|
37
|
+
// tabs transition nothing that changes (colour and underline switch at once).
|
|
38
|
+
nativeFlex: {
|
|
39
|
+
display: 'flex',
|
|
40
|
+
transitionProperty: 'opacity',
|
|
41
|
+
},
|
|
35
42
|
tab: {
|
|
36
43
|
display: 'inline-flex',
|
|
37
44
|
alignItems: 'center',
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {useCallback, useEffect, useState} from 'react'
|
|
2
|
+
import type {TabBox, TabIndicator, TabIndicatorInput} from './indicator'
|
|
3
|
+
|
|
4
|
+
/** Native: the indicator follows the active tab's `onLayout` box. */
|
|
5
|
+
export function useTabIndicator({
|
|
6
|
+
activeValue,
|
|
7
|
+
orientation,
|
|
8
|
+
setIndicatorActive,
|
|
9
|
+
}: TabIndicatorInput): TabIndicator {
|
|
10
|
+
const [boxes, setBoxes] = useState<ReadonlyMap<string, TabBox>>(() => new Map())
|
|
11
|
+
const reportTabLayout = useCallback((value: string, box: TabBox) => {
|
|
12
|
+
setBoxes((prev) => {
|
|
13
|
+
const old = prev.get(value)
|
|
14
|
+
if (
|
|
15
|
+
old &&
|
|
16
|
+
old.x === box.x &&
|
|
17
|
+
old.y === box.y &&
|
|
18
|
+
old.width === box.width &&
|
|
19
|
+
old.height === box.height
|
|
20
|
+
) {
|
|
21
|
+
return prev
|
|
22
|
+
}
|
|
23
|
+
return new Map(prev).set(value, box)
|
|
24
|
+
})
|
|
25
|
+
}, [])
|
|
26
|
+
const box = activeValue ? boxes.get(activeValue) : undefined
|
|
27
|
+
const indicator = box
|
|
28
|
+
? orientation === 'horizontal'
|
|
29
|
+
? {offset: box.x, size: box.width}
|
|
30
|
+
: {offset: box.y, size: box.height}
|
|
31
|
+
: null
|
|
32
|
+
const active = indicator !== null
|
|
33
|
+
useEffect(() => setIndicatorActive(active), [active, setIndicatorActive])
|
|
34
|
+
return {indicator, reportTabLayout}
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {useLayoutEffect, useState} from 'react'
|
|
2
|
+
import type {IndicatorBox, TabIndicator, TabIndicatorInput} from './indicator'
|
|
3
|
+
|
|
4
|
+
/** Web: the active tab measured in the DOM, kept current by a ResizeObserver. */
|
|
5
|
+
export function useTabIndicator({
|
|
6
|
+
listRef,
|
|
7
|
+
activeValue,
|
|
8
|
+
orientation,
|
|
9
|
+
setIndicatorActive,
|
|
10
|
+
}: TabIndicatorInput): TabIndicator {
|
|
11
|
+
const [indicator, setIndicator] = useState<IndicatorBox | null>(null)
|
|
12
|
+
|
|
13
|
+
// Measure the active tab and drive the sliding indicator. useLayoutEffect so
|
|
14
|
+
// the first position is set before paint (the indicator element mounts already
|
|
15
|
+
// in place — no slide-in from zero; only later tab changes animate). Observes
|
|
16
|
+
// the active tab (its own size, e.g. a changing "(3)" count) and the list
|
|
17
|
+
// (container resize / reflow). A non-active sibling resizing while you're on
|
|
18
|
+
// another tab is the one case not caught; it self-corrects on the next
|
|
19
|
+
// selection or resize.
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
const list = listRef.current
|
|
22
|
+
if (!list || !activeValue) {
|
|
23
|
+
setIndicator(null)
|
|
24
|
+
setIndicatorActive(false)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
const el = list.querySelector<HTMLElement>(`[data-tab-value="${CSS.escape(activeValue)}"]`)
|
|
28
|
+
if (!el) {
|
|
29
|
+
setIndicator(null)
|
|
30
|
+
setIndicatorActive(false)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
const measure = () => {
|
|
34
|
+
setIndicator(
|
|
35
|
+
orientation === 'horizontal'
|
|
36
|
+
? {offset: el.offsetLeft, size: el.offsetWidth}
|
|
37
|
+
: {offset: el.offsetTop, size: el.offsetHeight},
|
|
38
|
+
)
|
|
39
|
+
setIndicatorActive(true)
|
|
40
|
+
}
|
|
41
|
+
measure()
|
|
42
|
+
const ro = new ResizeObserver(measure)
|
|
43
|
+
ro.observe(el)
|
|
44
|
+
ro.observe(list)
|
|
45
|
+
return () => ro.disconnect()
|
|
46
|
+
}, [listRef, activeValue, orientation, setIndicatorActive])
|
|
47
|
+
|
|
48
|
+
return {indicator, reportTabLayout: null}
|
|
49
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {useEffect} from 'react'
|
|
2
|
+
import type {Orientation} from '../../shared/types'
|
|
3
|
+
|
|
4
|
+
interface TabKeysInput {
|
|
5
|
+
listRef: React.RefObject<HTMLDivElement | null>
|
|
6
|
+
orientation: Orientation
|
|
7
|
+
activeValue: string | null
|
|
8
|
+
onSelect: (value: string) => void
|
|
9
|
+
tabsRef: React.RefObject<Map<string, boolean>>
|
|
10
|
+
orderRef: React.RefObject<string[]>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Web: arrow keys, Home and End move the selection and focus along the list. */
|
|
14
|
+
export function useTabKeys({
|
|
15
|
+
listRef,
|
|
16
|
+
orientation,
|
|
17
|
+
activeValue,
|
|
18
|
+
onSelect,
|
|
19
|
+
tabsRef,
|
|
20
|
+
orderRef,
|
|
21
|
+
}: TabKeysInput) {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const el = listRef.current
|
|
24
|
+
if (!el) return
|
|
25
|
+
const listEl = el
|
|
26
|
+
|
|
27
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
28
|
+
const order = orderRef.current
|
|
29
|
+
const tabs = tabsRef.current
|
|
30
|
+
if (order.length === 0) return
|
|
31
|
+
|
|
32
|
+
const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'
|
|
33
|
+
const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'
|
|
34
|
+
|
|
35
|
+
let targetValue: string | null = null
|
|
36
|
+
|
|
37
|
+
switch (e.key) {
|
|
38
|
+
case nextKey: {
|
|
39
|
+
e.preventDefault()
|
|
40
|
+
const currentIdx = activeValue ? order.indexOf(activeValue) : -1
|
|
41
|
+
for (let i = 1; i <= order.length; i++) {
|
|
42
|
+
const idx = (currentIdx + i) % order.length
|
|
43
|
+
const val = order[idx]
|
|
44
|
+
if (!tabs.get(val)) {
|
|
45
|
+
targetValue = val
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
case prevKey: {
|
|
52
|
+
e.preventDefault()
|
|
53
|
+
const currentIdx = activeValue ? order.indexOf(activeValue) : 0
|
|
54
|
+
for (let i = 1; i <= order.length; i++) {
|
|
55
|
+
const idx = (currentIdx - i + order.length) % order.length
|
|
56
|
+
const val = order[idx]
|
|
57
|
+
if (!tabs.get(val)) {
|
|
58
|
+
targetValue = val
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
case 'Home': {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
for (const val of order) {
|
|
67
|
+
if (!tabs.get(val)) {
|
|
68
|
+
targetValue = val
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
case 'End': {
|
|
75
|
+
e.preventDefault()
|
|
76
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
77
|
+
if (!tabs.get(order[i])) {
|
|
78
|
+
targetValue = order[i]
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (targetValue) {
|
|
87
|
+
onSelect(targetValue)
|
|
88
|
+
// Focus the newly activated tab button
|
|
89
|
+
const tabEl = listEl.querySelector(
|
|
90
|
+
`[data-tab-value="${targetValue}"]`,
|
|
91
|
+
) as HTMLElement | null
|
|
92
|
+
tabEl?.focus()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
el.addEventListener('keydown', handleKeyDown)
|
|
97
|
+
return () => el.removeEventListener('keydown', handleKeyDown)
|
|
98
|
+
}, [listRef, orientation, activeValue, onSelect, tabsRef, orderRef])
|
|
99
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Svg, {Path} from 'react-native-svg'
|
|
2
|
+
import {useResolvedColors} from '../ThemeProvider/ThemeProvider'
|
|
3
|
+
import type {RemoveGlyphProps} from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The same cross as the web's, drawn with react-native-svg (the native icon
|
|
7
|
+
* renderer, plan N0 Decision 9) in the tag's resolved text colour: a raw
|
|
8
|
+
* host has no CSS `currentColor` to inherit.
|
|
9
|
+
*/
|
|
10
|
+
export function RemoveGlyph({size, color}: RemoveGlyphProps) {
|
|
11
|
+
const stroke = useResolvedColors()[color]
|
|
12
|
+
return (
|
|
13
|
+
<Svg width={size} height={size} viewBox="0 0 12 12" fill="none" aria-hidden>
|
|
14
|
+
<Path d="M3 3l6 6M9 3l-6 6" stroke={stroke} strokeWidth={1.5} strokeLinecap="round" />
|
|
15
|
+
</Svg>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type {RemoveGlyphProps} from './types'
|
|
2
|
+
|
|
3
|
+
/** The tag's remove cross: 1.5 strokes on a 12 box, drawn in the tag's text colour. */
|
|
4
|
+
export function RemoveGlyph({size}: RemoveGlyphProps) {
|
|
5
|
+
return (
|
|
6
|
+
<svg width={size} height={size} viewBox="0 0 12 12" fill="none">
|
|
7
|
+
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" />
|
|
8
|
+
</svg>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Interactive tag/chip with optional remove button. Works standalone or inside TagGroup for collection management. Visually similar to Badge but supports interaction.',
|
|
5
|
+
'Interactive tag/chip with optional remove button. Works standalone or inside TagGroup for collection management. Visually similar to Badge but supports interaction. On React Native a Tag stands alone (TagGroup is web only): the label and, when removable, a remove button named from a string label.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Removable items in a list (emails, categories)',
|
|
8
8
|
'Inside TagGroup for editable tag collections',
|