@astryxdesign/core 0.5.2-canary.c9c8564 → 0.5.2-canary.e03511b
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/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/locales/en.json +20 -0
- package/locales/pseudo.json +15 -0
- package/package.json +2 -2
- 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/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/CheckboxList/CheckboxList.doc.mjs +172 -31
- 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 +42 -0
- package/src/ComplexSelector/ComplexSelector.spec.md +189 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +61 -1
- package/src/ContextMenu/ContextMenu.spec.md +206 -0
- package/src/ContextMenu/ContextMenu.test.tsx +131 -1
- package/src/ContextMenu/ContextMenu.tsx +199 -32
- package/src/ContextMenu/index.ts +1 -0
- package/src/Divider/Divider.doc.mjs +24 -0
- package/src/Divider/Divider.spec.md +150 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +305 -40
- package/src/DropdownMenu/DropdownMenu.spec.md +245 -0
- 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/FieldStatus/FieldStatus.doc.mjs +21 -0
- package/src/FieldStatus/FieldStatus.spec.md +212 -0
- 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/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 +59 -0
- package/src/MoreMenu/MoreMenu.spec.md +221 -0
- package/src/MoreMenu/MoreMenu.test.tsx +47 -0
- package/src/MoreMenu/MoreMenu.tsx +13 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +128 -0
- 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/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/ProgressBar/ProgressBar.doc.mjs +198 -38
- package/src/ProgressBar/ProgressBar.spec.md +260 -0
- 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 +12 -0
- package/src/Selector/Selector.spec.md +202 -0
- 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 +184 -30
- 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 +40 -0
- package/src/Switch/Switch.spec.md +174 -0
- package/src/Table/Table.doc.mjs +102 -24
- package/src/Table/Table.spec.md +283 -0
- package/src/Text/Text.doc.mjs +190 -43
- package/src/Text/Text.spec.md +205 -0
- package/src/TextArea/TextArea.doc.mjs +255 -45
- package/src/TextArea/TextArea.spec.md +204 -0
- 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/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 +83 -1
- 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/theme/themingTargets.test.ts +91 -25
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 1
|
|
3
|
+
template_version: 3
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:ComplexSelector
|
|
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
|
+
[
|
|
15
|
+
packages/core/src/ComplexSelector/ComplexSelector.test.tsx,
|
|
16
|
+
packages/core/src/Field/Field.test.tsx,
|
|
17
|
+
packages/core/src/Icon/Icon.test.tsx,
|
|
18
|
+
scripts/check-knowledge.mjs,
|
|
19
|
+
]
|
|
20
|
+
families: [family:overlay-dismissal]
|
|
21
|
+
design_specs: []
|
|
22
|
+
architecture:
|
|
23
|
+
[architecture:component-theming-surface, architecture:layer-runtime]
|
|
24
|
+
contributing: []
|
|
25
|
+
system_specs: []
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# ComplexSelector component contract
|
|
29
|
+
|
|
30
|
+
## Intent
|
|
31
|
+
|
|
32
|
+
ComplexSelector renders a field-owned shell, an owned trigger, and a dialog
|
|
33
|
+
popup for caller-provided rich selection content. This draft records current
|
|
34
|
+
consumer anatomy and theming ownership without changing runtime behavior,
|
|
35
|
+
styling, targets, or public API.
|
|
36
|
+
|
|
37
|
+
## Compatibility and migration
|
|
38
|
+
|
|
39
|
+
- Released default preserved: `yes`
|
|
40
|
+
- Compatibility class: additive documentation only; runtime, DOM, styling,
|
|
41
|
+
targets, and public API remain unchanged
|
|
42
|
+
- Controlled/uncontrolled behavior: unchanged
|
|
43
|
+
- Migration decision: none
|
|
44
|
+
|
|
45
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
46
|
+
|
|
47
|
+
## Ownership boundary
|
|
48
|
+
|
|
49
|
+
**Owns**
|
|
50
|
+
|
|
51
|
+
- The trigger and its current `complex-selector` target.
|
|
52
|
+
- The trailing indicator icon and its current
|
|
53
|
+
`complex-selector-indicator-icon` target.
|
|
54
|
+
- The painted popup surface and its current `complex-selector-popup` target.
|
|
55
|
+
|
|
56
|
+
**Does not own / non-goals**
|
|
57
|
+
|
|
58
|
+
- Field-shell presentation — rendered by Field and themed through Field's
|
|
59
|
+
current `field` target.
|
|
60
|
+
- General Icon presentation — rendered by Icon for semantic icon names and icon
|
|
61
|
+
component types and themed through Icon's current `icon` target.
|
|
62
|
+
- Arbitrary ReactNode content supplied through `startIcon` — rendered directly
|
|
63
|
+
and owned by the product callsite.
|
|
64
|
+
- The selector-specific structure supplied through `children` — owned by the
|
|
65
|
+
product callsite.
|
|
66
|
+
- Shared layer lifecycle, positioning, and dismissal behavior.
|
|
67
|
+
|
|
68
|
+
## Public concepts
|
|
69
|
+
|
|
70
|
+
No new public concept is introduced. Consumer props and usage remain documented
|
|
71
|
+
in `ComplexSelector.doc.mjs`.
|
|
72
|
+
|
|
73
|
+
## Behavioral and layout contract
|
|
74
|
+
|
|
75
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
76
|
+
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------- |
|
|
77
|
+
| FR1 | The current render contains a Field, Trigger, Indicator icon, and mounted Popup; one optional start slot may contain either an Icon-rendered start icon or caller-rendered start content. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
|
|
78
|
+
| FR2 | Trigger, Indicator icon, and Popup carry `complex-selector`, `complex-selector-indicator-icon`, and `complex-selector-popup`, respectively. | Current source, docs, and tests | Verified current behavior; no target change |
|
|
79
|
+
| FR3 | Field delegates to Field's `field` target, and a semantic name or icon component in the start slot delegates to Icon's `icon` target. | Current source and owner tests | Verified current behavior; no target change |
|
|
80
|
+
| FR4 | Arbitrary ReactNode start content renders directly and carries no ComplexSelector-owned target. | Current source | Verified current behavior; no target change |
|
|
81
|
+
|
|
82
|
+
### Allowed variation
|
|
83
|
+
|
|
84
|
+
- Caller-provided popup content may vary without becoming a new
|
|
85
|
+
ComplexSelector-owned anatomy part or target.
|
|
86
|
+
- The optional start slot has two factual branches: semantic names and icon
|
|
87
|
+
component types render through Icon, while arbitrary ReactNode content renders
|
|
88
|
+
directly under caller ownership.
|
|
89
|
+
|
|
90
|
+
### Representative states
|
|
91
|
+
|
|
92
|
+
- Input and ghost variants use the same six-part anatomy and current targets.
|
|
93
|
+
- The start slot is absent when `startIcon` is omitted. When present, exactly
|
|
94
|
+
one of Icon-rendered start icon or Caller-rendered start content applies.
|
|
95
|
+
- The Popup remains mounted in both states: it is hidden while closed and shown
|
|
96
|
+
while open. The Indicator icon remains present and reflects collapsed or
|
|
97
|
+
expanded state on its current target.
|
|
98
|
+
|
|
99
|
+
### Transformation and precedence order
|
|
100
|
+
|
|
101
|
+
- No new value, open-state, positioning, or style precedence rule is introduced.
|
|
102
|
+
|
|
103
|
+
### Performance and resources
|
|
104
|
+
|
|
105
|
+
- No new performance or resource rule is introduced.
|
|
106
|
+
|
|
107
|
+
## Accessibility contract
|
|
108
|
+
|
|
109
|
+
This draft does not change or extend ComplexSelector's existing field, trigger,
|
|
110
|
+
dialog, focus, or keyboard behavior.
|
|
111
|
+
|
|
112
|
+
## Design relationships
|
|
113
|
+
|
|
114
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
115
|
+
| ----------------------------- | -------------------------------------------------------------------------------- | ------------------------------ | ----------------- | ------------------ |
|
|
116
|
+
| Field | Provides the field shell around the owned trigger. | Field component | Supporting | FR1, FR3 |
|
|
117
|
+
| Trigger | Displays the current value or placeholder and opens the popup. | Current source and public docs | Prominent | FR1, FR2 |
|
|
118
|
+
| Icon-rendered start icon | Optionally presents a semantic or component icon through Icon. | Icon component | Supporting | FR1, FR3 |
|
|
119
|
+
| Caller-rendered start content | Optionally presents arbitrary React content directly in the start slot. | Caller-supplied content | Context-dependent | FR1, FR4 |
|
|
120
|
+
| Indicator icon | Presents the trailing disclosure glyph and reflects collapsed or expanded state. | Current source and public docs | Supporting | FR1, FR2 |
|
|
121
|
+
| Popup | Keeps the dialog surface mounted, hidden while closed and painted while open. | Current source and public docs | Prominent | FR1, FR2 |
|
|
122
|
+
|
|
123
|
+
The Field and Icon delegations preserve their existing owners. Arbitrary
|
|
124
|
+
ReactNode start content remains caller-owned and receives no local target. The
|
|
125
|
+
local Indicator icon target remains distinct because it carries the selector's
|
|
126
|
+
expanded/collapsed state and rotation on the glyph itself. Caller-provided popup
|
|
127
|
+
content remains outside the owned anatomy inventory.
|
|
128
|
+
|
|
129
|
+
### Theming anatomy
|
|
130
|
+
|
|
131
|
+
<!-- anatomy-theming:v1 -->
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"Field": {"delegatesTo": {"owner": "component:Field", "target": "field"}},
|
|
136
|
+
"Trigger": {"target": "complex-selector"},
|
|
137
|
+
"Icon-rendered start icon": {
|
|
138
|
+
"delegatesTo": {"owner": "component:Icon", "target": "icon"}
|
|
139
|
+
},
|
|
140
|
+
"Caller-rendered start content": {
|
|
141
|
+
"none": {
|
|
142
|
+
"reason": "intentional: Arbitrary ReactNode content is caller-owned and receives no ComplexSelector target."
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"Indicator icon": {"target": "complex-selector-indicator-icon"},
|
|
146
|
+
"Popup": {"target": "complex-selector-popup"}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Family and system relationships
|
|
151
|
+
|
|
152
|
+
- `architecture:component-theming-surface` owns anatomy qualification, local
|
|
153
|
+
target mapping, and delegation rules.
|
|
154
|
+
- `architecture:layer-runtime` owns the current `usePopover` host, positioning,
|
|
155
|
+
native light-dismiss, and visibility reconciliation used by the Popup.
|
|
156
|
+
- `family:overlay-dismissal` owns shared Escape and platform-close ordering;
|
|
157
|
+
ComplexSelector participates through its composed Popover owner.
|
|
158
|
+
- Field and Icon retain ownership of their delegated targets and presentation.
|
|
159
|
+
|
|
160
|
+
## Verification map
|
|
161
|
+
|
|
162
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
163
|
+
| ------------------- | -------------------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------- | ------------------------------- |
|
|
164
|
+
| FR1 | `ComplexSelector.test.tsx` render, ghost-trigger, and popup suites | Default closed, ghost start branches, open Popup | Removing a documented part fails existing role, content, or structure assertions. | `audit:ComplexSelector/anatomy` |
|
|
165
|
+
| FR2 | Component target suites, source inspection, and theming target inventories | Closed/open Popup; expanded Indicator icon | Removing or renaming a current local target fails component assertions or target inventories. | `audit:ComplexSelector/theming` |
|
|
166
|
+
| FR3, FR4 | Icon owner tests and `renderIconSlot` source inspection | Semantic/component icon; arbitrary ReactNode | A branch gains the wrong owner, loses its target, or receives an invented local target. | `audit:ComplexSelector/theming` |
|
|
167
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current local targets | Missing, extra, prefixed, stale, or multiply assigned mappings fail repository validation. | `audit:ComplexSelector/theming` |
|
|
168
|
+
|
|
169
|
+
Current component tests directly assert the Trigger and Popup targets. Source
|
|
170
|
+
inspection confirms that non-lazy `useLayer` keeps the Popup mounted while
|
|
171
|
+
closed. The Indicator icon target is covered by source inspection and shared
|
|
172
|
+
target inventories rather than a dedicated component assertion. `renderIconSlot`
|
|
173
|
+
and Icon owner tests distinguish the Icon-rendered branch from arbitrary
|
|
174
|
+
caller-owned ReactNode content.
|
|
175
|
+
|
|
176
|
+
## Decision log
|
|
177
|
+
|
|
178
|
+
None. This draft records current facts and introduces no component-local design,
|
|
179
|
+
API, theming, or layer-system decision.
|
|
180
|
+
|
|
181
|
+
## Open questions
|
|
182
|
+
|
|
183
|
+
None.
|
|
184
|
+
|
|
185
|
+
## Content boundary
|
|
186
|
+
|
|
187
|
+
This file does not duplicate consumer prop tables, examples, caller-provided
|
|
188
|
+
popup structure, implementation steps, or shared layer and theming rules. It
|
|
189
|
+
links to their owners.
|
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
|
|
4
|
+
const anatomy = [
|
|
5
|
+
{
|
|
6
|
+
name: 'Trigger area',
|
|
7
|
+
required: true,
|
|
8
|
+
description:
|
|
9
|
+
'Caller-provided region that accepts right-click, keyboard context-menu, and long-press input.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'Pointer menu surface',
|
|
13
|
+
required: false,
|
|
14
|
+
description:
|
|
15
|
+
'Cursor-positioned menu panel used by the pointer presentation.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'Pointer action row',
|
|
19
|
+
required: false,
|
|
20
|
+
description:
|
|
21
|
+
'DropdownMenu-owned action, selectable option, or submenu row in the pointer presentation.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'Touch sheet frame',
|
|
25
|
+
required: false,
|
|
26
|
+
description:
|
|
27
|
+
'BottomSheet panel, content area, handle, and optional scrim that host touch actions.',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Touch menu surface',
|
|
31
|
+
required: false,
|
|
32
|
+
description:
|
|
33
|
+
'ContextMenu-owned content panel rendered inside the touch sheet frame.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'Touch action list',
|
|
37
|
+
required: false,
|
|
38
|
+
description: 'Spacious List that groups data-driven touch actions.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Touch action row',
|
|
42
|
+
required: false,
|
|
43
|
+
description:
|
|
44
|
+
'ListItem button used for a data-driven action or drill-in entry in the touch presentation.',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
2
48
|
/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
|
|
3
49
|
|
|
4
50
|
export const docs = {
|
|
@@ -32,7 +78,7 @@ export const docs = {
|
|
|
32
78
|
{
|
|
33
79
|
name: 'items',
|
|
34
80
|
type: 'ContextMenuOption[]',
|
|
35
|
-
description: 'Array of menu entries. Each entry is one of: an action item `{label, onClick?, icon?, isDisabled?, variant?}` (variant `"destructive"` renders it in the error color), a divider `{type: "divider"}`, or a section `{type: "section", title?, items: [...action items]}`.',
|
|
81
|
+
description: 'Array of menu entries. Each entry is one of: an action item `{label, onClick?, icon?, isDisabled?, variant?, items?}` (nested `items` open a flyout in popover presentation and drill into a new view in bottom-sheet presentation; variant `"destructive"` renders it in the error color), a divider `{type: "divider"}`, or a section `{type: "section", title?, items: [...action items]}`.',
|
|
36
82
|
required: true,
|
|
37
83
|
},
|
|
38
84
|
{
|
|
@@ -63,6 +109,12 @@ export const docs = {
|
|
|
63
109
|
type: '(isOpen: boolean) => void',
|
|
64
110
|
description: 'Callback fired when the menu opens or closes.',
|
|
65
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: 'presentation',
|
|
114
|
+
type: "'popover' | 'bottom-sheet' | 'adaptive'",
|
|
115
|
+
description: 'Presentation policy. `popover` opens at the pointer position, `bottom-sheet` always uses an action sheet, and `adaptive` uses the BottomSheet at 768px and below when the primary pointer is coarse.',
|
|
116
|
+
default: "'popover'",
|
|
117
|
+
},
|
|
66
118
|
{
|
|
67
119
|
name: 'isDisabled',
|
|
68
120
|
type: 'boolean',
|
|
@@ -74,11 +126,14 @@ export const docs = {
|
|
|
74
126
|
{name: 'ContextMenuItem'},
|
|
75
127
|
],
|
|
76
128
|
usage: {
|
|
129
|
+
anatomy,
|
|
77
130
|
description: 'A right-click context menu that appears at the cursor position. Use to provide contextual actions for specific elements or regions without cluttering the UI with visible buttons.',
|
|
78
131
|
bestPractices: [
|
|
79
132
|
{ guidance: true, description: 'Keep menu items concise and action-oriented; users expect quick access to contextual actions.' },
|
|
80
133
|
{ guidance: true, description: 'Use sections and dividers to group related actions when the menu has many items.' },
|
|
134
|
+
{ guidance: true, description: 'Use `presentation="adaptive"` when right-click should remain cursor-positioned on desktop while long-press opens a reachable BottomSheet on compact touch devices.' },
|
|
81
135
|
{ guidance: true, description: 'Ensure all context menu actions are also accessible via other UI elements for keyboard-only users.' },
|
|
136
|
+
{ guidance: true, description: 'Keep a visible MoreMenu or equivalent trigger for important mobile actions; long-press must not be the only route.' },
|
|
82
137
|
{ guidance: false, description: 'Use a ContextMenu as the only way to access important actions; not all users know to right-click.' },
|
|
83
138
|
{ guidance: false, description: 'Place more than 10–12 items in a single menu without grouping them into sections.' },
|
|
84
139
|
],
|
|
@@ -107,7 +162,9 @@ export const docsZh = {
|
|
|
107
162
|
bestPractices: [
|
|
108
163
|
{ guidance: true, description: '保持菜单项简洁和面向操作。' },
|
|
109
164
|
{ guidance: true, description: '有很多项时使用分组和分隔线。' },
|
|
165
|
+
{ guidance: true, description: '在桌面端需要光标定位、紧凑触控设备上需要 BottomSheet 时,使用 `presentation="adaptive"`。' },
|
|
110
166
|
{ guidance: true, description: '确保所有上下文菜单操作也可通过其他 UI 元素访问。' },
|
|
167
|
+
{ guidance: true, description: '重要的移动端操作还应提供可见的 MoreMenu 或同等入口,不要只依赖长按。' },
|
|
111
168
|
{ guidance: false, description: '将上下文菜单作为访问重要操作的唯一方式。' },
|
|
112
169
|
{ guidance: false, description: '在单个菜单中放置超过 10-12 个项而不分组。' },
|
|
113
170
|
],
|
|
@@ -118,11 +175,14 @@ export const docsZh = {
|
|
|
118
175
|
export const docsDense = {
|
|
119
176
|
description: 'right-click context menu at cursor position',
|
|
120
177
|
usage: {
|
|
178
|
+
anatomy,
|
|
121
179
|
description: 'A right-click context menu that appears at the cursor position. Use to provide contextual actions for specific elements or regions.',
|
|
122
180
|
bestPractices: [
|
|
123
181
|
{ guidance: true, description: 'Keep items concise and action-oriented.' },
|
|
124
182
|
{ guidance: true, description: 'Group related actions with sections and dividers.' },
|
|
183
|
+
{ guidance: true, description: 'Use adaptive presentation for cursor popover on desktop and BottomSheet on compact touch.' },
|
|
125
184
|
{ guidance: true, description: 'Ensure actions are also accessible via other UI elements.' },
|
|
185
|
+
{ guidance: true, description: 'Keep a visible mobile entry point for important actions.' },
|
|
126
186
|
{ guidance: false, description: 'Use as the only way to access important actions.' },
|
|
127
187
|
{ guidance: false, description: 'Place more than 10–12 items without grouping.' },
|
|
128
188
|
],
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
schema_version: 1
|
|
3
|
+
template_version: 3
|
|
4
|
+
kind: component
|
|
5
|
+
id: component:ContextMenu
|
|
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
|
+
[
|
|
15
|
+
packages/core/src/ContextMenu/ContextMenu.test.tsx,
|
|
16
|
+
packages/core/src/DropdownMenu/DropdownMenu.test.tsx,
|
|
17
|
+
packages/core/src/BottomSheet/BottomSheet.test.tsx,
|
|
18
|
+
packages/core/src/List/List.test.tsx,
|
|
19
|
+
scripts/check-knowledge.mjs,
|
|
20
|
+
]
|
|
21
|
+
families: [family:overlay-dismissal]
|
|
22
|
+
design_specs: []
|
|
23
|
+
architecture:
|
|
24
|
+
[
|
|
25
|
+
architecture:component-theming-surface,
|
|
26
|
+
architecture:interaction-modality,
|
|
27
|
+
architecture:layer-runtime,
|
|
28
|
+
]
|
|
29
|
+
contributing: []
|
|
30
|
+
system_specs: []
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# ContextMenu component contract
|
|
34
|
+
|
|
35
|
+
## Intent
|
|
36
|
+
|
|
37
|
+
ContextMenu presents actions for a caller-provided region in either a
|
|
38
|
+
cursor-positioned pointer menu or a BottomSheet-hosted touch action list. This
|
|
39
|
+
draft records current consumer anatomy, presentation-specific ownership, and
|
|
40
|
+
theming reachability without changing runtime behavior, styling, targets, or
|
|
41
|
+
public API.
|
|
42
|
+
|
|
43
|
+
## Compatibility and migration
|
|
44
|
+
|
|
45
|
+
- Released default preserved: `yes`
|
|
46
|
+
- Compatibility class: additive documentation only; runtime, DOM, styling,
|
|
47
|
+
targets, and public API remain unchanged
|
|
48
|
+
- Controlled/uncontrolled behavior: unchanged
|
|
49
|
+
- Migration decision: none
|
|
50
|
+
|
|
51
|
+
Consumer migration instructions belong in consumer docs and release notes.
|
|
52
|
+
|
|
53
|
+
## Ownership boundary
|
|
54
|
+
|
|
55
|
+
**Owns**
|
|
56
|
+
|
|
57
|
+
- The Trigger area's context-menu invocation wiring.
|
|
58
|
+
- The alternative Pointer menu surface and Touch menu surface and their shared
|
|
59
|
+
current `context-menu` target.
|
|
60
|
+
- Cursor-anchor placement and current open-state coordination across the two
|
|
61
|
+
presentation paths.
|
|
62
|
+
|
|
63
|
+
**Does not own / non-goals**
|
|
64
|
+
|
|
65
|
+
- Caller-provided Trigger area content.
|
|
66
|
+
- Pointer action-row presentation — delegated to `component:DropdownMenu`.
|
|
67
|
+
- The Touch sheet frame — delegated to `component:BottomSheet`.
|
|
68
|
+
- Data-driven Touch action-list and row presentation — delegated to
|
|
69
|
+
`component:List`.
|
|
70
|
+
- Shared layer lifecycle or dismissal policy.
|
|
71
|
+
|
|
72
|
+
## Public concepts
|
|
73
|
+
|
|
74
|
+
No new public concept is introduced. Consumer props, content modes, and
|
|
75
|
+
presentation policy remain documented in `ContextMenu.doc.mjs`.
|
|
76
|
+
|
|
77
|
+
## Behavioral and layout contract
|
|
78
|
+
|
|
79
|
+
| ID | Candidate invariant | Basis | Draft review state |
|
|
80
|
+
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------- |
|
|
81
|
+
| FR1 | Every render contains a caller-provided Trigger area. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
|
|
82
|
+
| FR2 | The current `context-menu` target remains on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | Current source, docs, and tests | Verified current inventory; no target change |
|
|
83
|
+
| FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | Verified current delegation; no ownership change |
|
|
84
|
+
| FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | Verified current branches; no behavior change |
|
|
85
|
+
|
|
86
|
+
### Allowed variation
|
|
87
|
+
|
|
88
|
+
- **AV1 — Invocation.** Right-click, keyboard context-menu invocation, and
|
|
89
|
+
long-press may open the current resolved presentation without changing
|
|
90
|
+
anatomy ownership.
|
|
91
|
+
- **AV2 — Presentation.** `popover`, `bottom-sheet`, and resolved `adaptive`
|
|
92
|
+
presentation may select the current pointer or touch anatomy.
|
|
93
|
+
- **AV3 — Content mode.** Data-driven pointer content renders through
|
|
94
|
+
DropdownMenu helpers; compound content remains caller-composed. Data-driven
|
|
95
|
+
touch content renders through List.
|
|
96
|
+
|
|
97
|
+
### Representative states
|
|
98
|
+
|
|
99
|
+
| State | Required invariant | Allowed variation |
|
|
100
|
+
| --------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
101
|
+
| Pointer data menu | Trigger area opens Pointer menu surface with DropdownMenu-owned rows. | Cursor position, row content, width, and nested flyouts may vary. |
|
|
102
|
+
| Pointer compound menu | Trigger area opens the same surface around caller-supplied menu content. | Caller chooses supported DropdownMenu interior components. |
|
|
103
|
+
| Touch data menu | Trigger area opens Touch sheet frame and Touch menu surface containing List-owned actions. | Heading, sections, dividers, icons, and drill-in depth may vary. |
|
|
104
|
+
| Touch compound menu | Trigger area opens Touch sheet frame around the ContextMenu surface and caller-supplied interior. | Caller-supplied content remains responsible for its own structure. |
|
|
105
|
+
| Disabled trigger | Trigger area remains rendered and the native browser context menu is not suppressed by ContextMenu. | Caller-provided content remains unchanged. |
|
|
106
|
+
|
|
107
|
+
### Transformation and precedence order
|
|
108
|
+
|
|
109
|
+
- No new presentation-resolution, cursor-position, long-press, item-rendering,
|
|
110
|
+
or styling precedence rule is introduced.
|
|
111
|
+
|
|
112
|
+
### Performance and resources
|
|
113
|
+
|
|
114
|
+
- No new loading, listener, measurement, or render constraint is introduced.
|
|
115
|
+
|
|
116
|
+
## Accessibility contract
|
|
117
|
+
|
|
118
|
+
This draft does not change or extend ContextMenu's existing menu and dialog
|
|
119
|
+
naming, keyboard invocation, long-press path, focus movement, item semantics, or
|
|
120
|
+
dismissal behavior.
|
|
121
|
+
|
|
122
|
+
## Design relationships
|
|
123
|
+
|
|
124
|
+
| Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
|
|
125
|
+
| -------------------- | --------------------------------------------------------------------------------- | ------------------------------ | ----------------- | ------------------ |
|
|
126
|
+
| Trigger area | Defines the caller-owned region that accepts context-menu input. | Caller-supplied content | Context-dependent | FR1, FR2 |
|
|
127
|
+
| Pointer menu surface | Paints the menu at the current cursor anchor. | Current source and public docs | Prominent | FR1, FR2 |
|
|
128
|
+
| Pointer action row | Presents an action, selectable option, or nested-menu entry through DropdownMenu. | `component:DropdownMenu` | Prominent | FR1, FR3, FR4 |
|
|
129
|
+
| Touch sheet frame | Supplies the touch panel, scrolling area, handle, and optional scrim. | `component:BottomSheet` | Prominent | FR1, FR3 |
|
|
130
|
+
| Touch menu surface | Arranges ContextMenu-owned heading and content inside the sheet. | Current source and public docs | Prominent | FR1, FR2 |
|
|
131
|
+
| Touch action list | Groups data-driven touch actions using the spacious List presentation. | `component:List` | Prominent | FR1, FR3, FR4 |
|
|
132
|
+
| Touch action row | Presents one data-driven touch action or drill-in entry using ListItem. | `component:List` | Prominent | FR1, FR3, FR4 |
|
|
133
|
+
|
|
134
|
+
The `context-menu` target intentionally appears on both alternative painted
|
|
135
|
+
menu surfaces. Trigger area content stays caller-owned and the zero-size cursor
|
|
136
|
+
anchor is positioning infrastructure rather than consumer anatomy.
|
|
137
|
+
|
|
138
|
+
### Theming anatomy
|
|
139
|
+
|
|
140
|
+
<!-- anatomy-theming:v1 -->
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"Trigger area": {
|
|
145
|
+
"none": {
|
|
146
|
+
"reason": "intentional: Caller-provided trigger content remains caller-owned and the ContextMenu wrapper has no public target."
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"Pointer menu surface": {"target": "context-menu"},
|
|
150
|
+
"Pointer action row": {
|
|
151
|
+
"delegatesTo": {
|
|
152
|
+
"owner": "component:DropdownMenu",
|
|
153
|
+
"target": "dropdown-menu-item"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"Touch sheet frame": {
|
|
157
|
+
"delegatesTo": {"owner": "component:BottomSheet", "target": "bottom-sheet"}
|
|
158
|
+
},
|
|
159
|
+
"Touch menu surface": {"target": "context-menu"},
|
|
160
|
+
"Touch action list": {
|
|
161
|
+
"delegatesTo": {"owner": "component:List", "target": "list"}
|
|
162
|
+
},
|
|
163
|
+
"Touch action row": {
|
|
164
|
+
"delegatesTo": {"owner": "component:List", "target": "list-item"}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Family and system relationships
|
|
170
|
+
|
|
171
|
+
- `architecture:component-theming-surface` owns anatomy qualification, local
|
|
172
|
+
target mapping, presentation-specific rows, and delegation rules.
|
|
173
|
+
- `architecture:interaction-modality` owns the shared right-click, keyboard,
|
|
174
|
+
long-press, pointer, touch, and adaptive-presentation boundaries; this draft
|
|
175
|
+
changes none of them.
|
|
176
|
+
- `architecture:layer-runtime` owns the current context-mode `useLayer` host and
|
|
177
|
+
cursor-anchor positioning. ContextMenu currently retains local outside-click
|
|
178
|
+
and Escape listeners; touch hosting delegates to BottomSheet.
|
|
179
|
+
- `family:overlay-dismissal` owns shared Escape and platform-close ordering and
|
|
180
|
+
records ContextMenu and BottomSheet as current local-only adoption gaps. This
|
|
181
|
+
anatomy backfill does not migrate either path.
|
|
182
|
+
|
|
183
|
+
## Verification map
|
|
184
|
+
|
|
185
|
+
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
|
|
186
|
+
| ------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------- |
|
|
187
|
+
| FR1, FR4 | `ContextMenu.test.tsx` pointer, long-press, bottom-sheet, adaptive, compound, and drill-in suites | Pointer data/compound and touch data/compound | Collapsing presentation-specific parts or content owners fails structure, role, or behavior tests. | `audit:ContextMenu/anatomy` |
|
|
188
|
+
| FR2 | ContextMenu target tests, source inspection, and theming target inventories | Pointer and touch menu surfaces | Moving `context-menu` to the trigger or removing it from a painted branch fails evidence. | `audit:ContextMenu/theming` |
|
|
189
|
+
| FR3 | ContextMenu, DropdownMenu, BottomSheet, and List owner tests | Pointer rows, touch frame, touch list/rows | A composed part loses its owner target or is documented as a new ContextMenu target. | `audit:ContextMenu/theming` |
|
|
190
|
+
| Layer relationships | `ContextMenu.test.tsx` and current layer/dismissal architecture records | Outside click, Escape, context anchor, sheet | Documentation claims shared dismissal where current source retains local behavior. | `audit:ContextMenu/behavior` |
|
|
191
|
+
| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target | Missing, extra, prefixed, stale, or unclassified mappings fail repository validation. | `audit:ContextMenu/theming` |
|
|
192
|
+
|
|
193
|
+
## Decision log
|
|
194
|
+
|
|
195
|
+
None. This draft records current facts and introduces no component-local design,
|
|
196
|
+
API, theming, modality, or layer-system decision.
|
|
197
|
+
|
|
198
|
+
## Open questions
|
|
199
|
+
|
|
200
|
+
None.
|
|
201
|
+
|
|
202
|
+
## Content boundary
|
|
203
|
+
|
|
204
|
+
This file does not duplicate consumer prop tables, menu examples, cursor and
|
|
205
|
+
long-press algorithms, implementation steps, or shared modality, layer,
|
|
206
|
+
dismissal, and theming rules. It links to their owners.
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
12
|
-
import {render, screen, fireEvent, act} from '@testing-library/react';
|
|
12
|
+
import {render, screen, fireEvent, act, waitFor} from '@testing-library/react';
|
|
13
13
|
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import {readFileSync} from 'node:fs';
|
|
14
15
|
import {ContextMenu} from './ContextMenu';
|
|
15
16
|
import {
|
|
16
17
|
ContextMenuItem,
|
|
@@ -23,6 +24,30 @@ import {DropdownMenuItem} from '../DropdownMenu/DropdownMenuItem';
|
|
|
23
24
|
import {Divider} from '../Divider';
|
|
24
25
|
|
|
25
26
|
beforeEach(() => {
|
|
27
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function (
|
|
28
|
+
this: HTMLDialogElement,
|
|
29
|
+
) {
|
|
30
|
+
this.setAttribute('open', '');
|
|
31
|
+
});
|
|
32
|
+
HTMLDialogElement.prototype.show = vi.fn(function (this: HTMLDialogElement) {
|
|
33
|
+
this.setAttribute('open', '');
|
|
34
|
+
});
|
|
35
|
+
HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
|
|
36
|
+
this.removeAttribute('open');
|
|
37
|
+
});
|
|
38
|
+
vi.stubGlobal(
|
|
39
|
+
'matchMedia',
|
|
40
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
41
|
+
matches: false,
|
|
42
|
+
media: query,
|
|
43
|
+
onchange: null,
|
|
44
|
+
addEventListener: vi.fn(),
|
|
45
|
+
removeEventListener: vi.fn(),
|
|
46
|
+
addListener: vi.fn(),
|
|
47
|
+
removeListener: vi.fn(),
|
|
48
|
+
dispatchEvent: vi.fn(),
|
|
49
|
+
})),
|
|
50
|
+
);
|
|
26
51
|
HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
|
|
27
52
|
this.setAttribute('popover-open', '');
|
|
28
53
|
const event = new Event('toggle', {bubbles: false});
|
|
@@ -123,6 +148,100 @@ describe('ContextMenu', () => {
|
|
|
123
148
|
expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
|
|
124
149
|
});
|
|
125
150
|
|
|
151
|
+
it('opens the real menu content in a BottomSheet when requested', async () => {
|
|
152
|
+
render(
|
|
153
|
+
<ContextMenu
|
|
154
|
+
presentation="bottom-sheet"
|
|
155
|
+
label="Row actions"
|
|
156
|
+
items={[{label: 'Edit'}]}>
|
|
157
|
+
<div>Right-click me</div>
|
|
158
|
+
</ContextMenu>,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
fireEvent.contextMenu(screen.getByText('Right-click me'));
|
|
162
|
+
await waitFor(() =>
|
|
163
|
+
expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalledOnce(),
|
|
164
|
+
);
|
|
165
|
+
const dialog = screen.getByRole('dialog', {name: 'Row actions'});
|
|
166
|
+
const actionList = dialog.querySelector('[data-autofocus]');
|
|
167
|
+
expect(actionList).not.toBeNull();
|
|
168
|
+
await waitFor(() => expect(actionList).toHaveFocus());
|
|
169
|
+
expect(screen.getByRole('button', {name: 'Edit'})).not.toHaveFocus();
|
|
170
|
+
expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('drills into nested data items inside the BottomSheet', async () => {
|
|
174
|
+
const user = userEvent.setup();
|
|
175
|
+
const onMove = vi.fn();
|
|
176
|
+
|
|
177
|
+
render(
|
|
178
|
+
<ContextMenu
|
|
179
|
+
presentation="bottom-sheet"
|
|
180
|
+
label="Row actions"
|
|
181
|
+
items={[
|
|
182
|
+
{
|
|
183
|
+
label: 'Move to project',
|
|
184
|
+
items: [{label: 'Apollo launch', onClick: onMove}],
|
|
185
|
+
},
|
|
186
|
+
]}>
|
|
187
|
+
<div>Right-click me</div>
|
|
188
|
+
</ContextMenu>,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
fireEvent.contextMenu(screen.getByText('Right-click me'));
|
|
192
|
+
await user.click(screen.getByRole('button', {name: 'Move to project'}));
|
|
193
|
+
|
|
194
|
+
const submenuHeading = screen.getByRole('heading', {
|
|
195
|
+
name: 'Move to project',
|
|
196
|
+
});
|
|
197
|
+
expect(submenuHeading).toBeInTheDocument();
|
|
198
|
+
await waitFor(() => expect(submenuHeading).toHaveFocus());
|
|
199
|
+
expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
|
|
200
|
+
|
|
201
|
+
await user.click(screen.getByRole('button', {name: 'Apollo launch'}));
|
|
202
|
+
expect(onMove).toHaveBeenCalledOnce();
|
|
203
|
+
await waitFor(() =>
|
|
204
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('keeps only the top content padding in a menu BottomSheet', () => {
|
|
209
|
+
const source = readFileSync(
|
|
210
|
+
'packages/core/src/DropdownMenu/MenuBottomSheet.tsx',
|
|
211
|
+
'utf8',
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(source).toContain('paddingBlockStart={4}');
|
|
215
|
+
expect(source).toContain('paddingBlockEnd={0}');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('uses the BottomSheet for adaptive presentation on compact touch', async () => {
|
|
219
|
+
vi.stubGlobal(
|
|
220
|
+
'matchMedia',
|
|
221
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
222
|
+
matches: query === '(max-width: 768px) and (pointer: coarse)',
|
|
223
|
+
media: query,
|
|
224
|
+
onchange: null,
|
|
225
|
+
addEventListener: vi.fn(),
|
|
226
|
+
removeEventListener: vi.fn(),
|
|
227
|
+
addListener: vi.fn(),
|
|
228
|
+
removeListener: vi.fn(),
|
|
229
|
+
dispatchEvent: vi.fn(),
|
|
230
|
+
})),
|
|
231
|
+
);
|
|
232
|
+
render(
|
|
233
|
+
<ContextMenu presentation="adaptive" items={[{label: 'Edit'}]}>
|
|
234
|
+
<div>Right-click me</div>
|
|
235
|
+
</ContextMenu>,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
fireEvent.contextMenu(screen.getByText('Right-click me'));
|
|
239
|
+
await waitFor(() =>
|
|
240
|
+
expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalledOnce(),
|
|
241
|
+
);
|
|
242
|
+
expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
|
|
243
|
+
});
|
|
244
|
+
|
|
126
245
|
it('closes on Escape even when opened without auto-focus', () => {
|
|
127
246
|
render(
|
|
128
247
|
<ContextMenu items={[{label: 'Item 1'}]}>
|
|
@@ -273,6 +392,17 @@ describe('ContextMenu', () => {
|
|
|
273
392
|
}
|
|
274
393
|
});
|
|
275
394
|
|
|
395
|
+
it('suppresses native text selection on the long-press trigger', () => {
|
|
396
|
+
const source = readFileSync(
|
|
397
|
+
'packages/core/src/ContextMenu/ContextMenu.tsx',
|
|
398
|
+
'utf8',
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
expect(source).toMatch(
|
|
402
|
+
/trigger:[\s\S]*?WebkitTouchCallout: 'none',[\s\S]*?WebkitUserSelect: 'none',[\s\S]*?userSelect: 'none'/,
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
|
|
276
406
|
it('cancels the long-press when the finger moves past the threshold', () => {
|
|
277
407
|
vi.useFakeTimers();
|
|
278
408
|
try {
|