@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,152 @@
|
|
|
1
|
+
import {type ReactNode, useEffect, useLayoutEffect, useRef} from 'react'
|
|
2
|
+
|
|
3
|
+
/** Typed characters within this many ms build one typeahead prefix. */
|
|
4
|
+
const TYPEAHEAD_RESET_MS = 500
|
|
5
|
+
|
|
6
|
+
interface TreeKeyboardInput {
|
|
7
|
+
rootRef: React.RefObject<HTMLUListElement | null>
|
|
8
|
+
focusValue: string | null
|
|
9
|
+
setFocusValue: (value: string | null) => void
|
|
10
|
+
setKeyboardFocus: (on: boolean) => void
|
|
11
|
+
open: ReadonlyArray<string>
|
|
12
|
+
children: ReactNode
|
|
13
|
+
onSelect: (value: string) => void
|
|
14
|
+
setExpanded: (value: string, open: boolean) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Web: the tree's roving tab stop, arrow keys, Home/End, Enter and typeahead
|
|
19
|
+
* (WAI-ARIA tree pattern), read from the rendered DOM. Native has none of
|
|
20
|
+
* them (plan N0, Decision 6): useTreeKeyboard.native.ts.
|
|
21
|
+
*/
|
|
22
|
+
export function useTreeKeyboard({
|
|
23
|
+
rootRef,
|
|
24
|
+
focusValue,
|
|
25
|
+
setFocusValue,
|
|
26
|
+
setKeyboardFocus,
|
|
27
|
+
open,
|
|
28
|
+
children,
|
|
29
|
+
onSelect,
|
|
30
|
+
setExpanded,
|
|
31
|
+
}: TreeKeyboardInput) {
|
|
32
|
+
// Exactly one item is tabbable: the focus holder, else the selection, else
|
|
33
|
+
// the first item — re-checked when an item appears, closes or goes away.
|
|
34
|
+
useLayoutEffect(() => {
|
|
35
|
+
const root = rootRef.current
|
|
36
|
+
if (!root) return
|
|
37
|
+
const tabbable = root.querySelector('[role="treeitem"][tabindex="0"]')
|
|
38
|
+
if (tabbable) return
|
|
39
|
+
const first = root.querySelector<HTMLElement>('[role="treeitem"]')
|
|
40
|
+
const next = first?.dataset.treeValue ?? null
|
|
41
|
+
if (next !== focusValue) setFocusValue(next)
|
|
42
|
+
}, [rootRef, focusValue, setFocusValue, open, children])
|
|
43
|
+
|
|
44
|
+
// Keyboard: DOM order over the rendered items IS the visible order (a
|
|
45
|
+
// closed branch renders no children), so navigation reads it directly.
|
|
46
|
+
const typeahead = useRef({text: '', at: 0})
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const root = rootRef.current
|
|
49
|
+
if (!root) return
|
|
50
|
+
const rootEl = root
|
|
51
|
+
const items = () => Array.from(rootEl.querySelectorAll<HTMLElement>('[role="treeitem"]'))
|
|
52
|
+
const focusItem = (el: HTMLElement | null | undefined) => {
|
|
53
|
+
if (!el) return
|
|
54
|
+
el.focus()
|
|
55
|
+
}
|
|
56
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
57
|
+
const current = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
|
|
58
|
+
if (!current || !rootEl.contains(current)) return
|
|
59
|
+
const list = items()
|
|
60
|
+
const i = list.indexOf(current)
|
|
61
|
+
const val = current.dataset.treeValue ?? ''
|
|
62
|
+
const isOpen = current.getAttribute('aria-expanded')
|
|
63
|
+
switch (e.key) {
|
|
64
|
+
case 'ArrowDown':
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
focusItem(list[i + 1])
|
|
67
|
+
return
|
|
68
|
+
case 'ArrowUp':
|
|
69
|
+
e.preventDefault()
|
|
70
|
+
focusItem(list[i - 1])
|
|
71
|
+
return
|
|
72
|
+
case 'ArrowRight':
|
|
73
|
+
e.preventDefault()
|
|
74
|
+
if (isOpen === 'false') setExpanded(val, true)
|
|
75
|
+
else if (isOpen === 'true') focusItem(list[i + 1])
|
|
76
|
+
return
|
|
77
|
+
case 'ArrowLeft': {
|
|
78
|
+
e.preventDefault()
|
|
79
|
+
if (isOpen === 'true') {
|
|
80
|
+
setExpanded(val, false)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
const parent = current.parentElement?.closest<HTMLElement>('[role="treeitem"]')
|
|
84
|
+
if (parent && rootEl.contains(parent)) focusItem(parent)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
case 'Home':
|
|
88
|
+
e.preventDefault()
|
|
89
|
+
focusItem(list[0])
|
|
90
|
+
return
|
|
91
|
+
case 'End':
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
focusItem(list[list.length - 1])
|
|
94
|
+
return
|
|
95
|
+
case 'Enter':
|
|
96
|
+
case ' ': {
|
|
97
|
+
e.preventDefault()
|
|
98
|
+
// an item with href: follow its link (the click selects it and
|
|
99
|
+
// runs onNavigate, or the browser navigates)
|
|
100
|
+
const link = current.querySelector<HTMLElement>(':scope > [data-tree-link]')
|
|
101
|
+
if (link) link.click()
|
|
102
|
+
else onSelect(val)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// typeahead: a printable character, no modifier
|
|
107
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
108
|
+
const now = Date.now()
|
|
109
|
+
const t = typeahead.current
|
|
110
|
+
t.text = now - t.at > TYPEAHEAD_RESET_MS ? e.key : t.text + e.key
|
|
111
|
+
t.at = now
|
|
112
|
+
// the same letter typed again cycles through the items it starts;
|
|
113
|
+
// a real prefix ("fi") narrows, and may match the current item
|
|
114
|
+
const typed = t.text.toLocaleLowerCase()
|
|
115
|
+
const repeat = [...typed].every((c) => c === typed[0])
|
|
116
|
+
const needle = repeat ? typed[0] : typed
|
|
117
|
+
const start = repeat ? i + 1 : i
|
|
118
|
+
for (let k = 0; k < list.length; k++) {
|
|
119
|
+
const el = list[(start + k) % list.length]
|
|
120
|
+
if ((el.dataset.treeText ?? '').toLocaleLowerCase().startsWith(needle)) {
|
|
121
|
+
focusItem(el)
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// the roving tab stop follows focus, however it moved (keys or pointer)
|
|
128
|
+
function onFocusIn(e: FocusEvent) {
|
|
129
|
+
const item = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
|
|
130
|
+
const v = item?.dataset.treeValue
|
|
131
|
+
if (v) setFocusValue(v)
|
|
132
|
+
setKeyboardFocus(item?.matches(':focus-visible') ?? false)
|
|
133
|
+
}
|
|
134
|
+
function onFocusOut(e: FocusEvent) {
|
|
135
|
+
if (!rootEl.contains(e.relatedTarget as Node | null)) setKeyboardFocus(false)
|
|
136
|
+
}
|
|
137
|
+
const onPointerDown = () => setKeyboardFocus(false)
|
|
138
|
+
const onKey = () => setKeyboardFocus(true)
|
|
139
|
+
root.addEventListener('keydown', onKeyDown)
|
|
140
|
+
root.addEventListener('keydown', onKey)
|
|
141
|
+
root.addEventListener('focusin', onFocusIn)
|
|
142
|
+
root.addEventListener('focusout', onFocusOut)
|
|
143
|
+
root.addEventListener('pointerdown', onPointerDown)
|
|
144
|
+
return () => {
|
|
145
|
+
root.removeEventListener('keydown', onKeyDown)
|
|
146
|
+
root.removeEventListener('keydown', onKey)
|
|
147
|
+
root.removeEventListener('focusin', onFocusIn)
|
|
148
|
+
root.removeEventListener('focusout', onFocusOut)
|
|
149
|
+
root.removeEventListener('pointerdown', onPointerDown)
|
|
150
|
+
}
|
|
151
|
+
}, [rootRef, onSelect, setExpanded, setFocusValue, setKeyboardFocus])
|
|
152
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'A windowed list on @tanstack/react-virtual: only the rows in view (plus overscan) are mounted, and rows are measured so they may differ in height. role="list" with listitem rows carrying aria-setsize and aria-posinset. anchor="end" opens at the last item and stays pinned to the bottom while new items arrive and the reader is at the bottom (a transcript, a log). It fills its parent, or stops at maxHeight. Web only: on native, host FlashList or Legend List behind your own .native.tsx leaf.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Hundreds to tens of thousands of rows: sessions, a chat transcript, log lines',
|
|
8
|
+
'A list that grows at the bottom while the reader follows it (anchor="end")',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Tabular data with columns and sorting — use VirtualTable',
|
|
12
|
+
'A few dozen rows — use List; windowing only adds cost there',
|
|
13
|
+
'Selectable options in a picker — use Listbox',
|
|
14
|
+
],
|
|
15
|
+
anatomy: {
|
|
16
|
+
required: [],
|
|
17
|
+
optional: [],
|
|
18
|
+
},
|
|
19
|
+
relatedTo: [
|
|
20
|
+
{
|
|
21
|
+
component: 'VirtualTable',
|
|
22
|
+
kind: 'contrast',
|
|
23
|
+
relationship: 'VirtualTable windows table rows with columns; VirtualList windows any row',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
component: 'List',
|
|
27
|
+
kind: 'composition',
|
|
28
|
+
relationship: 'Render a List.Item-like row in renderItem for the list look',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
example: `<VirtualList
|
|
32
|
+
aria-label="Transcript"
|
|
33
|
+
items={messages}
|
|
34
|
+
getKey={(m) => m.id}
|
|
35
|
+
estimateSize="controlLg"
|
|
36
|
+
anchor="end"
|
|
37
|
+
renderItem={(m) => <Text>{m.text}</Text>}
|
|
38
|
+
/>`,
|
|
39
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {expect, waitFor} from 'storybook/test'
|
|
3
|
+
import type {SizeToken} from '@duro-app/tokens/keys'
|
|
4
|
+
import {VirtualList} from './VirtualList'
|
|
5
|
+
import {Text} from '../Text/Text'
|
|
6
|
+
|
|
7
|
+
const meta: Meta = {
|
|
8
|
+
title: 'Components/VirtualList',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
type Story = StoryObj
|
|
13
|
+
|
|
14
|
+
const ITEMS = Array.from({length: 10_000}, (_, i) => ({id: i, text: `Message ${i + 1}`}))
|
|
15
|
+
|
|
16
|
+
function Rows({anchor}: {anchor?: 'start' | 'end'}) {
|
|
17
|
+
return (
|
|
18
|
+
<VirtualList
|
|
19
|
+
aria-label="Transcript"
|
|
20
|
+
items={ITEMS}
|
|
21
|
+
getKey={(m) => m.id}
|
|
22
|
+
estimateSize="controlLg"
|
|
23
|
+
anchor={anchor}
|
|
24
|
+
maxHeight="previewMaxH"
|
|
25
|
+
renderItem={(m) => <Text>{m.text}</Text>}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PlaygroundArgs {
|
|
31
|
+
count: number
|
|
32
|
+
anchor: 'start' | 'end'
|
|
33
|
+
overscan: number
|
|
34
|
+
estimateSize: SizeToken
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Playground: StoryObj<PlaygroundArgs> = {
|
|
38
|
+
args: {count: 10_000, anchor: 'start', overscan: 5, estimateSize: 'controlLg'},
|
|
39
|
+
argTypes: {
|
|
40
|
+
count: {
|
|
41
|
+
control: {type: 'number', min: 0, max: 10_000, step: 500},
|
|
42
|
+
description: 'How many items',
|
|
43
|
+
},
|
|
44
|
+
anchor: {
|
|
45
|
+
control: 'inline-radio',
|
|
46
|
+
options: ['start', 'end'],
|
|
47
|
+
description: "'end' opens at the last item and follows new ones",
|
|
48
|
+
},
|
|
49
|
+
overscan: {
|
|
50
|
+
control: {type: 'number', min: 0, max: 50},
|
|
51
|
+
description: 'Rows rendered past each edge',
|
|
52
|
+
},
|
|
53
|
+
estimateSize: {
|
|
54
|
+
control: 'select',
|
|
55
|
+
options: ['controlSm', 'controlLg', 'touchTarget', 'iconXl'],
|
|
56
|
+
description: "Each row's starting height estimate (a size token)",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
render: ({count, anchor, overscan, estimateSize}) => (
|
|
60
|
+
<VirtualList
|
|
61
|
+
aria-label="Transcript"
|
|
62
|
+
items={ITEMS.slice(0, count)}
|
|
63
|
+
getKey={(m) => m.id}
|
|
64
|
+
estimateSize={estimateSize}
|
|
65
|
+
anchor={anchor}
|
|
66
|
+
overscan={overscan}
|
|
67
|
+
maxHeight="previewMaxH"
|
|
68
|
+
renderItem={(m) => <Text>{m.text}</Text>}
|
|
69
|
+
/>
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const TenThousand: Story = {
|
|
74
|
+
render: () => <Rows />,
|
|
75
|
+
play: async ({canvas}) => {
|
|
76
|
+
const list = canvas.getByRole('list', {name: 'Transcript'})
|
|
77
|
+
await waitFor(() => expect(canvas.getAllByRole('listitem').length).toBeGreaterThan(0))
|
|
78
|
+
const rows = canvas.getAllByRole('listitem')
|
|
79
|
+
await expect(rows.length).toBeLessThan(50)
|
|
80
|
+
await expect(rows[0]).toHaveAttribute('aria-setsize', '10000')
|
|
81
|
+
await expect(rows[0]).toHaveAttribute('aria-posinset', '1')
|
|
82
|
+
await expect(list.scrollHeight).toBeGreaterThan(list.clientHeight * 100)
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const AnchoredToTheEnd: Story = {
|
|
87
|
+
render: () => <Rows anchor="end" />,
|
|
88
|
+
play: async ({canvas}) => {
|
|
89
|
+
await waitFor(() => expect(canvas.getByText('Message 10000')).toBeInTheDocument())
|
|
90
|
+
const rows = canvas.getAllByRole('listitem')
|
|
91
|
+
await expect(rows.length).toBeLessThan(50)
|
|
92
|
+
const last = rows.find((r) => r.getAttribute('aria-posinset') === '10000')
|
|
93
|
+
await expect(last).toBeDefined()
|
|
94
|
+
},
|
|
95
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {type ReactNode, useEffect, useLayoutEffect, useRef} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {useVirtualizer} from '@tanstack/react-virtual'
|
|
4
|
+
import {SIZES_PX, type SizeToken} from '@duro-app/tokens/keys'
|
|
5
|
+
import {resolveLength, type Length} from '../../shared/length'
|
|
6
|
+
import {styles} from './styles.css'
|
|
7
|
+
|
|
8
|
+
export interface VirtualListProps<T> {
|
|
9
|
+
items: readonly T[]
|
|
10
|
+
/** Each row's starting height estimate, a size token; rows are then measured. */
|
|
11
|
+
estimateSize: SizeToken
|
|
12
|
+
renderItem: (item: T, index: number) => ReactNode
|
|
13
|
+
/** A stable key per item (an id), so rows keep their measurement as items change. */
|
|
14
|
+
getKey: (item: T, index: number) => string | number
|
|
15
|
+
/** Rows rendered beyond each edge of the view. Default 5 */
|
|
16
|
+
overscan?: number
|
|
17
|
+
/**
|
|
18
|
+
* `end` starts at the last item and stays pinned to the bottom while new
|
|
19
|
+
* items arrive, as long as the reader is at the bottom (a transcript, a log).
|
|
20
|
+
* Default `start`.
|
|
21
|
+
*/
|
|
22
|
+
anchor?: 'start' | 'end'
|
|
23
|
+
/** Cap the list's height; without it, it fills its parent. */
|
|
24
|
+
maxHeight?: Length
|
|
25
|
+
/** The list's accessible name. */
|
|
26
|
+
'aria-label': string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Within this many px of the end counts as "at the bottom" for `anchor="end"`. */
|
|
30
|
+
const BOTTOM_SLOP = SIZES_PX.touchTarget
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A windowed list: only the rows in view (plus `overscan`) are mounted, so a
|
|
34
|
+
* list of tens of thousands stays fast. Rows are measured as they render, so
|
|
35
|
+
* they may differ in height. It is a `role="list"`; each row is a listitem
|
|
36
|
+
* with `aria-setsize` and `aria-posinset`, so assistive tech knows the full
|
|
37
|
+
* length while most rows are not in the DOM. Web only: on native, host
|
|
38
|
+
* FlashList or Legend List behind your own `.native.tsx` leaf.
|
|
39
|
+
*/
|
|
40
|
+
export function VirtualList<T>({
|
|
41
|
+
items,
|
|
42
|
+
estimateSize,
|
|
43
|
+
renderItem,
|
|
44
|
+
getKey,
|
|
45
|
+
overscan = 5,
|
|
46
|
+
anchor = 'start',
|
|
47
|
+
maxHeight,
|
|
48
|
+
'aria-label': ariaLabel,
|
|
49
|
+
}: VirtualListProps<T>) {
|
|
50
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
51
|
+
const virtualizer = useVirtualizer({
|
|
52
|
+
count: items.length,
|
|
53
|
+
getScrollElement: () => scrollRef.current,
|
|
54
|
+
estimateSize: () => SIZES_PX[estimateSize],
|
|
55
|
+
getItemKey: (index) => getKey(items[index], index),
|
|
56
|
+
overscan,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// `end`: open at the last item, and follow new items while at the bottom.
|
|
60
|
+
const atBottom = useRef(true)
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const el = scrollRef.current
|
|
63
|
+
if (!el || anchor !== 'end') return
|
|
64
|
+
const onScroll = () => {
|
|
65
|
+
atBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight <= BOTTOM_SLOP
|
|
66
|
+
}
|
|
67
|
+
el.addEventListener('scroll', onScroll, {passive: true})
|
|
68
|
+
return () => el.removeEventListener('scroll', onScroll)
|
|
69
|
+
}, [anchor])
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
if (anchor !== 'end' || items.length === 0 || !atBottom.current) return
|
|
72
|
+
virtualizer.scrollToIndex(items.length - 1, {align: 'end'})
|
|
73
|
+
}, [anchor, items.length, virtualizer])
|
|
74
|
+
|
|
75
|
+
const rows = virtualizer.getVirtualItems()
|
|
76
|
+
return (
|
|
77
|
+
<html.div
|
|
78
|
+
ref={scrollRef}
|
|
79
|
+
role="list"
|
|
80
|
+
aria-label={ariaLabel}
|
|
81
|
+
style={[
|
|
82
|
+
styles.scroller,
|
|
83
|
+
maxHeight !== undefined && styles.scrollMax(resolveLength(maxHeight)),
|
|
84
|
+
]}
|
|
85
|
+
>
|
|
86
|
+
<html.div style={styles.total(virtualizer.getTotalSize())}>
|
|
87
|
+
{rows.map((row) => (
|
|
88
|
+
<html.div
|
|
89
|
+
key={row.key}
|
|
90
|
+
ref={virtualizer.measureElement}
|
|
91
|
+
data-index={row.index}
|
|
92
|
+
role="listitem"
|
|
93
|
+
aria-setsize={items.length}
|
|
94
|
+
aria-posinset={row.index + 1}
|
|
95
|
+
style={styles.row(row.start)}
|
|
96
|
+
>
|
|
97
|
+
{renderItem(items[row.index], row.index)}
|
|
98
|
+
</html.div>
|
|
99
|
+
))}
|
|
100
|
+
</html.div>
|
|
101
|
+
</html.div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
|
|
3
|
+
export const styles = css.create({
|
|
4
|
+
// The scroll container: it fills its parent (a pane, a flex column) by
|
|
5
|
+
// default, or stops at `maxHeight`.
|
|
6
|
+
scroller: {
|
|
7
|
+
position: 'relative',
|
|
8
|
+
width: '100%',
|
|
9
|
+
height: '100%',
|
|
10
|
+
minHeight: 0,
|
|
11
|
+
overflowY: 'auto',
|
|
12
|
+
overscrollBehavior: 'contain',
|
|
13
|
+
},
|
|
14
|
+
scrollMax: (maxHeight: number | string) => ({
|
|
15
|
+
height: 'auto',
|
|
16
|
+
maxHeight,
|
|
17
|
+
}),
|
|
18
|
+
// The full virtual height, so the scrollbar is the list's real length.
|
|
19
|
+
total: (height: number) => ({
|
|
20
|
+
position: 'relative',
|
|
21
|
+
width: '100%',
|
|
22
|
+
height,
|
|
23
|
+
}),
|
|
24
|
+
row: (y: number) => ({
|
|
25
|
+
position: 'absolute',
|
|
26
|
+
top: 0,
|
|
27
|
+
left: 0,
|
|
28
|
+
width: '100%',
|
|
29
|
+
transform: `translateY(${y}px)`,
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
@@ -9,10 +9,14 @@ export const styles = css.create({
|
|
|
9
9
|
wrap: {
|
|
10
10
|
position: 'relative',
|
|
11
11
|
width: '100%',
|
|
12
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
13
|
+
boxSizing: 'border-box',
|
|
12
14
|
},
|
|
13
15
|
scroll: {
|
|
14
16
|
position: 'relative',
|
|
15
17
|
width: '100%',
|
|
18
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
19
|
+
boxSizing: 'border-box',
|
|
16
20
|
borderWidth: borders.hairline,
|
|
17
21
|
borderStyle: 'solid',
|
|
18
22
|
borderColor: colors.border,
|
|
@@ -66,6 +70,8 @@ export const styles = css.create({
|
|
|
66
70
|
bodyTotal: (height: number) => ({
|
|
67
71
|
position: 'relative',
|
|
68
72
|
width: '100%',
|
|
73
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
74
|
+
boxSizing: 'border-box',
|
|
69
75
|
height,
|
|
70
76
|
}),
|
|
71
77
|
row: {
|
|
@@ -73,6 +79,8 @@ export const styles = css.create({
|
|
|
73
79
|
flexDirection: 'row',
|
|
74
80
|
alignItems: 'center',
|
|
75
81
|
width: '100%',
|
|
82
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
83
|
+
boxSizing: 'border-box',
|
|
76
84
|
borderBottomWidth: borders.hairline,
|
|
77
85
|
borderBottomStyle: 'solid',
|
|
78
86
|
borderBottomColor: colors.border,
|
|
@@ -113,6 +121,8 @@ export const styles = css.create({
|
|
|
113
121
|
stackScroll: {
|
|
114
122
|
position: 'relative',
|
|
115
123
|
width: '100%',
|
|
124
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
125
|
+
boxSizing: 'border-box',
|
|
116
126
|
backgroundColor: 'transparent',
|
|
117
127
|
},
|
|
118
128
|
stackBody: {
|
|
@@ -125,6 +135,8 @@ export const styles = css.create({
|
|
|
125
135
|
flexDirection: 'column',
|
|
126
136
|
gap: spacing.xs,
|
|
127
137
|
width: '100%',
|
|
138
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
139
|
+
boxSizing: 'border-box',
|
|
128
140
|
padding: spacing.sm,
|
|
129
141
|
borderWidth: borders.hairline,
|
|
130
142
|
borderStyle: 'solid',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {useEffect, useState, type RefObject} from 'react'
|
|
2
|
+
import {useWindowDimensions} from 'react-native'
|
|
3
|
+
import {breakpointsPx, type Breakpoint} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
4
|
+
|
|
5
|
+
interface Measurable {
|
|
6
|
+
measure?: (cb: (x: number, y: number, width: number) => void) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Native: whether the view behind `ref` is narrower than a Duro breakpoint.
|
|
11
|
+
* There is no ResizeObserver; the view is measured after layout and again
|
|
12
|
+
* whenever the window changes size (a rotation, a split screen). `false`
|
|
13
|
+
* until the first measurement, as on the web.
|
|
14
|
+
*/
|
|
15
|
+
export function useContainerBelow(ref: RefObject<unknown>, breakpoint: Breakpoint): boolean {
|
|
16
|
+
const [below, setBelow] = useState(false)
|
|
17
|
+
const limit = breakpointsPx[breakpoint]
|
|
18
|
+
const {width: windowWidth} = useWindowDimensions()
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const frame = requestAnimationFrame(() => {
|
|
22
|
+
;(ref.current as Measurable | null)?.measure?.((_x, _y, width) => {
|
|
23
|
+
if (width > 0) setBelow(width < limit)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
return () => cancelAnimationFrame(frame)
|
|
27
|
+
}, [ref, limit, windowWidth])
|
|
28
|
+
|
|
29
|
+
return below
|
|
30
|
+
}
|
package/src/index.native.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* TagGroup, ThemeProvider's portal) that use web-only DOM APIs — and Combobox
|
|
8
8
|
* imports `react-dom`, so pulling the full barrel crashes on Metro. This barrel
|
|
9
9
|
* re-exports ONLY the components whose source is RN-safe (pure react-strict-dom
|
|
10
|
-
* `html` + `css
|
|
10
|
+
* `html` + `css`) or that have a `.native` variant; `pnpm typecheck:native` and
|
|
11
|
+
* `scripts/check-native-graph.mjs` hold the line (plan N0).
|
|
11
12
|
*
|
|
12
13
|
* Tokens resolve fine on native already: @duro-app/tokens's subpath exports
|
|
13
14
|
* (`./tokens/*.css`, `./themes/*.css`) point straight at their `.css.ts`
|
|
@@ -29,7 +30,63 @@ export {Textarea, type TextareaVariant} from './components/Textarea/Textarea'
|
|
|
29
30
|
export {Field} from './components/Field/Field'
|
|
30
31
|
export {InputGroup} from './components/InputGroup/InputGroup'
|
|
31
32
|
|
|
33
|
+
// Status and feedback (5.12: Icon draws with react-native-svg on native)
|
|
34
|
+
export {Alert, type AlertVariant} from './components/Alert/Alert'
|
|
35
|
+
export {Callout, type CalloutVariant} from './components/Callout/Callout'
|
|
36
|
+
export {EmptyState} from './components/EmptyState/EmptyState'
|
|
37
|
+
export {Icon, type IconName} from './components/Icon'
|
|
38
|
+
export {Meter, type MeterProps, type MeterTone} from './components/Meter/Meter'
|
|
39
|
+
export {Spinner, type SpinnerSize} from './components/Spinner/Spinner'
|
|
40
|
+
export {
|
|
41
|
+
StatusIcon,
|
|
42
|
+
type StatusIconName,
|
|
43
|
+
type StatusIconVariant,
|
|
44
|
+
} from './components/StatusIcon/StatusIcon'
|
|
45
|
+
|
|
46
|
+
// Choice and collection
|
|
47
|
+
export {List, type ListSelectionMode} from './components/List/List'
|
|
48
|
+
export {RadioGroup} from './components/RadioGroup/RadioGroup'
|
|
49
|
+
export {Toggle, type ToggleSize} from './components/Toggle/Toggle'
|
|
50
|
+
export {Tag, type TagVariant} from './components/Tag/Tag'
|
|
51
|
+
export {SideNav} from './components/SideNav/SideNav'
|
|
52
|
+
export {Tabs} from './components/Tabs/Tabs'
|
|
53
|
+
export {Tree} from './components/Tree/Tree'
|
|
54
|
+
// Always stacked on native: a phone is below every collapseBelow.
|
|
55
|
+
export {SplitPane} from './components/SplitPane/SplitPane'
|
|
56
|
+
// Always the phone shape on native: the bar on top, the rail in a left Drawer.
|
|
57
|
+
export {
|
|
58
|
+
AppShell,
|
|
59
|
+
type AppShellCollapse,
|
|
60
|
+
type AppShellRailState,
|
|
61
|
+
} from './components/AppShell/AppShell'
|
|
62
|
+
|
|
63
|
+
// Overlays (5.12: rendered in an OverlayModal on native)
|
|
64
|
+
export {ConfirmDialog} from './components/ConfirmDialog/ConfirmDialog'
|
|
65
|
+
export {Dialog, type DialogSize} from './components/Dialog/Dialog'
|
|
66
|
+
// 5.13: the panel follows the finger on the UI thread (Gesture Handler + Reanimated)
|
|
67
|
+
export {
|
|
68
|
+
Drawer,
|
|
69
|
+
type DrawerAnchor,
|
|
70
|
+
type DrawerSize,
|
|
71
|
+
type SnapPoint,
|
|
72
|
+
} from './components/Drawer/Drawer'
|
|
73
|
+
export {Menu, type MenuTriggerVariant} from './components/Menu/Menu'
|
|
74
|
+
export {Popover, type PopoverHandle} from './components/Popover/Popover'
|
|
75
|
+
// Touch has no hover: a Tooltip's text is its trigger's accessibility hint.
|
|
76
|
+
export {Tooltip} from './components/Tooltip/Tooltip'
|
|
77
|
+
// Drawn through the portal host, above every open overlay and the keyboard.
|
|
78
|
+
export {
|
|
79
|
+
ToastProvider,
|
|
80
|
+
useToast,
|
|
81
|
+
type ToastVariant,
|
|
82
|
+
type ToastOptions,
|
|
83
|
+
type ToastAction,
|
|
84
|
+
type ToastContextValue,
|
|
85
|
+
type ToastProviderProps,
|
|
86
|
+
} from './components/Toast'
|
|
87
|
+
|
|
32
88
|
// Layout primitives
|
|
89
|
+
export {PageShell, type PageShellMaxWidth, type PageShellPadding} from './components/PageShell'
|
|
33
90
|
export {Stack, type SpacingKey} from './components/Stack/Stack'
|
|
34
91
|
export {Inline} from './components/Inline/Inline'
|
|
35
92
|
export {Cluster} from './components/Cluster/Cluster'
|
|
@@ -41,5 +98,8 @@ export type {GridTrack} from './shared/length'
|
|
|
41
98
|
export {
|
|
42
99
|
ThemeProvider,
|
|
43
100
|
usePortalMount,
|
|
101
|
+
useResolvedColors,
|
|
44
102
|
type ThemeName,
|
|
45
103
|
} from './components/ThemeProvider/ThemeProvider'
|
|
104
|
+
// Where toasts draw on native: mount one inside a natively presented route.
|
|
105
|
+
export {PortalHost} from './shared/portal/PortalHost'
|
package/src/index.ts
CHANGED
|
@@ -49,7 +49,12 @@ export {
|
|
|
49
49
|
} from './components/DragDrop/DragDrop'
|
|
50
50
|
export {Dialog, type DialogSize} from './components/Dialog/Dialog'
|
|
51
51
|
export {DetailPanel, type DetailPanelSize} from './components/DetailPanel/DetailPanel'
|
|
52
|
-
export {
|
|
52
|
+
export {
|
|
53
|
+
Drawer,
|
|
54
|
+
type DrawerAnchor,
|
|
55
|
+
type DrawerSize,
|
|
56
|
+
type SnapPoint,
|
|
57
|
+
} from './components/Drawer/Drawer'
|
|
53
58
|
export {EmptyState} from './components/EmptyState/EmptyState'
|
|
54
59
|
export {Heading, type HeadingVariant} from './components/Heading/Heading'
|
|
55
60
|
export {List, type ListSelectionMode} from './components/List/List'
|
|
@@ -115,6 +120,8 @@ export {
|
|
|
115
120
|
type TimelineRowProps,
|
|
116
121
|
} from './components/Timeline/Timeline'
|
|
117
122
|
export {Tree} from './components/Tree/Tree'
|
|
123
|
+
export {SplitPane} from './components/SplitPane/SplitPane'
|
|
124
|
+
export {VirtualList, type VirtualListProps} from './components/VirtualList/VirtualList'
|
|
118
125
|
export {Textarea, type TextareaVariant} from './components/Textarea/Textarea'
|
|
119
126
|
export {
|
|
120
127
|
ThemeProvider,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The native graph has no DOM lib. A few spots on it still name DOM members:
|
|
2
|
+
// ConfirmDialog reads `event.target.value` from an Input change (RSD's native
|
|
3
|
+
// change event carries it too), and Grid reads `process.env.NODE_ENV`
|
|
4
|
+
// (inlined by Metro). Declare exactly those members rather than pulling in
|
|
5
|
+
// lib.dom.
|
|
6
|
+
interface HTMLInputElement {
|
|
7
|
+
value: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare const process: {env: {NODE_ENV?: string}}
|