@astryxdesign/core 0.5.1 → 0.5.2-canary.00477a5
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/CHANGELOG.md +19 -0
- package/dist/BottomSheet/BottomSheet.d.ts +3 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +4 -3
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
- package/dist/Button/Button.d.ts +2 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -4
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +2 -2
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +34 -17
- package/dist/ContextMenu/ContextMenu.d.ts +14 -3
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +149 -26
- package/dist/ContextMenu/index.d.ts +1 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +340 -26
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
- package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
- package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
- package/dist/DropdownMenu/index.d.ts +2 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/menuWidth.d.ts +14 -0
- package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
- package/dist/DropdownMenu/menuWidth.js +35 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
- package/dist/DropdownMenu/useMenuOverflow.js +60 -0
- package/dist/MoreMenu/MoreMenu.d.ts +8 -2
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +2 -0
- package/dist/MultiSelector/MultiSelector.d.ts +11 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +97 -68
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/Popover/Popover.d.ts +10 -6
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +141 -16
- package/dist/Popover/usePopover.d.ts +4 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +52 -5
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +106 -80
- package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
- package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
- package/dist/Selector/SelectorBottomSheet.js +83 -0
- package/dist/Selector/index.d.ts +1 -1
- package/dist/Selector/index.d.ts.map +1 -1
- package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
- package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
- package/dist/Selector/selectorPresentation.stylex.js +21 -0
- package/dist/Selector/useSelectorPresentation.d.ts +32 -0
- package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
- package/dist/Selector/useSelectorPresentation.js +90 -0
- package/dist/Toast/useToastGesture.d.ts.map +1 -1
- package/dist/Toast/useToastGesture.js +46 -9
- package/dist/astryx.css +19 -1
- package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
- package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
- package/dist/hooks/useAdaptivePresentation.js +19 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
- package/dist/hooks/useFocusReturnVisibility.js +35 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +12 -0
- package/dist/i18n/InternationalizationContext.d.ts +2 -0
- package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
- package/dist/i18n/InternationalizationContext.js +3 -1
- package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
- package/dist/i18n/InternationalizationProvider.js +3 -1
- package/dist/i18n/resolve.d.ts +1 -1
- package/dist/i18n/resolve.d.ts.map +1 -1
- package/dist/i18n/resolve.js +37 -38
- package/dist/i18n/useTranslator.d.ts.map +1 -1
- package/dist/i18n/useTranslator.js +2 -3
- package/locales/af-ZA.json +0 -4
- package/locales/ar-SA.json +0 -4
- package/locales/ca-ES.json +0 -4
- package/locales/cs-CZ.json +0 -4
- package/locales/da-DK.json +0 -4
- package/locales/de-DE.json +0 -4
- package/locales/el-GR.json +0 -4
- package/locales/en.json +20 -0
- package/locales/es-ES.json +0 -4
- package/locales/fi-FI.json +0 -4
- package/locales/fr-FR.json +0 -4
- package/locales/he-IL.json +0 -4
- package/locales/hu-HU.json +0 -4
- package/locales/it-IT.json +0 -4
- package/locales/ja-JP.json +0 -4
- package/locales/ko-KR.json +0 -4
- package/locales/nl-NL.json +0 -4
- package/locales/no-NO.json +0 -4
- package/locales/pl-PL.json +0 -4
- package/locales/pseudo.json +15 -0
- package/locales/pt-BR.json +0 -4
- package/locales/pt-PT.json +0 -4
- package/locales/ro-RO.json +0 -4
- package/locales/ru-RU.json +0 -4
- package/locales/sr-SP.json +0 -4
- package/locales/sv-SE.json +0 -4
- package/locales/tr-TR.json +0 -4
- package/locales/uk-UA.json +0 -4
- package/locales/vi-VN.json +0 -4
- package/locales/zh-CN.json +0 -4
- package/locales/zh-TW.json +0 -4
- package/package.json +3 -3
- package/src/BottomSheet/BottomSheet.doc.mjs +37 -0
- package/src/BottomSheet/BottomSheet.spec.md +177 -0
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +7 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
- package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
- package/src/Button/Button.doc.mjs +56 -0
- package/src/Button/Button.test.tsx +10 -0
- package/src/Button/Button.tsx +13 -5
- package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
- package/src/Calendar/Calendar.doc.mjs +1 -1
- package/src/Carousel/Carousel.test.tsx +83 -0
- package/src/Carousel/Carousel.tsx +8 -2
- package/src/Chat/ChatToolCalls.test.tsx +44 -0
- package/src/Chat/ChatToolCalls.tsx +36 -15
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/CheckboxList/CheckboxList.doc.mjs +173 -32
- package/src/CheckboxList/CheckboxList.spec.md +214 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +72 -0
- package/src/CommandPalette/CommandPalette.spec.md +223 -0
- package/src/ComplexSelector/ComplexSelector.doc.mjs +44 -2
- package/src/ComplexSelector/ComplexSelector.spec.md +189 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
- package/src/ContextMenu/ContextMenu.test.tsx +131 -1
- package/src/ContextMenu/ContextMenu.tsx +199 -32
- package/src/ContextMenu/index.ts +1 -0
- package/src/DateInput/DateInput.doc.mjs +1 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
- package/src/Divider/Divider.doc.mjs +24 -0
- package/src/Divider/Divider.spec.md +150 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
- package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
- package/src/DropdownMenu/DropdownMenu.tsx +459 -28
- package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
- package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
- package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
- package/src/DropdownMenu/index.ts +2 -0
- package/src/DropdownMenu/menuWidth.ts +51 -0
- package/src/DropdownMenu/useMenuOverflow.ts +77 -0
- package/src/Field/Field.doc.mjs +1 -1
- package/src/FieldStatus/FieldStatus.doc.mjs +22 -1
- package/src/FieldStatus/FieldStatus.spec.md +212 -0
- package/src/FileInput/FileInput.doc.mjs +1 -1
- package/src/Grid/Grid.doc.mjs +19 -0
- package/src/Grid/Grid.spec.md +161 -0
- package/src/Icon/Icon.doc.mjs +196 -48
- package/src/Icon/Icon.spec.md +191 -0
- package/src/IconButton/IconButton.doc.mjs +38 -0
- package/src/Indicator/Indicator.doc.mjs +1 -1
- package/src/InputGroup/InputGroup.doc.mjs +1 -1
- package/src/Kbd/Kbd.doc.mjs +18 -0
- package/src/Kbd/Kbd.spec.md +141 -0
- package/src/Layout/Layout.doc.mjs +32 -0
- package/src/Layout/Layout.spec.md +199 -0
- package/src/Lightbox/Lightbox.doc.mjs +149 -33
- package/src/Lightbox/Lightbox.spec.md +197 -0
- package/src/Markdown/Markdown.doc.mjs +60 -0
- package/src/Markdown/Markdown.spec.md +200 -0
- package/src/MobileNav/MobileNav.doc.mjs +140 -32
- package/src/MobileNav/MobileNav.spec.md +208 -0
- package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
- package/src/MoreMenu/MoreMenu.test.tsx +47 -0
- package/src/MoreMenu/MoreMenu.tsx +13 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +129 -1
- package/src/MultiSelector/MultiSelector.spec.md +266 -0
- package/src/MultiSelector/MultiSelector.test.tsx +127 -0
- package/src/MultiSelector/MultiSelector.tsx +121 -73
- package/src/MultiSelector/index.ts +1 -0
- package/src/NavIcon/NavIcon.doc.mjs +17 -0
- package/src/NavIcon/NavIcon.spec.md +133 -0
- package/src/NavMenu/NavHeadingMenu.spec.md +205 -0
- package/src/NavMenu/NavMenu.doc.mjs +37 -0
- package/src/NumberInput/NumberInput.doc.mjs +1 -1
- package/src/Outline/Outline.doc.mjs +35 -0
- package/src/Outline/Outline.spec.md +161 -0
- package/src/OverflowList/OverflowList.doc.mjs +24 -0
- package/src/OverflowList/OverflowList.spec.md +156 -0
- package/src/Pagination/Pagination.doc.mjs +74 -0
- package/src/Pagination/Pagination.spec.md +225 -0
- package/src/Popover/Popover.doc.mjs +11 -11
- package/src/Popover/Popover.test.tsx +397 -2
- package/src/Popover/Popover.tsx +228 -21
- package/src/Popover/usePopover.tsx +92 -15
- package/src/PowerSearch/PowerSearch.doc.mjs +1 -1
- package/src/ProgressBar/ProgressBar.doc.mjs +198 -38
- package/src/ProgressBar/ProgressBar.spec.md +260 -0
- package/src/RadioList/RadioList.doc.mjs +1 -1
- package/src/Resizable/Resizable.doc.mjs +59 -16
- package/src/Resizable/Resizable.spec.md +194 -0
- package/src/Section/Section.doc.mjs +17 -0
- package/src/Section/Section.spec.md +138 -0
- package/src/SegmentedControl/SegmentedControl.doc.mjs +83 -0
- package/src/SegmentedControl/SegmentedControl.spec.md +151 -0
- package/src/Selector/Selector.doc.mjs +13 -1
- package/src/Selector/Selector.test.tsx +135 -0
- package/src/Selector/Selector.tsx +141 -82
- package/src/Selector/SelectorBottomSheet.tsx +97 -0
- package/src/Selector/index.ts +1 -0
- package/src/Selector/selectorPresentation.stylex.ts +20 -0
- package/src/Selector/useSelectorPresentation.ts +147 -0
- package/src/Skeleton/Skeleton.doc.mjs +12 -0
- package/src/Skeleton/Skeleton.spec.md +126 -0
- package/src/Slider/Slider.doc.mjs +186 -32
- package/src/Slider/Slider.spec.md +223 -0
- package/src/Stack/Stack.doc.mjs +22 -0
- package/src/Stack/Stack.spec.md +154 -0
- package/src/StatusDot/StatusDot.doc.mjs +17 -0
- package/src/StatusDot/StatusDot.spec.md +137 -0
- package/src/Switch/Switch.doc.mjs +41 -1
- package/src/Switch/Switch.spec.md +174 -0
- package/src/Text/Text.doc.mjs +190 -43
- package/src/Text/Text.spec.md +205 -0
- package/src/TextArea/TextArea.doc.mjs +256 -46
- package/src/TextArea/TextArea.spec.md +204 -0
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.doc.mjs +1 -1
- package/src/Toast/ToastViewport.test.tsx +118 -4
- package/src/Toast/useToastGesture.ts +61 -8
- package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
- package/src/Toolbar/Toolbar.doc.mjs +19 -0
- package/src/Toolbar/Toolbar.spec.md +173 -0
- package/src/Tooltip/Tooltip.doc.mjs +17 -0
- package/src/Tooltip/Tooltip.spec.md +141 -0
- package/src/TreeList/TreeList.doc.mjs +58 -0
- package/src/TreeList/TreeList.spec.md +220 -0
- package/src/Typeahead/Typeahead.doc.mjs +84 -2
- package/src/Typeahead/Typeahead.spec.md +250 -0
- package/src/hooks/useAdaptivePresentation.ts +35 -0
- package/src/hooks/useFocusReturnVisibility.ts +45 -0
- package/src/hooks/useFocusTrap.test.tsx +40 -0
- package/src/hooks/useFocusTrap.ts +13 -0
- package/src/i18n/InternationalizationContext.ts +3 -0
- package/src/i18n/InternationalizationProvider.tsx +8 -1
- package/src/i18n/__tests__/resolve.test.ts +23 -1
- package/src/i18n/resolve.ts +46 -49
- package/src/i18n/useTranslator.ts +2 -7
- package/src/theme/themingTargets.test.ts +91 -25
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @file ContextMenu.tsx
|
|
6
|
-
* @input Uses React, StyleX, useLayer
|
|
7
|
-
*
|
|
6
|
+
* @input Uses React, StyleX, useLayer, BottomSheet, useListFocus, and the
|
|
7
|
+
* shared menu-presentation resolver
|
|
8
|
+
* @output Exports ContextMenu with cursor-popover and touch-sheet presentations
|
|
8
9
|
* @position Core implementation; consumed by index.ts
|
|
9
10
|
*
|
|
10
11
|
* Right-click context menu positioned at the cursor. The cursor point is
|
|
@@ -34,13 +35,19 @@ import React, {
|
|
|
34
35
|
useCallback,
|
|
35
36
|
useEffect,
|
|
36
37
|
useId,
|
|
38
|
+
lazy,
|
|
37
39
|
useMemo,
|
|
38
40
|
useRef,
|
|
39
41
|
useState,
|
|
42
|
+
Suspense,
|
|
40
43
|
} from 'react';
|
|
41
44
|
import type {ReactNode} from 'react';
|
|
42
45
|
import * as stylex from '@stylexjs/stylex';
|
|
46
|
+
import {Button} from '../Button';
|
|
47
|
+
import {Heading} from '../Heading';
|
|
48
|
+
import {Icon} from '../Icon';
|
|
43
49
|
import {useLayer} from '../Layer/useLayer';
|
|
50
|
+
import {MenuBottomSheetActionList} from '../DropdownMenu/MenuBottomSheetActionList';
|
|
44
51
|
import {renderDropdownItems} from '../DropdownMenu/renderDropdownItems';
|
|
45
52
|
import {
|
|
46
53
|
DropdownMenuContext,
|
|
@@ -63,7 +70,7 @@ import {
|
|
|
63
70
|
easeVars,
|
|
64
71
|
shadowVars,
|
|
65
72
|
} from '../theme/tokens.stylex';
|
|
66
|
-
import {mergeProps, isImeKeyEvent} from '../utils';
|
|
73
|
+
import {mergeProps, isImeKeyEvent, rtlStyles} from '../utils';
|
|
67
74
|
import type {BaseProps} from '../BaseProps';
|
|
68
75
|
import type {StyleXStyles} from '../theme/types';
|
|
69
76
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -76,6 +83,16 @@ import type {
|
|
|
76
83
|
} from '../DropdownMenu/DropdownMenu';
|
|
77
84
|
|
|
78
85
|
import {useMergedRefs} from '../hooks/useMergedRefs';
|
|
86
|
+
import {
|
|
87
|
+
useAdaptivePresentation,
|
|
88
|
+
type AdaptivePresentation,
|
|
89
|
+
} from '../hooks/useAdaptivePresentation';
|
|
90
|
+
|
|
91
|
+
const LazyMenuBottomSheet = lazy(async () =>
|
|
92
|
+
import('../DropdownMenu/MenuBottomSheet').then(module => ({
|
|
93
|
+
default: module.MenuBottomSheet,
|
|
94
|
+
})),
|
|
95
|
+
);
|
|
79
96
|
const styles = stylex.create({
|
|
80
97
|
// Trigger wrapper: suppress the iOS long-press callout/selection so the
|
|
81
98
|
// long-press opens our context menu instead of the native text/callout UI.
|
|
@@ -85,6 +102,8 @@ const styles = stylex.create({
|
|
|
85
102
|
trigger: {
|
|
86
103
|
position: 'relative',
|
|
87
104
|
WebkitTouchCallout: 'none',
|
|
105
|
+
WebkitUserSelect: 'none',
|
|
106
|
+
userSelect: 'none',
|
|
88
107
|
},
|
|
89
108
|
// Zero-size anchor placed at the cursor point within the trigger. The menu
|
|
90
109
|
// is anchored to this element, so it sits under the cursor yet is positioned
|
|
@@ -121,6 +140,33 @@ const styles = stylex.create({
|
|
|
121
140
|
popoverCustomWidth: (width: string | number) => ({
|
|
122
141
|
minWidth: typeof width === 'number' ? `${width}px` : width,
|
|
123
142
|
}),
|
|
143
|
+
sheetMenu: {
|
|
144
|
+
boxSizing: 'border-box',
|
|
145
|
+
display: 'flex',
|
|
146
|
+
flexDirection: 'column',
|
|
147
|
+
gap: spacingVars['--spacing-0-5'],
|
|
148
|
+
width: '100%',
|
|
149
|
+
'--_dropdown-menu-radius': radiusVars['--radius-container'],
|
|
150
|
+
'--_dropdown-menu-padding': spacingVars['--spacing-1'],
|
|
151
|
+
padding: spacingVars['--spacing-1'],
|
|
152
|
+
borderRadius: 'var(--_dropdown-menu-radius)',
|
|
153
|
+
backgroundColor: colorVars['--color-background-surface'],
|
|
154
|
+
outline: 'none',
|
|
155
|
+
userSelect: 'none',
|
|
156
|
+
},
|
|
157
|
+
sheetHeader: {
|
|
158
|
+
display: 'flex',
|
|
159
|
+
flexDirection: 'row',
|
|
160
|
+
alignItems: 'center',
|
|
161
|
+
gap: spacingVars['--spacing-1'],
|
|
162
|
+
marginBottom: spacingVars['--spacing-2'],
|
|
163
|
+
},
|
|
164
|
+
sheetRootHeading: {
|
|
165
|
+
marginInlineStart: spacingVars['--spacing-3'],
|
|
166
|
+
},
|
|
167
|
+
sheetViewHeading: {
|
|
168
|
+
outline: 'none',
|
|
169
|
+
},
|
|
124
170
|
});
|
|
125
171
|
|
|
126
172
|
// =============================================================================
|
|
@@ -164,6 +210,15 @@ interface ContextMenuBaseProps extends BaseProps {
|
|
|
164
210
|
isDisabled?: boolean;
|
|
165
211
|
/** Called when the menu opens or closes. */
|
|
166
212
|
onOpenChange?: (isOpen: boolean) => void;
|
|
213
|
+
/**
|
|
214
|
+
* Presentation policy for the menu.
|
|
215
|
+
* - `popover`: open beside the pointer position.
|
|
216
|
+
* - `bottom-sheet`: open as an action sheet.
|
|
217
|
+
* - `adaptive`: use a BottomSheet on compact coarse-pointer viewports and
|
|
218
|
+
* the cursor-positioned popover elsewhere.
|
|
219
|
+
* @default 'popover'
|
|
220
|
+
*/
|
|
221
|
+
presentation?: AdaptivePresentation;
|
|
167
222
|
'data-testid'?: string;
|
|
168
223
|
}
|
|
169
224
|
|
|
@@ -215,6 +270,7 @@ export function ContextMenu({
|
|
|
215
270
|
label: labelFromProps,
|
|
216
271
|
isDisabled = false,
|
|
217
272
|
onOpenChange,
|
|
273
|
+
presentation = 'popover',
|
|
218
274
|
ref,
|
|
219
275
|
className,
|
|
220
276
|
style,
|
|
@@ -225,6 +281,9 @@ export function ContextMenu({
|
|
|
225
281
|
}: ContextMenuProps) {
|
|
226
282
|
const t = useTranslator();
|
|
227
283
|
const label = labelFromProps ?? t('@astryx.contextMenu.label');
|
|
284
|
+
const backLabel = t('@astryx.dropdownMenu.back');
|
|
285
|
+
const resolvedPresentation = useAdaptivePresentation(presentation);
|
|
286
|
+
const usesBottomSheet = resolvedPresentation === 'bottom-sheet';
|
|
228
287
|
// Separate content props (union discriminant) from DOM pass-through attrs.
|
|
229
288
|
// The union means exactly one of items/menuContent exists in rest; destructure
|
|
230
289
|
// both so triggerProps contains only DOM-safe attributes.
|
|
@@ -250,8 +309,25 @@ export function ContextMenu({
|
|
|
250
309
|
// Element focused before the menu opened, restored when it closes so focus
|
|
251
310
|
// does not fall to <body> after Escape or outside-click dismissal.
|
|
252
311
|
const triggerFocusRef = useRef<HTMLElement | null>(null);
|
|
312
|
+
const sheetHeadingRef = useRef<HTMLHeadingElement>(null);
|
|
253
313
|
|
|
254
314
|
const [isOpen, setIsOpen] = useState(false);
|
|
315
|
+
const [submenuPath, setSubmenuPath] = useState<ContextMenuItemData[]>([]);
|
|
316
|
+
const currentSubmenu = submenuPath.at(-1);
|
|
317
|
+
const currentItems = currentSubmenu?.items ?? items;
|
|
318
|
+
const currentTitle = currentSubmenu?.label ?? label;
|
|
319
|
+
const sheetLabel = typeof currentTitle === 'string' ? currentTitle : label;
|
|
320
|
+
|
|
321
|
+
const updateOpenState = useCallback(
|
|
322
|
+
(nextIsOpen: boolean) => {
|
|
323
|
+
if (!nextIsOpen) {
|
|
324
|
+
setSubmenuPath([]);
|
|
325
|
+
}
|
|
326
|
+
setIsOpen(nextIsOpen);
|
|
327
|
+
onOpenChange?.(nextIsOpen);
|
|
328
|
+
},
|
|
329
|
+
[onOpenChange],
|
|
330
|
+
);
|
|
255
331
|
|
|
256
332
|
const layer = useLayer({
|
|
257
333
|
mode: 'context',
|
|
@@ -273,8 +349,35 @@ export function ContextMenu({
|
|
|
273
349
|
});
|
|
274
350
|
|
|
275
351
|
const closeMenu = useCallback(() => {
|
|
276
|
-
|
|
277
|
-
|
|
352
|
+
if (usesBottomSheet) {
|
|
353
|
+
updateOpenState(false);
|
|
354
|
+
} else {
|
|
355
|
+
layer.hide();
|
|
356
|
+
}
|
|
357
|
+
}, [layer, updateOpenState, usesBottomSheet]);
|
|
358
|
+
|
|
359
|
+
const handleBottomSheetSelect = useCallback(
|
|
360
|
+
(item: ContextMenuItemData) => {
|
|
361
|
+
if (item.isDisabled) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
item.onClick?.();
|
|
365
|
+
if (item.hasCloseOnSelect !== false) {
|
|
366
|
+
closeMenu();
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
[closeMenu],
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (!isOpen || submenuPath.length === 0) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const frame = requestAnimationFrame(() => {
|
|
377
|
+
sheetHeadingRef.current?.focus({preventScroll: true});
|
|
378
|
+
});
|
|
379
|
+
return () => cancelAnimationFrame(frame);
|
|
380
|
+
}, [isOpen, submenuPath.length]);
|
|
278
381
|
|
|
279
382
|
const {
|
|
280
383
|
listRef,
|
|
@@ -308,7 +411,7 @@ export function ContextMenu({
|
|
|
308
411
|
// opening right-click as a dismiss event. Handling it ourselves via
|
|
309
412
|
// mousedown avoids that race.
|
|
310
413
|
useEffect(() => {
|
|
311
|
-
if (!isOpen) {
|
|
414
|
+
if (!isOpen || usesBottomSheet) {
|
|
312
415
|
return;
|
|
313
416
|
}
|
|
314
417
|
const handleClickOutside = (e: MouseEvent) => {
|
|
@@ -321,14 +424,14 @@ export function ContextMenu({
|
|
|
321
424
|
return () => {
|
|
322
425
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
323
426
|
};
|
|
324
|
-
}, [isOpen, closeMenu, listRef]);
|
|
427
|
+
}, [isOpen, closeMenu, listRef, usesBottomSheet]);
|
|
325
428
|
|
|
326
429
|
// Dismiss on Escape from anywhere while open. The menu div's own onKeyDown
|
|
327
430
|
// only fires when focus is inside the menu; a document-level listener is
|
|
328
431
|
// kept as a reliable fallback Escape path (e.g. if focus has moved out of
|
|
329
432
|
// the menu). Guards against IME composition-cancel.
|
|
330
433
|
useEffect(() => {
|
|
331
|
-
if (!isOpen) {
|
|
434
|
+
if (!isOpen || usesBottomSheet) {
|
|
332
435
|
return;
|
|
333
436
|
}
|
|
334
437
|
const handleEscape = (e: KeyboardEvent) => {
|
|
@@ -347,7 +450,7 @@ export function ContextMenu({
|
|
|
347
450
|
return () => {
|
|
348
451
|
document.removeEventListener('keydown', handleEscape);
|
|
349
452
|
};
|
|
350
|
-
}, [isOpen, closeMenu]);
|
|
453
|
+
}, [isOpen, closeMenu, usesBottomSheet]);
|
|
351
454
|
|
|
352
455
|
const listKeyDown = useCallback(
|
|
353
456
|
(e: React.KeyboardEvent) => {
|
|
@@ -404,10 +507,14 @@ export function ContextMenu({
|
|
|
404
507
|
document.activeElement instanceof HTMLElement
|
|
405
508
|
? document.activeElement
|
|
406
509
|
: focusEl;
|
|
407
|
-
|
|
408
|
-
|
|
510
|
+
if (usesBottomSheet) {
|
|
511
|
+
updateOpenState(true);
|
|
512
|
+
} else {
|
|
513
|
+
layer.show();
|
|
514
|
+
requestAnimationFrame(() => focusFirst());
|
|
515
|
+
}
|
|
409
516
|
},
|
|
410
|
-
[layer, focusFirst],
|
|
517
|
+
[layer, focusFirst, updateOpenState, usesBottomSheet],
|
|
411
518
|
);
|
|
412
519
|
|
|
413
520
|
const handleContextMenu = useCallback(
|
|
@@ -465,6 +572,74 @@ export function ContextMenu({
|
|
|
465
572
|
const resolvedMenuContent =
|
|
466
573
|
itemsProp !== undefined ? renderDropdownItems(items) : menuContent;
|
|
467
574
|
|
|
575
|
+
const renderedMenu = (
|
|
576
|
+
<div
|
|
577
|
+
ref={listRef}
|
|
578
|
+
id={menuId}
|
|
579
|
+
role="menu"
|
|
580
|
+
data-autofocus={usesBottomSheet ? '' : undefined}
|
|
581
|
+
tabIndex={usesBottomSheet ? 0 : -1}
|
|
582
|
+
aria-label={label}
|
|
583
|
+
onKeyDown={listKeyDown}
|
|
584
|
+
onContextMenu={e => e.preventDefault()}
|
|
585
|
+
{...mergeProps(
|
|
586
|
+
themeProps('context-menu'),
|
|
587
|
+
stylex.props(usesBottomSheet ? styles.sheetMenu : styles.menu, xstyle),
|
|
588
|
+
className,
|
|
589
|
+
style,
|
|
590
|
+
)}>
|
|
591
|
+
<DropdownMenuContext value={contextValue}>
|
|
592
|
+
{resolvedMenuContent}
|
|
593
|
+
</DropdownMenuContext>
|
|
594
|
+
</div>
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const renderedBottomSheetContent =
|
|
598
|
+
itemsProp !== undefined ? (
|
|
599
|
+
<div
|
|
600
|
+
ref={listRef}
|
|
601
|
+
data-autofocus=""
|
|
602
|
+
tabIndex={0}
|
|
603
|
+
{...mergeProps(
|
|
604
|
+
themeProps('context-menu'),
|
|
605
|
+
stylex.props(styles.sheetMenu, xstyle),
|
|
606
|
+
className,
|
|
607
|
+
style,
|
|
608
|
+
)}>
|
|
609
|
+
<div {...stylex.props(styles.sheetHeader)}>
|
|
610
|
+
{submenuPath.length > 0 && (
|
|
611
|
+
<Button
|
|
612
|
+
label={backLabel}
|
|
613
|
+
variant="ghost"
|
|
614
|
+
size="sm"
|
|
615
|
+
icon={
|
|
616
|
+
<Icon icon="chevronLeft" size="sm" xstyle={rtlStyles.mirror} />
|
|
617
|
+
}
|
|
618
|
+
isIconOnly
|
|
619
|
+
onClick={() => setSubmenuPath(path => path.slice(0, -1))}
|
|
620
|
+
/>
|
|
621
|
+
)}
|
|
622
|
+
<Heading
|
|
623
|
+
ref={sheetHeadingRef}
|
|
624
|
+
level={3}
|
|
625
|
+
tabIndex={-1}
|
|
626
|
+
xstyle={[
|
|
627
|
+
styles.sheetViewHeading,
|
|
628
|
+
submenuPath.length === 0 && styles.sheetRootHeading,
|
|
629
|
+
]}>
|
|
630
|
+
{currentTitle}
|
|
631
|
+
</Heading>
|
|
632
|
+
</div>
|
|
633
|
+
<MenuBottomSheetActionList
|
|
634
|
+
items={currentItems}
|
|
635
|
+
onSelect={handleBottomSheetSelect}
|
|
636
|
+
onOpenSubmenu={item => setSubmenuPath(path => [...path, item])}
|
|
637
|
+
/>
|
|
638
|
+
</div>
|
|
639
|
+
) : (
|
|
640
|
+
renderedMenu
|
|
641
|
+
);
|
|
642
|
+
|
|
468
643
|
return (
|
|
469
644
|
<>
|
|
470
645
|
<div
|
|
@@ -494,29 +669,21 @@ export function ContextMenu({
|
|
|
494
669
|
/>
|
|
495
670
|
</div>
|
|
496
671
|
|
|
497
|
-
{
|
|
498
|
-
<
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
className,
|
|
509
|
-
style,
|
|
510
|
-
)}>
|
|
511
|
-
<DropdownMenuContext value={contextValue}>
|
|
512
|
-
{resolvedMenuContent}
|
|
513
|
-
</DropdownMenuContext>
|
|
514
|
-
</div>,
|
|
515
|
-
{
|
|
672
|
+
{usesBottomSheet ? (
|
|
673
|
+
<Suspense fallback={null}>
|
|
674
|
+
<LazyMenuBottomSheet
|
|
675
|
+
isOpen={isOpen}
|
|
676
|
+
onOpenChange={updateOpenState}
|
|
677
|
+
label={sheetLabel}>
|
|
678
|
+
{renderedBottomSheetContent}
|
|
679
|
+
</LazyMenuBottomSheet>
|
|
680
|
+
</Suspense>
|
|
681
|
+
) : (
|
|
682
|
+
layer.render(renderedMenu, {
|
|
516
683
|
placement: 'below',
|
|
517
684
|
alignment: 'start',
|
|
518
685
|
xstyle: [popoverXstyle, layerAnimations.below],
|
|
519
|
-
}
|
|
686
|
+
})
|
|
520
687
|
)}
|
|
521
688
|
</>
|
|
522
689
|
);
|
package/src/ContextMenu/index.ts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
|
|
4
|
+
const anatomy = [
|
|
5
|
+
{
|
|
6
|
+
name: 'Divider group',
|
|
7
|
+
required: true,
|
|
8
|
+
description:
|
|
9
|
+
'Separator group that arranges one or two rules around an optional label.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'Rule',
|
|
13
|
+
required: true,
|
|
14
|
+
description:
|
|
15
|
+
'Painted line segment; a second segment renders when a label is present.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'Label',
|
|
19
|
+
required: false,
|
|
20
|
+
description: 'Optional content displayed between two rule segments.',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
3
24
|
|
|
4
25
|
|
|
5
26
|
/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
|
|
@@ -47,6 +68,7 @@ export const docs = {
|
|
|
47
68
|
],
|
|
48
69
|
},
|
|
49
70
|
usage: {
|
|
71
|
+
anatomy,
|
|
50
72
|
description: 'A visual separator that divides content into distinct sections. Use to create clear boundaries between groups of related content, or to demarcate interactive regions within a layout.',
|
|
51
73
|
bestPractices: [
|
|
52
74
|
{ guidance: true, description: 'Use subtle dividers between related content sections and strong dividers for high-contrast boundaries.' },
|
|
@@ -97,6 +119,7 @@ export const docsZh = {
|
|
|
97
119
|
],
|
|
98
120
|
},
|
|
99
121
|
usage: {
|
|
122
|
+
anatomy,
|
|
100
123
|
description: 'A visual separator that divides content into distinct sections. Use to create clear boundaries between groups of related content, or to demarcate interactive regions within a layout.',
|
|
101
124
|
bestPractices: [
|
|
102
125
|
{ guidance: true, description: 'Use subtle dividers between related content sections and strong dividers for high-contrast boundaries.' },
|
|
@@ -110,6 +133,7 @@ export const docsZh = {
|
|
|
110
133
|
export const docsDense = {
|
|
111
134
|
description: 'visual separator w/ optional label, using Astryx design tokens',
|
|
112
135
|
usage: {
|
|
136
|
+
anatomy,
|
|
113
137
|
description: 'A visual separator that divides content into distinct sections. Use to create clear boundaries between groups of related content, or to demarcate interactive regions within a layout.',
|
|
114
138
|
bestPractices: [
|
|
115
139
|
{ guidance: true, description: 'Use subtle dividers between related content sections and strong dividers for high-contrast boundaries.' },
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 1
|
|
3
|
+
template_version: 3
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:Divider
|
|
6
|
+
authority: draft
|
|
7
|
+
archive_reason: null
|
|
8
|
+
superseded_by: null
|
|
9
|
+
approved_by: null
|
|
10
|
+
approved_at: null
|
|
11
|
+
owners: [cixzhang]
|
|
12
|
+
review_triggers: [theming]
|
|
13
|
+
verified_by:
|
|
14
|
+
[packages/core/src/Divider/Divider.test.tsx, scripts/check-knowledge.mjs]
|
|
15
|
+
families: []
|
|
16
|
+
design_specs: []
|
|
17
|
+
architecture: [architecture:component-theming-surface]
|
|
18
|
+
contributing: []
|
|
19
|
+
system_specs: []
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Divider component contract
|
|
23
|
+
|
|
24
|
+
## Intent
|
|
25
|
+
|
|
26
|
+
Divider presents a separator group as one rule, or as two rules around an
|
|
27
|
+
optional label. This draft records current consumer anatomy and theming
|
|
28
|
+
reachability without changing runtime behavior, styling, targets, or public API.
|
|
29
|
+
|
|
30
|
+
## Compatibility and migration
|
|
31
|
+
|
|
32
|
+
- Released default preserved: `yes`
|
|
33
|
+
- Compatibility class: additive documentation only; runtime, DOM, styling,
|
|
34
|
+
targets, aliases, and public API remain unchanged
|
|
35
|
+
- Controlled/uncontrolled behavior: not applicable
|
|
36
|
+
- Migration decision: none
|
|
37
|
+
|
|
38
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
39
|
+
|
|
40
|
+
## Ownership boundary
|
|
41
|
+
|
|
42
|
+
**Owns**
|
|
43
|
+
|
|
44
|
+
- The separator group, its current `divider` target, the painted rule segments,
|
|
45
|
+
and the optional label placement.
|
|
46
|
+
|
|
47
|
+
**Does not own / non-goals**
|
|
48
|
+
|
|
49
|
+
- The meaning of the content regions separated by the divider — owned by the
|
|
50
|
+
product callsite.
|
|
51
|
+
- Whether Rule or Label should gain public targets — unresolved by this factual
|
|
52
|
+
backfill.
|
|
53
|
+
|
|
54
|
+
## Public concepts
|
|
55
|
+
|
|
56
|
+
No new public concept is introduced. Consumer props and usage remain documented
|
|
57
|
+
in `Divider.doc.mjs`.
|
|
58
|
+
|
|
59
|
+
## Behavioral and layout contract
|
|
60
|
+
|
|
61
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
62
|
+
| --- | -------------------------------------------------------------------------------------------------------- | ------------------------------- | --------------------------------------------------- |
|
|
63
|
+
| FR1 | The current render contains one rule without a label and two rules with the optional label between them. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
|
|
64
|
+
| FR2 | The divider group carries the current `divider` target; Rule and Label currently carry no public target. | Current source, docs, and tests | Verified current behavior; theming intent unsettled |
|
|
65
|
+
|
|
66
|
+
### Allowed variation
|
|
67
|
+
|
|
68
|
+
- Orientation, visual weight, full-bleed layout, and label content may vary
|
|
69
|
+
without changing the three-part anatomy recorded here.
|
|
70
|
+
|
|
71
|
+
### Representative states
|
|
72
|
+
|
|
73
|
+
- Unlabelled horizontal and vertical dividers render one Rule.
|
|
74
|
+
- Labelled horizontal and vertical dividers render Label between two instances
|
|
75
|
+
of Rule.
|
|
76
|
+
|
|
77
|
+
### Transformation and precedence order
|
|
78
|
+
|
|
79
|
+
- No new layout or styling precedence rule is introduced.
|
|
80
|
+
|
|
81
|
+
### Performance and resources
|
|
82
|
+
|
|
83
|
+
- No new performance or resource rule is introduced.
|
|
84
|
+
|
|
85
|
+
## Accessibility contract
|
|
86
|
+
|
|
87
|
+
This draft does not change or extend Divider's existing separator naming or
|
|
88
|
+
orientation behavior.
|
|
89
|
+
|
|
90
|
+
## Design relationships
|
|
91
|
+
|
|
92
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
93
|
+
| ---------------- | ----------------------------------------------------------------- | ------------------------------ | -------------- | ------------------ |
|
|
94
|
+
| Divider group | Arranges the separator's rule segments and optional label. | Current source and public docs | Supporting | FR1, FR2 |
|
|
95
|
+
| Rule | Paints the separator line in the selected orientation and weight. | Current source and public docs | Supporting | FR1, FR2 |
|
|
96
|
+
| Label | Presents optional content between two rule segments. | Current source and public docs | Prominent | FR1, FR2 |
|
|
97
|
+
|
|
98
|
+
### Theming anatomy
|
|
99
|
+
|
|
100
|
+
<!-- anatomy-theming:v1 -->
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"Divider group": {"target": "divider"},
|
|
105
|
+
"Rule": {
|
|
106
|
+
"none": {
|
|
107
|
+
"reason": "unsettled: No current public target reaches this part"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"Label": {
|
|
111
|
+
"none": {
|
|
112
|
+
"reason": "unsettled: No current public target reaches this part"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The two `none` dispositions record current reachability while target exposure
|
|
119
|
+
remains unsettled. They do not decide that Rule or Label should remain without
|
|
120
|
+
public targets.
|
|
121
|
+
|
|
122
|
+
## Family and system relationships
|
|
123
|
+
|
|
124
|
+
- `architecture:component-theming-surface` owns anatomy qualification, target
|
|
125
|
+
mapping, and the difference between factual reachability and intended public
|
|
126
|
+
theming API.
|
|
127
|
+
|
|
128
|
+
## Verification map
|
|
129
|
+
|
|
130
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
131
|
+
| ------------------- | ------------------------------------------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------- | ----------------------- |
|
|
132
|
+
| FR1 | `Divider.test.tsx` structure and label suites | Labelled and unlabelled; both axes | Removing or reordering Rule or Label instances fails existing child-count and content assertions. | `audit:Divider/anatomy` |
|
|
133
|
+
| FR2 | Source inspection and current target inventories | Group, Rule, and Label | Adding or documenting a current target without updating the anatomy map fails repository checks. | `audit:Divider/theming` |
|
|
134
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target | Missing, extra, prefixed, stale, or alias-backed mappings fail repository validation. | `audit:Divider/theming` |
|
|
135
|
+
|
|
136
|
+
## Decision log
|
|
137
|
+
|
|
138
|
+
None. This draft records current facts and introduces no component-local design
|
|
139
|
+
or theming decision.
|
|
140
|
+
|
|
141
|
+
## Open questions
|
|
142
|
+
|
|
143
|
+
- **OQ1 — Should Rule or Label gain stable public theming targets?** (`human-api`)
|
|
144
|
+
Their target exposure is unsettled; the current lack of reachability does not
|
|
145
|
+
decide the answer.
|
|
146
|
+
|
|
147
|
+
## Content boundary
|
|
148
|
+
|
|
149
|
+
This file does not duplicate consumer prop tables/examples, current audit
|
|
150
|
+
results, implementation steps, or system rules. It links to their owners.
|