@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
|
@@ -14,83 +14,15 @@ import {html} from 'react-strict-dom'
|
|
|
14
14
|
import {Icon, type IconName} from '../Icon'
|
|
15
15
|
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
16
16
|
import {styles} from './styles.css'
|
|
17
|
+
import {
|
|
18
|
+
createToastStore,
|
|
19
|
+
type ToastContextValue,
|
|
20
|
+
type ToastEntry,
|
|
21
|
+
type ToastStore,
|
|
22
|
+
type ToastVariant,
|
|
23
|
+
} from './store'
|
|
17
24
|
|
|
18
|
-
export type
|
|
19
|
-
|
|
20
|
-
export interface ToastAction {
|
|
21
|
-
label: string
|
|
22
|
-
onClick: () => void
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ToastOptions {
|
|
26
|
-
/** Visual + semantic tone. Errors announce assertively; errors and
|
|
27
|
-
* warnings linger longer. */
|
|
28
|
-
variant?: ToastVariant
|
|
29
|
-
message: ReactNode
|
|
30
|
-
/** Optional single action, e.g. Undo. Runs, then dismisses the toast. */
|
|
31
|
-
action?: ToastAction
|
|
32
|
-
/** Auto-dismiss delay in ms. `0` (or negative) keeps it until dismissed.
|
|
33
|
-
* The countdown pauses while the toast is hovered or holds focus. */
|
|
34
|
-
duration?: number
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface ToastContextValue {
|
|
38
|
-
/** Show a toast; returns its id so it can be dismissed programmatically. */
|
|
39
|
-
toast: (opts: ToastOptions) => string
|
|
40
|
-
dismiss: (id: string) => void
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface ToastEntry {
|
|
44
|
-
id: string
|
|
45
|
-
variant: ToastVariant
|
|
46
|
-
message: ReactNode
|
|
47
|
-
action?: ToastAction
|
|
48
|
-
duration: number
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface ToastStore {
|
|
52
|
-
subscribe: (cb: () => void) => () => void
|
|
53
|
-
getSnapshot: () => readonly ToastEntry[]
|
|
54
|
-
add: (opts: ToastOptions) => string
|
|
55
|
-
remove: (id: string) => void
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const DEFAULT_DURATION: Record<ToastVariant, number> = {
|
|
59
|
-
success: 5000,
|
|
60
|
-
info: 5000,
|
|
61
|
-
error: 8000,
|
|
62
|
-
warning: 8000,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function createToastStore(): ToastStore {
|
|
66
|
-
let entries: ToastEntry[] = []
|
|
67
|
-
let seq = 0
|
|
68
|
-
const listeners = new Set<() => void>()
|
|
69
|
-
const notify = () => listeners.forEach((l) => l())
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
subscribe: (cb) => {
|
|
73
|
-
listeners.add(cb)
|
|
74
|
-
return () => listeners.delete(cb)
|
|
75
|
-
},
|
|
76
|
-
getSnapshot: () => entries,
|
|
77
|
-
add: (opts) => {
|
|
78
|
-
const variant = opts.variant ?? 'info'
|
|
79
|
-
const id = `toast-${++seq}`
|
|
80
|
-
const duration = opts.duration ?? DEFAULT_DURATION[variant]
|
|
81
|
-
entries = [...entries, {id, variant, message: opts.message, action: opts.action, duration}]
|
|
82
|
-
notify()
|
|
83
|
-
return id
|
|
84
|
-
},
|
|
85
|
-
remove: (id) => {
|
|
86
|
-
const next = entries.filter((e) => e.id !== id)
|
|
87
|
-
if (next.length !== entries.length) {
|
|
88
|
-
entries = next
|
|
89
|
-
notify()
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
}
|
|
93
|
-
}
|
|
25
|
+
export type {ToastAction, ToastContextValue, ToastOptions, ToastVariant} from './store'
|
|
94
26
|
|
|
95
27
|
const variantIcon: Record<ToastVariant, IconName> = {
|
|
96
28
|
success: 'check-circle-filled',
|
|
@@ -115,10 +47,12 @@ export function useToast(): ToastContextValue {
|
|
|
115
47
|
|
|
116
48
|
function ToastItem({
|
|
117
49
|
entry,
|
|
50
|
+
store,
|
|
118
51
|
onDismiss,
|
|
119
52
|
dismissLabel,
|
|
120
53
|
}: {
|
|
121
54
|
entry: ToastEntry
|
|
55
|
+
store: ToastStore
|
|
122
56
|
onDismiss: (id: string) => void
|
|
123
57
|
dismissLabel: string
|
|
124
58
|
}) {
|
|
@@ -126,19 +60,17 @@ function ToastItem({
|
|
|
126
60
|
const [hovered, setHovered] = useState(false)
|
|
127
61
|
const [focused, setFocused] = useState(false)
|
|
128
62
|
const paused = hovered || focused
|
|
129
|
-
//
|
|
130
|
-
// timer with what was left (WCAG 2.2.1: the reader controls the timing).
|
|
131
|
-
const remaining = useRef(duration)
|
|
132
|
-
|
|
63
|
+
// The store keeps what is left of the countdown; a pause keeps it, a resume
|
|
64
|
+
// restarts the timer with what was left (WCAG 2.2.1: the reader controls the timing).
|
|
133
65
|
useEffect(() => {
|
|
134
66
|
if (duration <= 0 || paused) return
|
|
135
67
|
const started = Date.now()
|
|
136
|
-
const timer = setTimeout(() => onDismiss(id), Math.max(remaining
|
|
68
|
+
const timer = setTimeout(() => onDismiss(id), Math.max(store.remaining(id), 0))
|
|
137
69
|
return () => {
|
|
138
70
|
clearTimeout(timer)
|
|
139
|
-
|
|
71
|
+
store.spend(id, Date.now() - started)
|
|
140
72
|
}
|
|
141
|
-
}, [id, duration, paused, onDismiss])
|
|
73
|
+
}, [id, duration, paused, onDismiss, store])
|
|
142
74
|
|
|
143
75
|
return (
|
|
144
76
|
<html.div
|
|
@@ -213,7 +145,13 @@ export function ToastProvider({dismissLabel = 'Dismiss', children}: ToastProvide
|
|
|
213
145
|
const region = (
|
|
214
146
|
<html.div role="region" aria-label="Notifications" style={styles.region}>
|
|
215
147
|
{entries.map((entry) => (
|
|
216
|
-
<ToastItem
|
|
148
|
+
<ToastItem
|
|
149
|
+
key={entry.id}
|
|
150
|
+
entry={entry}
|
|
151
|
+
store={store}
|
|
152
|
+
onDismiss={dismiss}
|
|
153
|
+
dismissLabel={dismissLabel}
|
|
154
|
+
/>
|
|
217
155
|
))}
|
|
218
156
|
</html.div>
|
|
219
157
|
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
7
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The native toast, the picked B0 direction (docs/mockups/native-toast/
|
|
11
|
+
* Tinted.dc.html): a card with a status-coloured edge, one row of message,
|
|
12
|
+
* action and close. Motion is Reanimated's.
|
|
13
|
+
*/
|
|
14
|
+
export const nativeStyles = css.create({
|
|
15
|
+
region: {
|
|
16
|
+
position: 'absolute',
|
|
17
|
+
left: 0,
|
|
18
|
+
right: 0,
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'column',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
gap: spacing.sm,
|
|
23
|
+
paddingLeft: spacing.md,
|
|
24
|
+
paddingRight: spacing.md,
|
|
25
|
+
},
|
|
26
|
+
// The bottom safe-area inset and the keyboard, plus the edge spacing.
|
|
27
|
+
regionBottom: (bottom: number) => ({bottom}),
|
|
28
|
+
toast: {
|
|
29
|
+
boxSizing: 'border-box',
|
|
30
|
+
width: '100%',
|
|
31
|
+
maxWidth: sizes.toastMaxW,
|
|
32
|
+
display: 'flex',
|
|
33
|
+
flexDirection: 'row',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
gap: spacing.sm,
|
|
36
|
+
paddingTop: spacing.sm,
|
|
37
|
+
paddingBottom: spacing.sm,
|
|
38
|
+
paddingRight: spacing.sm,
|
|
39
|
+
paddingLeft: spacing.md,
|
|
40
|
+
borderRadius: radii.md,
|
|
41
|
+
borderWidth: borders.hairline,
|
|
42
|
+
borderStyle: 'solid',
|
|
43
|
+
borderColor: colors.border,
|
|
44
|
+
borderLeftWidth: borders.accent,
|
|
45
|
+
backgroundColor: colors.bgCard,
|
|
46
|
+
boxShadow: shadows.md,
|
|
47
|
+
},
|
|
48
|
+
success: {borderLeftColor: colors.successBorder},
|
|
49
|
+
error: {borderLeftColor: colors.errorBorder},
|
|
50
|
+
info: {borderLeftColor: colors.infoBorder},
|
|
51
|
+
warning: {borderLeftColor: colors.warningBorder},
|
|
52
|
+
message: {
|
|
53
|
+
flexGrow: 1,
|
|
54
|
+
flexShrink: 1,
|
|
55
|
+
color: colors.text,
|
|
56
|
+
fontSize: typography.fontSizeSm,
|
|
57
|
+
lineHeight: typography.lineHeight,
|
|
58
|
+
},
|
|
59
|
+
action: {
|
|
60
|
+
display: 'flex',
|
|
61
|
+
padding: spacing.sm,
|
|
62
|
+
borderWidth: 0,
|
|
63
|
+
backgroundColor: 'transparent',
|
|
64
|
+
color: colors.accent,
|
|
65
|
+
fontSize: typography.fontSizeSm,
|
|
66
|
+
fontWeight: typography.fontWeightSemibold,
|
|
67
|
+
},
|
|
68
|
+
close: {
|
|
69
|
+
display: 'flex',
|
|
70
|
+
alignItems: 'center',
|
|
71
|
+
justifyContent: 'center',
|
|
72
|
+
width: sizes.touchTarget,
|
|
73
|
+
height: sizes.touchTarget,
|
|
74
|
+
borderRadius: radii.sm,
|
|
75
|
+
borderWidth: 0,
|
|
76
|
+
backgroundColor: 'transparent',
|
|
77
|
+
},
|
|
78
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
|
|
3
|
+
/** The toast queue both platforms' ToastProvider render (plan N0, Slice B). */
|
|
4
|
+
|
|
5
|
+
export type ToastVariant = 'success' | 'error' | 'info' | 'warning'
|
|
6
|
+
|
|
7
|
+
export interface ToastAction {
|
|
8
|
+
label: string
|
|
9
|
+
onClick: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ToastOptions {
|
|
13
|
+
/** Visual + semantic tone. Errors announce assertively; errors and
|
|
14
|
+
* warnings linger longer. */
|
|
15
|
+
variant?: ToastVariant
|
|
16
|
+
message: ReactNode
|
|
17
|
+
/** Optional single action, e.g. Undo. Runs, then dismisses the toast. */
|
|
18
|
+
action?: ToastAction
|
|
19
|
+
/** Auto-dismiss delay in ms. `0` (or negative) keeps it until dismissed.
|
|
20
|
+
* The countdown pauses while the toast is hovered or holds focus. */
|
|
21
|
+
duration?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ToastContextValue {
|
|
25
|
+
/** Show a toast; returns its id so it can be dismissed programmatically. */
|
|
26
|
+
toast: (opts: ToastOptions) => string
|
|
27
|
+
dismiss: (id: string) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ToastEntry {
|
|
31
|
+
id: string
|
|
32
|
+
variant: ToastVariant
|
|
33
|
+
message: ReactNode
|
|
34
|
+
action?: ToastAction
|
|
35
|
+
duration: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ToastStore {
|
|
39
|
+
subscribe: (cb: () => void) => () => void
|
|
40
|
+
getSnapshot: () => readonly ToastEntry[]
|
|
41
|
+
add: (opts: ToastOptions) => string
|
|
42
|
+
remove: (id: string) => void
|
|
43
|
+
/**
|
|
44
|
+
* What is left of a toast's countdown, in ms. Kept here, not in the toast's
|
|
45
|
+
* component: on native a toast re-renders into another host when a Dialog
|
|
46
|
+
* above it closes, and its countdown carries on.
|
|
47
|
+
*/
|
|
48
|
+
remaining: (id: string) => number
|
|
49
|
+
/** Count `ms` of shown time against a toast's countdown. */
|
|
50
|
+
spend: (id: string, ms: number) => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_DURATION: Record<ToastVariant, number> = {
|
|
54
|
+
success: 5000,
|
|
55
|
+
info: 5000,
|
|
56
|
+
error: 8000,
|
|
57
|
+
warning: 8000,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createToastStore(): ToastStore {
|
|
61
|
+
let entries: ToastEntry[] = []
|
|
62
|
+
let seq = 0
|
|
63
|
+
const left = new Map<string, number>()
|
|
64
|
+
const listeners = new Set<() => void>()
|
|
65
|
+
const notify = () => listeners.forEach((l) => l())
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
subscribe: (cb) => {
|
|
69
|
+
listeners.add(cb)
|
|
70
|
+
return () => listeners.delete(cb)
|
|
71
|
+
},
|
|
72
|
+
getSnapshot: () => entries,
|
|
73
|
+
add: (opts) => {
|
|
74
|
+
const variant = opts.variant ?? 'info'
|
|
75
|
+
const id = `toast-${++seq}`
|
|
76
|
+
const duration = opts.duration ?? DEFAULT_DURATION[variant]
|
|
77
|
+
entries = [...entries, {id, variant, message: opts.message, action: opts.action, duration}]
|
|
78
|
+
left.set(id, duration)
|
|
79
|
+
notify()
|
|
80
|
+
return id
|
|
81
|
+
},
|
|
82
|
+
remove: (id) => {
|
|
83
|
+
left.delete(id)
|
|
84
|
+
const next = entries.filter((e) => e.id !== id)
|
|
85
|
+
if (next.length !== entries.length) {
|
|
86
|
+
entries = next
|
|
87
|
+
notify()
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
remaining: (id) => left.get(id) ?? 0,
|
|
91
|
+
spend: (id, ms) => {
|
|
92
|
+
const current = left.get(id)
|
|
93
|
+
if (current !== undefined) left.set(id, current - ms)
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {type ReactNode, useCallback, useLayoutEffect, useRef} from 'react'
|
|
2
|
-
import {html} from 'react-strict-dom'
|
|
3
2
|
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
4
3
|
import {useToggleGroup} from '../ToggleGroup/ToggleGroupContext'
|
|
5
4
|
import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
|
|
6
5
|
import {mergeRefs} from '../../shared/mergeRefs'
|
|
6
|
+
import {ButtonHost} from '../../shared/ButtonHost'
|
|
7
7
|
import type {ControlSize} from '../../shared/types'
|
|
8
|
+
import {isNative} from '../../platform'
|
|
8
9
|
import {styles} from './styles.css'
|
|
9
10
|
|
|
10
11
|
export type ToggleSize = ControlSize
|
|
@@ -83,7 +84,7 @@ export function Toggle({
|
|
|
83
84
|
}, [disabled, isGrouped, value, group, pressed, setStandalonePressed])
|
|
84
85
|
|
|
85
86
|
return (
|
|
86
|
-
<
|
|
87
|
+
<ButtonHost
|
|
87
88
|
ref={mergeRefs(ref, grouped.ref)}
|
|
88
89
|
type="button"
|
|
89
90
|
tabIndex={
|
|
@@ -103,6 +104,7 @@ export function Toggle({
|
|
|
103
104
|
data-pressed={pressed ? '' : undefined}
|
|
104
105
|
style={[
|
|
105
106
|
styles.base,
|
|
107
|
+
isNative && styles.nativeFlex,
|
|
106
108
|
sizeMap[size],
|
|
107
109
|
pressed ? styles.pressed : styles.unpressed,
|
|
108
110
|
wrap && wrappedSizeMap[size],
|
|
@@ -114,6 +116,6 @@ export function Toggle({
|
|
|
114
116
|
]}
|
|
115
117
|
>
|
|
116
118
|
{children}
|
|
117
|
-
</
|
|
119
|
+
</ButtonHost>
|
|
118
120
|
)
|
|
119
121
|
}
|
|
@@ -27,13 +27,20 @@ export const styles = css.create({
|
|
|
27
27
|
minHeight: {default: null, '@media (pointer: coarse)': sizes.touchTarget},
|
|
28
28
|
},
|
|
29
29
|
sizeDefault: {
|
|
30
|
-
|
|
30
|
+
// Longhands: RSD-native rejects a multi-value shorthand.
|
|
31
|
+
paddingTop: spacing.sm,
|
|
32
|
+
paddingBottom: spacing.sm,
|
|
33
|
+
paddingLeft: spacing.md,
|
|
34
|
+
paddingRight: spacing.md,
|
|
31
35
|
fontSize: typography.fontSizeSm,
|
|
32
36
|
borderRadius: radii.sm,
|
|
33
37
|
gap: spacing.sm,
|
|
34
38
|
},
|
|
35
39
|
sizeSmall: {
|
|
36
|
-
|
|
40
|
+
paddingTop: spacing.xs,
|
|
41
|
+
paddingBottom: spacing.xs,
|
|
42
|
+
paddingLeft: spacing.sm,
|
|
43
|
+
paddingRight: spacing.sm,
|
|
37
44
|
fontSize: typography.fontSizeXs,
|
|
38
45
|
borderRadius: radii.sm,
|
|
39
46
|
gap: spacing.xs,
|
|
@@ -87,4 +94,9 @@ export const styles = css.create({
|
|
|
87
94
|
opacity: 0.5,
|
|
88
95
|
cursor: 'not-allowed',
|
|
89
96
|
},
|
|
97
|
+
// RSD-native only honors display:'flex' (not 'inline-flex'). Layered on
|
|
98
|
+
// native only.
|
|
99
|
+
nativeFlex: {
|
|
100
|
+
display: 'flex',
|
|
101
|
+
},
|
|
90
102
|
})
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Hover/focus tooltip that shows supplementary content. Compound component — Root is required (throws without it).',
|
|
5
|
+
'Hover/focus tooltip that shows supplementary content. Compound component — Root is required (throws without it). On touch (React Native) no bubble is drawn: a string content becomes the accessibility hint of the Duro trigger inside (Button, Toggle, Menu.Trigger, Popover.Trigger), read after its name. Keep tooltip text non-essential, and keep aria-label on icon-only triggers.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Providing extra context on hover for icon-only buttons',
|
|
8
8
|
'Explaining abbreviations or truncated text',
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {TooltipHintContext} from './TooltipHint'
|
|
3
|
+
|
|
4
|
+
type Placement = 'top' | 'bottom' | 'left' | 'right'
|
|
5
|
+
|
|
6
|
+
interface RootProps {
|
|
7
|
+
children: ReactNode
|
|
8
|
+
/** On native, a string becomes the trigger's accessibility hint; anything else is dropped. */
|
|
9
|
+
content: ReactNode
|
|
10
|
+
/** Web only. */
|
|
11
|
+
placement?: Placement
|
|
12
|
+
/** Web only. */
|
|
13
|
+
delay?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Native Tooltip (plan N0, Decision 5): touch has no hover, so no bubble is
|
|
18
|
+
* drawn. A string `content` becomes the accessibility hint of the Duro
|
|
19
|
+
* trigger inside (Button, Toggle, Menu.Trigger, Popover.Trigger), read after
|
|
20
|
+
* its name. Keep tooltip text non-essential: sighted touch users never see
|
|
21
|
+
* it, and an icon-only trigger still needs its `aria-label`.
|
|
22
|
+
*/
|
|
23
|
+
function Root({children, content}: RootProps) {
|
|
24
|
+
const hint = typeof content === 'string' ? content : undefined
|
|
25
|
+
return <TooltipHintContext.Provider value={hint}>{children}</TooltipHintContext.Provider>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Renders its child in place; the hint reaches it through context. */
|
|
29
|
+
function Trigger({children}: {children: ReactNode}) {
|
|
30
|
+
return children
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Tooltip = {
|
|
34
|
+
Root,
|
|
35
|
+
Trigger,
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {createContext, useContext} from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native: a Tooltip's string content, read by the Duro trigger inside it
|
|
5
|
+
* (Button, Toggle, Menu.Trigger, Popover.Trigger) and set as that trigger's
|
|
6
|
+
* own accessibility hint (plan N0, Decision 5). Touch has no hover, so the
|
|
7
|
+
* text is never drawn; screen readers read it after the trigger's name.
|
|
8
|
+
*/
|
|
9
|
+
export const TooltipHintContext = createContext<string | undefined>(undefined)
|
|
10
|
+
|
|
11
|
+
export function useTooltipHint(): string | undefined {
|
|
12
|
+
return useContext(TooltipHintContext)
|
|
13
|
+
}
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Hierarchy of items with single selection and expandable branches (the WAI-ARIA tree pattern): one tab stop, arrow keys, Home/End, typeahead. An Item with href renders its row as a real link (cmd-click opens a tab) outside the tab order; Enter/Space follows it, through onNavigate(href, event) when given. Compound component — Root is required.',
|
|
5
|
+
'Hierarchy of items with single selection and expandable branches (the WAI-ARIA tree pattern): one tab stop, arrow keys, Home/End, typeahead. An Item with href renders its row as a real link (cmd-click opens a tab) outside the tab order; Enter/Space follows it, through onNavigate(href, event) when given. Compound component — Root is required. On React Native selection, expansion and aria-level are the same; the keyboard model (arrows, Home/End, typeahead) is web only.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Browsing data that nests to any depth — a file tree, zones › services › capabilities, namespace › resource',
|
|
8
8
|
'Picking one node of a hierarchy to show its details beside the tree',
|
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Children,
|
|
3
|
-
type ReactNode,
|
|
4
|
-
useCallback,
|
|
5
|
-
useContext,
|
|
6
|
-
useEffect,
|
|
7
|
-
useId,
|
|
8
|
-
useLayoutEffect,
|
|
9
|
-
useRef,
|
|
10
|
-
useState,
|
|
11
|
-
} from 'react'
|
|
1
|
+
import {Children, type ReactNode, useCallback, useContext, useId, useRef, useState} from 'react'
|
|
12
2
|
import {html} from 'react-strict-dom'
|
|
3
|
+
import {SPACING_PX} from '@duro-app/tokens/keys'
|
|
13
4
|
import {styles} from './styles.css'
|
|
5
|
+
import {isNative} from '../../platform'
|
|
14
6
|
import {TreeContext, TreeLevelContext, useTree} from './TreeContext'
|
|
7
|
+
import {useTreeKeyboard} from './useTreeKeyboard'
|
|
15
8
|
import {isPlainClick, type LinkClickEvent, type OnNavigate} from '../../shared/navigate'
|
|
16
9
|
|
|
17
10
|
/* Tree — a hierarchy of items (the WAI-ARIA tree pattern): single selection,
|
|
@@ -43,8 +36,6 @@ interface RootProps {
|
|
|
43
36
|
onExpandedChange?: (expanded: string[]) => void
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
const TYPEAHEAD_RESET_MS = 500
|
|
47
|
-
|
|
48
39
|
function Root({
|
|
49
40
|
children,
|
|
50
41
|
'aria-label': ariaLabel,
|
|
@@ -86,126 +77,16 @@ function Root({
|
|
|
86
77
|
const isExpanded = useCallback((v: string) => open.includes(v), [open])
|
|
87
78
|
const toggle = useCallback((v: string) => setExpanded(v, !open.includes(v)), [open, setExpanded])
|
|
88
79
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}, [focusValue, open, children])
|
|
100
|
-
|
|
101
|
-
// Keyboard: DOM order over the rendered items IS the visible order (a
|
|
102
|
-
// closed branch renders no children), so navigation reads it directly.
|
|
103
|
-
const typeahead = useRef({text: '', at: 0})
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
const root = rootRef.current
|
|
106
|
-
if (!root) return
|
|
107
|
-
const rootEl = root
|
|
108
|
-
const items = () => Array.from(rootEl.querySelectorAll<HTMLElement>('[role="treeitem"]'))
|
|
109
|
-
const focusItem = (el: HTMLElement | null | undefined) => {
|
|
110
|
-
if (!el) return
|
|
111
|
-
el.focus()
|
|
112
|
-
}
|
|
113
|
-
function onKeyDown(e: KeyboardEvent) {
|
|
114
|
-
const current = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
|
|
115
|
-
if (!current || !rootEl.contains(current)) return
|
|
116
|
-
const list = items()
|
|
117
|
-
const i = list.indexOf(current)
|
|
118
|
-
const val = current.dataset.treeValue ?? ''
|
|
119
|
-
const isOpen = current.getAttribute('aria-expanded')
|
|
120
|
-
switch (e.key) {
|
|
121
|
-
case 'ArrowDown':
|
|
122
|
-
e.preventDefault()
|
|
123
|
-
focusItem(list[i + 1])
|
|
124
|
-
return
|
|
125
|
-
case 'ArrowUp':
|
|
126
|
-
e.preventDefault()
|
|
127
|
-
focusItem(list[i - 1])
|
|
128
|
-
return
|
|
129
|
-
case 'ArrowRight':
|
|
130
|
-
e.preventDefault()
|
|
131
|
-
if (isOpen === 'false') setExpanded(val, true)
|
|
132
|
-
else if (isOpen === 'true') focusItem(list[i + 1])
|
|
133
|
-
return
|
|
134
|
-
case 'ArrowLeft': {
|
|
135
|
-
e.preventDefault()
|
|
136
|
-
if (isOpen === 'true') {
|
|
137
|
-
setExpanded(val, false)
|
|
138
|
-
return
|
|
139
|
-
}
|
|
140
|
-
const parent = current.parentElement?.closest<HTMLElement>('[role="treeitem"]')
|
|
141
|
-
if (parent && rootEl.contains(parent)) focusItem(parent)
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
case 'Home':
|
|
145
|
-
e.preventDefault()
|
|
146
|
-
focusItem(list[0])
|
|
147
|
-
return
|
|
148
|
-
case 'End':
|
|
149
|
-
e.preventDefault()
|
|
150
|
-
focusItem(list[list.length - 1])
|
|
151
|
-
return
|
|
152
|
-
case 'Enter':
|
|
153
|
-
case ' ': {
|
|
154
|
-
e.preventDefault()
|
|
155
|
-
// an item with href: follow its link (the click selects it and
|
|
156
|
-
// runs onNavigate, or the browser navigates)
|
|
157
|
-
const link = current.querySelector<HTMLElement>(':scope > [data-tree-link]')
|
|
158
|
-
if (link) link.click()
|
|
159
|
-
else onSelect(val)
|
|
160
|
-
return
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// typeahead: a printable character, no modifier
|
|
164
|
-
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
165
|
-
const now = Date.now()
|
|
166
|
-
const t = typeahead.current
|
|
167
|
-
t.text = now - t.at > TYPEAHEAD_RESET_MS ? e.key : t.text + e.key
|
|
168
|
-
t.at = now
|
|
169
|
-
// the same letter typed again cycles through the items it starts;
|
|
170
|
-
// a real prefix ("fi") narrows, and may match the current item
|
|
171
|
-
const typed = t.text.toLocaleLowerCase()
|
|
172
|
-
const repeat = [...typed].every((c) => c === typed[0])
|
|
173
|
-
const needle = repeat ? typed[0] : typed
|
|
174
|
-
const start = repeat ? i + 1 : i
|
|
175
|
-
for (let k = 0; k < list.length; k++) {
|
|
176
|
-
const el = list[(start + k) % list.length]
|
|
177
|
-
if ((el.dataset.treeText ?? '').toLocaleLowerCase().startsWith(needle)) {
|
|
178
|
-
focusItem(el)
|
|
179
|
-
break
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// the roving tab stop follows focus, however it moved (keys or pointer)
|
|
185
|
-
function onFocusIn(e: FocusEvent) {
|
|
186
|
-
const item = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
|
|
187
|
-
const v = item?.dataset.treeValue
|
|
188
|
-
if (v) setFocusValue(v)
|
|
189
|
-
setKeyboardFocus(item?.matches(':focus-visible') ?? false)
|
|
190
|
-
}
|
|
191
|
-
function onFocusOut(e: FocusEvent) {
|
|
192
|
-
if (!rootEl.contains(e.relatedTarget as Node | null)) setKeyboardFocus(false)
|
|
193
|
-
}
|
|
194
|
-
const onPointerDown = () => setKeyboardFocus(false)
|
|
195
|
-
const onKey = () => setKeyboardFocus(true)
|
|
196
|
-
root.addEventListener('keydown', onKeyDown)
|
|
197
|
-
root.addEventListener('keydown', onKey)
|
|
198
|
-
root.addEventListener('focusin', onFocusIn)
|
|
199
|
-
root.addEventListener('focusout', onFocusOut)
|
|
200
|
-
root.addEventListener('pointerdown', onPointerDown)
|
|
201
|
-
return () => {
|
|
202
|
-
root.removeEventListener('keydown', onKeyDown)
|
|
203
|
-
root.removeEventListener('keydown', onKey)
|
|
204
|
-
root.removeEventListener('focusin', onFocusIn)
|
|
205
|
-
root.removeEventListener('focusout', onFocusOut)
|
|
206
|
-
root.removeEventListener('pointerdown', onPointerDown)
|
|
207
|
-
}
|
|
208
|
-
}, [onSelect, setExpanded])
|
|
80
|
+
useTreeKeyboard({
|
|
81
|
+
rootRef,
|
|
82
|
+
focusValue,
|
|
83
|
+
setFocusValue,
|
|
84
|
+
setKeyboardFocus,
|
|
85
|
+
open,
|
|
86
|
+
children,
|
|
87
|
+
onSelect,
|
|
88
|
+
setExpanded,
|
|
89
|
+
})
|
|
209
90
|
|
|
210
91
|
return (
|
|
211
92
|
<TreeContext.Provider
|
|
@@ -272,7 +153,9 @@ function Item({value, label, textValue, children, meta, href, onNavigate}: ItemP
|
|
|
272
153
|
// and a name computed from content would read the whole subtree
|
|
273
154
|
const labelId = useId()
|
|
274
155
|
const rowStyles = [
|
|
275
|
-
|
|
156
|
+
isNative
|
|
157
|
+
? styles.indentPx(SPACING_PX.sm + Math.max(0, level - 1) * SPACING_PX.lg)
|
|
158
|
+
: styles.indent(level),
|
|
276
159
|
selected && styles.rowSelected,
|
|
277
160
|
keyboardFocus && focusValue === value && styles.rowFocused,
|
|
278
161
|
]
|
|
@@ -287,7 +170,12 @@ function Item({value, label, textValue, children, meta, href, onNavigate}: ItemP
|
|
|
287
170
|
e.preventDefault()
|
|
288
171
|
toggle(value)
|
|
289
172
|
}}
|
|
290
|
-
style={[
|
|
173
|
+
style={[
|
|
174
|
+
styles.chevron,
|
|
175
|
+
isNative && styles.nativeFlex,
|
|
176
|
+
open && styles.chevronOpen,
|
|
177
|
+
!hasChildren && styles.chevronLeaf,
|
|
178
|
+
]}
|
|
291
179
|
>
|
|
292
180
|
▸
|
|
293
181
|
</html.span>
|
|
@@ -56,6 +56,8 @@ export const styles = css.create({
|
|
|
56
56
|
outlineWidth: 0,
|
|
57
57
|
outlineStyle: 'none',
|
|
58
58
|
},
|
|
59
|
+
// Native has no calc(): the same steps, in px from the spacing scale.
|
|
60
|
+
indentPx: (px: number) => ({paddingLeft: px}),
|
|
59
61
|
// one indent step per level below the top
|
|
60
62
|
indent: (level: number) => ({
|
|
61
63
|
paddingLeft: `calc(${spacing.sm} + ${Math.max(0, level - 1)} * ${spacing.lg})`,
|
|
@@ -71,6 +73,10 @@ export const styles = css.create({
|
|
|
71
73
|
fontWeight: typography.fontWeightMedium,
|
|
72
74
|
backgroundColor: colors.bgCardHover,
|
|
73
75
|
},
|
|
76
|
+
// RSD on native lays out only display: flex, not the web's inline-flex.
|
|
77
|
+
nativeFlex: {
|
|
78
|
+
display: 'flex',
|
|
79
|
+
},
|
|
74
80
|
chevron: {
|
|
75
81
|
display: 'inline-flex',
|
|
76
82
|
alignItems: 'center',
|