@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,268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
type Ref,
|
|
5
|
+
type RefObject,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
useRef,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import {Linking, Platform, Pressable, ScrollView, StyleSheet} from 'react-native'
|
|
13
|
+
import {html} from 'react-strict-dom'
|
|
14
|
+
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
15
|
+
import {styles} from './styles.css'
|
|
16
|
+
import {nativeStyles} from './native.css'
|
|
17
|
+
import {useMenuOpen} from './useMenuOpen'
|
|
18
|
+
import {AnchoredOverlay} from '../../shared/portal/AnchoredOverlay'
|
|
19
|
+
import {focusForAccessibility} from '../../shared/accessibilityFocus'
|
|
20
|
+
import {mergeRefs} from '../../shared/mergeRefs'
|
|
21
|
+
import type {LinkClickEvent, OnNavigate} from '../../shared/navigate'
|
|
22
|
+
import type {ControlSize} from '../../shared/types'
|
|
23
|
+
import {useTooltipHint} from '../Tooltip/TooltipHint'
|
|
24
|
+
import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
|
|
25
|
+
|
|
26
|
+
interface NativeMenuContextValue {
|
|
27
|
+
open: boolean
|
|
28
|
+
toggle: () => void
|
|
29
|
+
/**
|
|
30
|
+
* Close, then run `after` once the menu's Modal is gone: an item that opens
|
|
31
|
+
* a Dialog would otherwise present during a dismissal, which iOS drops.
|
|
32
|
+
*/
|
|
33
|
+
closeThen: (after?: () => void) => void
|
|
34
|
+
menuId: string
|
|
35
|
+
triggerRef: RefObject<unknown>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const MenuContext = createContext<NativeMenuContextValue | null>(null)
|
|
39
|
+
|
|
40
|
+
function useMenu() {
|
|
41
|
+
const ctx = useContext(MenuContext)
|
|
42
|
+
if (!ctx) throw new Error('Menu compound components must be used within Menu.Root')
|
|
43
|
+
return ctx
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RootProps {
|
|
47
|
+
children: ReactNode
|
|
48
|
+
/** Controlled open state. */
|
|
49
|
+
open?: boolean
|
|
50
|
+
/** Initial open state (uncontrolled). */
|
|
51
|
+
defaultOpen?: boolean
|
|
52
|
+
/** Called when the menu opens or closes. */
|
|
53
|
+
onOpenChange?: (open: boolean) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Native Menu (plan N0, Slice B): the same compound API, its popup placed
|
|
58
|
+
* against the trigger in an OverlayModal. A press outside or Android's back
|
|
59
|
+
* button closes it; an item runs its action after the menu has gone. Native
|
|
60
|
+
* has no arrow keys or active descendant (Decision 6): each item is its own
|
|
61
|
+
* screen-reader stop.
|
|
62
|
+
*/
|
|
63
|
+
function Root({children, ...options}: RootProps) {
|
|
64
|
+
const {open, setOpen} = useMenuOpen(options)
|
|
65
|
+
const menuId = useId()
|
|
66
|
+
const triggerRef = useRef<unknown>(null)
|
|
67
|
+
const pendingRef = useRef<(() => void) | null>(null)
|
|
68
|
+
|
|
69
|
+
const runPending = useCallback(() => {
|
|
70
|
+
const pending = pendingRef.current
|
|
71
|
+
pendingRef.current = null
|
|
72
|
+
pending?.()
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
const closeThen = useCallback(
|
|
76
|
+
(after?: () => void) => {
|
|
77
|
+
pendingRef.current = after ?? null
|
|
78
|
+
setOpen(false)
|
|
79
|
+
},
|
|
80
|
+
[setOpen],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Focus back on the trigger once the menu is gone; Android has no Modal
|
|
84
|
+
// onDismiss, so a pending action runs on the next frame there.
|
|
85
|
+
const wasOpen = useRef(open)
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (wasOpen.current && !open) {
|
|
88
|
+
focusForAccessibility(triggerRef.current, 'Menu')
|
|
89
|
+
if (Platform.OS !== 'ios') requestAnimationFrame(runPending)
|
|
90
|
+
}
|
|
91
|
+
wasOpen.current = open
|
|
92
|
+
}, [open, runPending])
|
|
93
|
+
|
|
94
|
+
const toggle = useCallback(() => setOpen(!open), [open, setOpen])
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<MenuContext.Provider value={{open, toggle, closeThen, menuId, triggerRef}}>
|
|
98
|
+
<PendingContext.Provider value={runPending}>
|
|
99
|
+
<html.div style={[styles.root, nativeStyles.nativeFlex]}>{children}</html.div>
|
|
100
|
+
</PendingContext.Provider>
|
|
101
|
+
</MenuContext.Provider>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** iOS: run the pending item action once the Modal has dismissed. */
|
|
106
|
+
const PendingContext = createContext<() => void>(() => {})
|
|
107
|
+
|
|
108
|
+
export type MenuTriggerVariant = 'default' | 'ghost'
|
|
109
|
+
|
|
110
|
+
interface TriggerProps {
|
|
111
|
+
children: ReactNode
|
|
112
|
+
/** Accessible name — required when the label is an icon only. */
|
|
113
|
+
'aria-label'?: string
|
|
114
|
+
/** `ghost`: no border or fill until hover, for toolbars. Default: `default`. */
|
|
115
|
+
variant?: MenuTriggerVariant
|
|
116
|
+
/** `small` matches a small Toggle or Button beside it. Default: `default`. */
|
|
117
|
+
size?: ControlSize
|
|
118
|
+
ref?: Ref<unknown>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The trigger is an RN Pressable host, so it carries an enclosing Tooltip's
|
|
123
|
+
* text as its own accessibility hint (RSD drops `accessibilityHint` on html.*).
|
|
124
|
+
*
|
|
125
|
+
* holds-until: react-strict-dom maps aria-description to accessibilityHint
|
|
126
|
+
* (https://github.com/react/react-strict-dom/issues/524).
|
|
127
|
+
*/
|
|
128
|
+
function Trigger({
|
|
129
|
+
children,
|
|
130
|
+
'aria-label': ariaLabel,
|
|
131
|
+
variant = 'default',
|
|
132
|
+
size = 'default',
|
|
133
|
+
ref,
|
|
134
|
+
}: TriggerProps) {
|
|
135
|
+
const {open, toggle, triggerRef} = useMenu()
|
|
136
|
+
const hint = useTooltipHint()
|
|
137
|
+
return (
|
|
138
|
+
<Pressable
|
|
139
|
+
ref={mergeRefs(triggerRef, ref) as Ref<never>}
|
|
140
|
+
onPress={toggle}
|
|
141
|
+
accessibilityRole="button"
|
|
142
|
+
accessibilityLabel={ariaLabel}
|
|
143
|
+
accessibilityHint={hint}
|
|
144
|
+
accessibilityState={{expanded: open}}
|
|
145
|
+
>
|
|
146
|
+
<html.div
|
|
147
|
+
style={[
|
|
148
|
+
styles.trigger,
|
|
149
|
+
nativeStyles.nativeFlex,
|
|
150
|
+
variant === 'ghost'
|
|
151
|
+
? [styles.triggerGhost, size === 'small' && styles.triggerGhostSmall]
|
|
152
|
+
: size === 'small' && styles.triggerSmall,
|
|
153
|
+
]}
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
</html.div>
|
|
157
|
+
</Pressable>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface PopupProps {
|
|
162
|
+
children: ReactNode
|
|
163
|
+
align?: 'start' | 'end'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function Popup({children, align = 'start'}: PopupProps) {
|
|
167
|
+
const {open, closeThen, menuId, triggerRef} = useMenu()
|
|
168
|
+
const runPending = useContext(PendingContext)
|
|
169
|
+
const popupRef = useRef<HTMLDivElement>(null)
|
|
170
|
+
return (
|
|
171
|
+
<AnchoredOverlay
|
|
172
|
+
visible={open}
|
|
173
|
+
anchorRef={triggerRef}
|
|
174
|
+
side="bottom"
|
|
175
|
+
align={align}
|
|
176
|
+
onRequestClose={() => closeThen()}
|
|
177
|
+
onShow={() => focusForAccessibility(popupRef.current, 'Menu')}
|
|
178
|
+
onDismiss={runPending}
|
|
179
|
+
>
|
|
180
|
+
<html.div
|
|
181
|
+
ref={popupRef}
|
|
182
|
+
id={menuId}
|
|
183
|
+
role="menu"
|
|
184
|
+
style={[styles.popup, nativeStyles.popupInPlace]}
|
|
185
|
+
>
|
|
186
|
+
<ScrollView style={layout.scroll} bounces={false}>
|
|
187
|
+
<ControlContextBoundary>{children}</ControlContextBoundary>
|
|
188
|
+
</ScrollView>
|
|
189
|
+
</html.div>
|
|
190
|
+
</AnchoredOverlay>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The popup's height cap, `sizes.listMaxH`, on the view that scrolls.
|
|
195
|
+
const layout = StyleSheet.create({scroll: {maxHeight: SIZES_PX.listMaxH, flexGrow: 0}})
|
|
196
|
+
|
|
197
|
+
interface ItemProps {
|
|
198
|
+
onClick?: () => void
|
|
199
|
+
children: ReactNode
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function Item({onClick, children}: ItemProps) {
|
|
203
|
+
const {closeThen} = useMenu()
|
|
204
|
+
return (
|
|
205
|
+
<Pressable accessibilityRole="menuitem" onPress={() => closeThen(onClick)}>
|
|
206
|
+
{({pressed}) => (
|
|
207
|
+
<html.div style={[styles.item, pressed && nativeStyles.itemPressed]}>{children}</html.div>
|
|
208
|
+
)}
|
|
209
|
+
</Pressable>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface LinkItemProps {
|
|
214
|
+
href: string
|
|
215
|
+
/**
|
|
216
|
+
* Called with the href when the item is pressed (ADR-0002). Call
|
|
217
|
+
* `event.preventDefault()` to navigate in-app; otherwise, and without it,
|
|
218
|
+
* the URL opens with the system (`Linking.openURL`).
|
|
219
|
+
*/
|
|
220
|
+
onNavigate?: OnNavigate
|
|
221
|
+
children: ReactNode
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function LinkItem({href, onNavigate, children}: LinkItemProps) {
|
|
225
|
+
const {closeThen} = useMenu()
|
|
226
|
+
const follow = () => {
|
|
227
|
+
let prevented = false
|
|
228
|
+
const event: LinkClickEvent = {
|
|
229
|
+
button: 0,
|
|
230
|
+
metaKey: false,
|
|
231
|
+
ctrlKey: false,
|
|
232
|
+
shiftKey: false,
|
|
233
|
+
altKey: false,
|
|
234
|
+
get defaultPrevented() {
|
|
235
|
+
return prevented
|
|
236
|
+
},
|
|
237
|
+
preventDefault() {
|
|
238
|
+
prevented = true
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
onNavigate?.(href, event)
|
|
242
|
+
if (!prevented) {
|
|
243
|
+
Linking.openURL(href).catch((error: unknown) => {
|
|
244
|
+
console.warn(`[duro] Menu.LinkItem: could not open ${href}: ${String(error)}`)
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return (
|
|
249
|
+
<Pressable accessibilityRole="link" onPress={() => closeThen(follow)}>
|
|
250
|
+
{({pressed}) => (
|
|
251
|
+
<html.div style={[styles.item, pressed && nativeStyles.itemPressed]}>{children}</html.div>
|
|
252
|
+
)}
|
|
253
|
+
</Pressable>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function Separator() {
|
|
258
|
+
return <html.div role="separator" aria-orientation="horizontal" style={styles.separator} />
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const Menu = {
|
|
262
|
+
Root,
|
|
263
|
+
Trigger,
|
|
264
|
+
Popup,
|
|
265
|
+
Item,
|
|
266
|
+
LinkItem,
|
|
267
|
+
Separator,
|
|
268
|
+
}
|
|
@@ -15,6 +15,7 @@ import {styles} from './styles.css'
|
|
|
15
15
|
import {MenuContext, useMenu} from './MenuContext'
|
|
16
16
|
import {useMenuRoot} from './useMenuRoot'
|
|
17
17
|
import {devWarnOnce} from '../../shared/devWarnOnce'
|
|
18
|
+
import {isPlainClick, type LinkClickEvent, type OnNavigate} from '../../shared/navigate'
|
|
18
19
|
import type {ControlSize} from '../../shared/types'
|
|
19
20
|
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
20
21
|
import {computePopoverPosition} from '../Popover/position'
|
|
@@ -222,10 +223,15 @@ function Item({onClick, children}: ItemProps) {
|
|
|
222
223
|
// --- LinkItem ---
|
|
223
224
|
interface LinkItemProps {
|
|
224
225
|
href: string
|
|
226
|
+
/**
|
|
227
|
+
* Called for a plain primary click (ADR-0002): call `event.preventDefault()`,
|
|
228
|
+
* then your router's navigate. Without it the browser follows the link.
|
|
229
|
+
*/
|
|
230
|
+
onNavigate?: OnNavigate
|
|
225
231
|
children: ReactNode
|
|
226
232
|
}
|
|
227
233
|
|
|
228
|
-
function LinkItem({href, children}: LinkItemProps) {
|
|
234
|
+
function LinkItem({href, onNavigate, children}: LinkItemProps) {
|
|
229
235
|
const {close, highlightedId, setHighlightedId, registerItem} = useMenu()
|
|
230
236
|
const id = useId()
|
|
231
237
|
const ref = useRef<HTMLAnchorElement>(null)
|
|
@@ -242,7 +248,10 @@ function LinkItem({href, children}: LinkItemProps) {
|
|
|
242
248
|
ref={ref}
|
|
243
249
|
id={id}
|
|
244
250
|
href={href}
|
|
245
|
-
onClick={() =>
|
|
251
|
+
onClick={(event: LinkClickEvent) => {
|
|
252
|
+
if (onNavigate && isPlainClick(event)) onNavigate(href, event)
|
|
253
|
+
close()
|
|
254
|
+
}}
|
|
246
255
|
role="menuitem"
|
|
247
256
|
onPointerEnter={() => setHighlightedId(id)}
|
|
248
257
|
style={[styles.item, styles.linkItem, isHighlighted && styles.itemHighlighted]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
|
|
4
|
+
/** Native Menu additions to the shared styles. */
|
|
5
|
+
export const nativeStyles = css.create({
|
|
6
|
+
// The AnchoredOverlay's view places the popup; it flows inside it.
|
|
7
|
+
// RSD on native lays out only display: flex, not the web's inline-flex.
|
|
8
|
+
nativeFlex: {
|
|
9
|
+
display: 'flex',
|
|
10
|
+
},
|
|
11
|
+
popupInPlace: {
|
|
12
|
+
position: 'relative',
|
|
13
|
+
},
|
|
14
|
+
// A press shows the highlight that hover and the arrow keys show on the web.
|
|
15
|
+
itemPressed: {
|
|
16
|
+
backgroundColor: colors.bgCardHover,
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {useCallback, useRef, useState} from 'react'
|
|
2
|
+
|
|
3
|
+
export interface MenuOpenOptions {
|
|
4
|
+
open?: boolean
|
|
5
|
+
defaultOpen?: boolean
|
|
6
|
+
onOpenChange?: (open: boolean) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The menu's open state, controlled or not, shared by both platforms (plan
|
|
11
|
+
* N0, Decision 10). Focus, highlight and keys are each platform's adapter:
|
|
12
|
+
* `useMenuRoot` on the web, Menu.native on native.
|
|
13
|
+
*/
|
|
14
|
+
export function useMenuOpen({open: openProp, defaultOpen = false, onOpenChange}: MenuOpenOptions) {
|
|
15
|
+
const [inner, setInner] = useState(defaultOpen)
|
|
16
|
+
const controlled = openProp !== undefined
|
|
17
|
+
const open = controlled ? openProp : inner
|
|
18
|
+
const openRef = useRef(open)
|
|
19
|
+
openRef.current = open
|
|
20
|
+
const onOpenChangeRef = useRef(onOpenChange)
|
|
21
|
+
onOpenChangeRef.current = onOpenChange
|
|
22
|
+
|
|
23
|
+
const setOpen = useCallback(
|
|
24
|
+
(next: boolean) => {
|
|
25
|
+
if (next === openRef.current) return
|
|
26
|
+
if (!controlled) setInner(next)
|
|
27
|
+
onOpenChangeRef.current?.(next)
|
|
28
|
+
},
|
|
29
|
+
[controlled],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return {open, openRef, setOpen}
|
|
33
|
+
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import {useState, useCallback, useRef, useId, useEffect, useLayoutEffect} from 'react'
|
|
2
2
|
import type {MenuContextValue} from './MenuContext'
|
|
3
3
|
import {isOutsidePress} from '../Popover/outside'
|
|
4
|
+
import {useMenuOpen, type MenuOpenOptions} from './useMenuOpen'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
open?: boolean
|
|
7
|
-
defaultOpen?: boolean
|
|
8
|
-
onOpenChange?: (open: boolean) => void
|
|
9
|
-
}
|
|
6
|
+
export type MenuRootOptions = MenuOpenOptions
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* The menu-button pattern (WAI-ARIA APG): on open, focus moves to the
|
|
@@ -16,10 +13,8 @@ export interface MenuRootOptions {
|
|
|
16
13
|
* elsewhere (an editor) are never intercepted, and Escape stops there so an
|
|
17
14
|
* enclosing Popover or Dialog stays open.
|
|
18
15
|
*/
|
|
19
|
-
export function useMenuRoot(
|
|
20
|
-
const
|
|
21
|
-
const controlled = openProp !== undefined
|
|
22
|
-
const open = controlled ? openProp : inner
|
|
16
|
+
export function useMenuRoot(options: MenuRootOptions) {
|
|
17
|
+
const {open, openRef, setOpen} = useMenuOpen(options)
|
|
23
18
|
const [highlightedId, setHighlightedId] = useState<string | null>(null)
|
|
24
19
|
// Read by the keydown handler, so activating an item is not a side effect
|
|
25
20
|
// inside a state updater (React may run updaters twice).
|
|
@@ -34,38 +29,24 @@ export function useMenuRoot({open: openProp, defaultOpen = false, onOpenChange}:
|
|
|
34
29
|
const popupRef = useRef<HTMLDivElement | null>(null)
|
|
35
30
|
const itemsRef = useRef(new Map<string, HTMLElement>())
|
|
36
31
|
const orderRef = useRef<string[]>([])
|
|
37
|
-
const openRef = useRef(open)
|
|
38
|
-
openRef.current = open
|
|
39
|
-
const onOpenChangeRef = useRef(onOpenChange)
|
|
40
|
-
onOpenChangeRef.current = onOpenChange
|
|
41
|
-
|
|
42
|
-
const setOpen = useCallback(
|
|
43
|
-
(next: boolean) => {
|
|
44
|
-
if (next === openRef.current) return
|
|
45
|
-
if (!controlled) setInner(next)
|
|
46
|
-
onOpenChangeRef.current?.(next)
|
|
47
|
-
},
|
|
48
|
-
[controlled],
|
|
49
|
-
)
|
|
50
|
-
|
|
51
32
|
// Closes and leaves focus where it is (a press elsewhere put it there).
|
|
52
33
|
const dismiss = useCallback(() => {
|
|
53
34
|
if (!openRef.current) return
|
|
54
35
|
setOpen(false)
|
|
55
36
|
setHighlightedId(null)
|
|
56
|
-
}, [setOpen])
|
|
37
|
+
}, [openRef, setOpen])
|
|
57
38
|
|
|
58
39
|
// Closes and puts focus back on the trigger.
|
|
59
40
|
const close = useCallback(() => {
|
|
60
41
|
if (!openRef.current) return
|
|
61
42
|
dismiss()
|
|
62
43
|
triggerRef.current?.focus()
|
|
63
|
-
}, [dismiss])
|
|
44
|
+
}, [openRef, dismiss])
|
|
64
45
|
|
|
65
46
|
const toggle = useCallback(() => {
|
|
66
47
|
if (openRef.current) close()
|
|
67
48
|
else setOpen(true)
|
|
68
|
-
}, [close, setOpen])
|
|
49
|
+
}, [openRef, close, setOpen])
|
|
69
50
|
|
|
70
51
|
// On open: focus the menu and highlight the first item. Child effects (item
|
|
71
52
|
// registration) run before this parent effect, so orderRef is populated.
|
|
@@ -8,6 +8,8 @@ export const styles = css.create({
|
|
|
8
8
|
track: {
|
|
9
9
|
position: 'relative',
|
|
10
10
|
width: '100%',
|
|
11
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
12
|
+
boxSizing: 'border-box',
|
|
11
13
|
height: sizes.meterH,
|
|
12
14
|
borderRadius: radii.full,
|
|
13
15
|
backgroundColor: colors.border,
|
|
@@ -6,9 +6,13 @@ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
|
6
6
|
export const styles = css.create({
|
|
7
7
|
root: {
|
|
8
8
|
width: '100%',
|
|
9
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
10
|
+
boxSizing: 'border-box',
|
|
9
11
|
},
|
|
10
12
|
container: {
|
|
11
13
|
width: '100%',
|
|
14
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
15
|
+
boxSizing: 'border-box',
|
|
12
16
|
marginLeft: 'auto',
|
|
13
17
|
marginRight: 'auto',
|
|
14
18
|
},
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Non-modal anchored overlay for small interactive content. Compound component — Root is required (throws without it). Anchored to Popover.Trigger, or to a virtual anchor (a () => DOMRect) when there is no Trigger; call reposition() through a ref when the anchor moves. Popover.Trigger takes size="small" beside small Toggles. Popover.Popup raised draws it over another floating surface (floatingRaised, popoverRaised in a modal), never over a modal.',
|
|
5
|
+
'Non-modal anchored overlay for small interactive content. Compound component — Root is required (throws without it). Anchored to Popover.Trigger, or to a virtual anchor (a () => DOMRect) when there is no Trigger; call reposition() through a ref when the anchor moves. Popover.Trigger takes size="small" beside small Toggles. Popover.Popup raised draws it over another floating surface (floatingRaised, popoverRaised in a modal), never over a modal. On React Native the popup is modal: it is placed against Popover.Trigger in an overlay, a press outside or the Android back button closes it, and screen-reader focus moves in on open and back to the trigger on close; anchor, ignore and raised are web only.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'A small form or controls tied to one element that stay open while you work in them',
|
|
8
8
|
'Anchoring to a spot that is not a button (a canvas point, a text selection) with a virtual anchor',
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
type Ref,
|
|
5
|
+
type RefObject,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
useImperativeHandle,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from 'react'
|
|
14
|
+
import {Pressable} from 'react-native'
|
|
15
|
+
import {html} from 'react-strict-dom'
|
|
16
|
+
import {styles} from './styles.css'
|
|
17
|
+
import {nativeStyles} from './native.css'
|
|
18
|
+
import type {PopoverAlign, PopoverSide} from './position'
|
|
19
|
+
import {AnchoredOverlay} from '../../shared/portal/AnchoredOverlay'
|
|
20
|
+
import {focusForAccessibility} from '../../shared/accessibilityFocus'
|
|
21
|
+
import {mergeRefs} from '../../shared/mergeRefs'
|
|
22
|
+
import {ModalContext} from '../../shared/ModalContext'
|
|
23
|
+
import type {ControlSize} from '../../shared/types'
|
|
24
|
+
import {useTooltipHint} from '../Tooltip/TooltipHint'
|
|
25
|
+
import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
|
|
26
|
+
|
|
27
|
+
interface PopoverContextValue {
|
|
28
|
+
open: boolean
|
|
29
|
+
setOpen: (open: boolean) => void
|
|
30
|
+
popoverId: string
|
|
31
|
+
side: PopoverSide
|
|
32
|
+
align: PopoverAlign
|
|
33
|
+
triggerRef: RefObject<unknown>
|
|
34
|
+
/** Bumped by `reposition()`: the popup measures its trigger again */
|
|
35
|
+
placement: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PopoverContext = createContext<PopoverContextValue | null>(null)
|
|
39
|
+
|
|
40
|
+
function usePopover(part: string) {
|
|
41
|
+
const ctx = useContext(PopoverContext)
|
|
42
|
+
if (!ctx) throw new Error(`Popover.${part} must be used within Popover.Root`)
|
|
43
|
+
return ctx
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PopoverHandle {
|
|
47
|
+
/** Place the popup against its trigger again. */
|
|
48
|
+
reposition: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface RootProps {
|
|
52
|
+
children: ReactNode
|
|
53
|
+
open?: boolean
|
|
54
|
+
defaultOpen?: boolean
|
|
55
|
+
onOpenChange?: (open: boolean) => void
|
|
56
|
+
side?: PopoverSide
|
|
57
|
+
align?: PopoverAlign
|
|
58
|
+
/** Web only: native places the popup against Popover.Trigger. */
|
|
59
|
+
anchor?: () => DOMRect
|
|
60
|
+
/** Web only: on native the popup is modal, so nothing outside it is pressed. */
|
|
61
|
+
ignore?: ReadonlyArray<unknown>
|
|
62
|
+
ref?: Ref<PopoverHandle>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Native Popover (plan N0, Slice B): the popup is placed against the trigger
|
|
67
|
+
* in an OverlayModal, so it is modal on native where the web's is not. A
|
|
68
|
+
* press outside or Android's back button closes it; the screen reader's
|
|
69
|
+
* focus moves into it on open and back to the trigger on close.
|
|
70
|
+
*/
|
|
71
|
+
function Root({
|
|
72
|
+
children,
|
|
73
|
+
open: openProp,
|
|
74
|
+
defaultOpen = false,
|
|
75
|
+
onOpenChange,
|
|
76
|
+
side = 'bottom',
|
|
77
|
+
align = 'start',
|
|
78
|
+
ref,
|
|
79
|
+
}: RootProps) {
|
|
80
|
+
const [inner, setInner] = useState(defaultOpen)
|
|
81
|
+
const controlled = openProp !== undefined
|
|
82
|
+
const open = controlled ? openProp : inner
|
|
83
|
+
const popoverId = useId()
|
|
84
|
+
const triggerRef = useRef<unknown>(null)
|
|
85
|
+
const [placement, setPlacement] = useState(0)
|
|
86
|
+
const openRef = useRef(open)
|
|
87
|
+
openRef.current = open
|
|
88
|
+
|
|
89
|
+
const setOpen = useCallback(
|
|
90
|
+
(next: boolean) => {
|
|
91
|
+
if (!controlled) setInner(next)
|
|
92
|
+
if (next !== openRef.current) onOpenChange?.(next)
|
|
93
|
+
},
|
|
94
|
+
[controlled, onOpenChange],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
useImperativeHandle(ref, () => ({reposition: () => setPlacement((n) => n + 1)}), [])
|
|
98
|
+
|
|
99
|
+
const wasOpen = useRef(open)
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (wasOpen.current && !open) focusForAccessibility(triggerRef.current, 'Popover')
|
|
102
|
+
wasOpen.current = open
|
|
103
|
+
}, [open])
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<PopoverContext.Provider value={{open, setOpen, popoverId, side, align, triggerRef, placement}}>
|
|
107
|
+
{children}
|
|
108
|
+
</PopoverContext.Provider>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface TriggerProps {
|
|
113
|
+
children: ReactNode
|
|
114
|
+
/** Accessible name: required when the label is an icon only. */
|
|
115
|
+
'aria-label'?: string
|
|
116
|
+
/** `small` matches a small Toggle or Button beside it. Default: `default`. */
|
|
117
|
+
size?: ControlSize
|
|
118
|
+
/** The trigger's host, e.g. to measure it. */
|
|
119
|
+
ref?: Ref<unknown>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* An RN Pressable host, so it carries an enclosing Tooltip's text as its hint.
|
|
124
|
+
*
|
|
125
|
+
* holds-until: react-strict-dom maps aria-description to accessibilityHint
|
|
126
|
+
* (https://github.com/react/react-strict-dom/issues/524).
|
|
127
|
+
*/
|
|
128
|
+
function Trigger({children, 'aria-label': ariaLabel, size = 'default', ref}: TriggerProps) {
|
|
129
|
+
const {open, setOpen, triggerRef} = usePopover('Trigger')
|
|
130
|
+
const hint = useTooltipHint()
|
|
131
|
+
return (
|
|
132
|
+
<Pressable
|
|
133
|
+
ref={mergeRefs(triggerRef, ref) as Ref<never>}
|
|
134
|
+
onPress={() => setOpen(!open)}
|
|
135
|
+
accessibilityRole="button"
|
|
136
|
+
accessibilityLabel={ariaLabel}
|
|
137
|
+
accessibilityHint={hint}
|
|
138
|
+
accessibilityState={{expanded: open}}
|
|
139
|
+
>
|
|
140
|
+
<html.div
|
|
141
|
+
style={[styles.trigger, nativeStyles.nativeFlex, size === 'small' && styles.triggerSmall]}
|
|
142
|
+
>
|
|
143
|
+
{children}
|
|
144
|
+
</html.div>
|
|
145
|
+
</Pressable>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface PopupProps {
|
|
150
|
+
children: ReactNode
|
|
151
|
+
/** Accessible name for the dialog. */
|
|
152
|
+
label: string
|
|
153
|
+
/** Web only: on native every popup is its own Modal, above what opened it. */
|
|
154
|
+
raised?: boolean
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function Popup({children, label}: PopupProps) {
|
|
158
|
+
const {open, setOpen, popoverId, side, align, triggerRef, placement} = usePopover('Popup')
|
|
159
|
+
const popupRef = useRef<HTMLDivElement>(null)
|
|
160
|
+
return (
|
|
161
|
+
<AnchoredOverlay
|
|
162
|
+
key={placement}
|
|
163
|
+
visible={open}
|
|
164
|
+
anchorRef={triggerRef}
|
|
165
|
+
side={side}
|
|
166
|
+
align={align}
|
|
167
|
+
onRequestClose={() => setOpen(false)}
|
|
168
|
+
onShow={() => focusForAccessibility(popupRef.current, 'Popover')}
|
|
169
|
+
>
|
|
170
|
+
<html.div
|
|
171
|
+
ref={popupRef}
|
|
172
|
+
id={popoverId}
|
|
173
|
+
role="dialog"
|
|
174
|
+
aria-modal
|
|
175
|
+
aria-label={label}
|
|
176
|
+
style={[styles.popup, nativeStyles.popupInPlace]}
|
|
177
|
+
>
|
|
178
|
+
{/* Modal on native: a Popover or Menu opened inside it stacks above it. */}
|
|
179
|
+
<ModalContext.Provider value={true}>
|
|
180
|
+
<ControlContextBoundary>{children}</ControlContextBoundary>
|
|
181
|
+
</ModalContext.Provider>
|
|
182
|
+
</html.div>
|
|
183
|
+
</AnchoredOverlay>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Close({children}: {children: ReactNode}) {
|
|
188
|
+
const {setOpen} = usePopover('Close')
|
|
189
|
+
return (
|
|
190
|
+
<html.button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={() => setOpen(false)}
|
|
193
|
+
style={[styles.close, nativeStyles.nativeFlex]}
|
|
194
|
+
>
|
|
195
|
+
{children}
|
|
196
|
+
</html.button>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const Popover = {
|
|
201
|
+
Root,
|
|
202
|
+
Trigger,
|
|
203
|
+
Popup,
|
|
204
|
+
Close,
|
|
205
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
|
|
3
|
+
/** Native Popover additions to the shared styles. */
|
|
4
|
+
export const nativeStyles = css.create({
|
|
5
|
+
// The AnchoredOverlay's view places the popup; it flows inside it.
|
|
6
|
+
// RSD on native lays out only display: flex, not the web's inline-flex.
|
|
7
|
+
nativeFlex: {
|
|
8
|
+
display: 'flex',
|
|
9
|
+
},
|
|
10
|
+
popupInPlace: {
|
|
11
|
+
position: 'relative',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
@@ -30,7 +30,7 @@ export const meta: ComponentMeta = {
|
|
|
30
30
|
'RadioGroup for form single-select; ToggleGroup for segmented/toolbar single-select',
|
|
31
31
|
},
|
|
32
32
|
],
|
|
33
|
-
example: `<RadioGroup.Root defaultValue="email" onValueChange={(v) => console.log(v)}>
|
|
33
|
+
example: `<RadioGroup.Root name="channel" defaultValue="email" onValueChange={(v) => console.log(v)}>
|
|
34
34
|
<RadioGroup.Item value="email">Email</RadioGroup.Item>
|
|
35
35
|
<RadioGroup.Item value="sms">SMS</RadioGroup.Item>
|
|
36
36
|
<RadioGroup.Item value="push">Push notification</RadioGroup.Item>
|