@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.
Files changed (270) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/BottomSheet/BottomSheet.d.ts +3 -1
  3. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  4. package/dist/BottomSheet/BottomSheet.js +4 -3
  5. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
  6. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
  7. package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
  8. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  9. package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
  10. package/dist/Button/Button.d.ts +2 -1
  11. package/dist/Button/Button.d.ts.map +1 -1
  12. package/dist/Button/Button.js +11 -4
  13. package/dist/Carousel/Carousel.d.ts.map +1 -1
  14. package/dist/Carousel/Carousel.js +2 -2
  15. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  16. package/dist/Chat/ChatToolCalls.js +34 -17
  17. package/dist/ContextMenu/ContextMenu.d.ts +14 -3
  18. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  19. package/dist/ContextMenu/ContextMenu.js +149 -26
  20. package/dist/ContextMenu/index.d.ts +1 -0
  21. package/dist/ContextMenu/index.d.ts.map +1 -1
  22. package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
  23. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  24. package/dist/DropdownMenu/DropdownMenu.js +340 -26
  25. package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
  26. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  27. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  28. package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
  29. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  31. package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
  32. package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
  33. package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
  34. package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
  35. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
  36. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
  37. package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
  38. package/dist/DropdownMenu/index.d.ts +2 -1
  39. package/dist/DropdownMenu/index.d.ts.map +1 -1
  40. package/dist/DropdownMenu/menuWidth.d.ts +14 -0
  41. package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
  42. package/dist/DropdownMenu/menuWidth.js +35 -0
  43. package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
  44. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
  45. package/dist/DropdownMenu/useMenuOverflow.js +60 -0
  46. package/dist/MoreMenu/MoreMenu.d.ts +8 -2
  47. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  48. package/dist/MoreMenu/MoreMenu.js +2 -0
  49. package/dist/MultiSelector/MultiSelector.d.ts +11 -1
  50. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  51. package/dist/MultiSelector/MultiSelector.js +97 -68
  52. package/dist/MultiSelector/index.d.ts +1 -1
  53. package/dist/MultiSelector/index.d.ts.map +1 -1
  54. package/dist/Popover/Popover.d.ts +10 -6
  55. package/dist/Popover/Popover.d.ts.map +1 -1
  56. package/dist/Popover/Popover.js +141 -16
  57. package/dist/Popover/usePopover.d.ts +4 -0
  58. package/dist/Popover/usePopover.d.ts.map +1 -1
  59. package/dist/Popover/usePopover.js +52 -5
  60. package/dist/Selector/Selector.d.ts +10 -0
  61. package/dist/Selector/Selector.d.ts.map +1 -1
  62. package/dist/Selector/Selector.js +106 -80
  63. package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
  64. package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
  65. package/dist/Selector/SelectorBottomSheet.js +83 -0
  66. package/dist/Selector/index.d.ts +1 -1
  67. package/dist/Selector/index.d.ts.map +1 -1
  68. package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
  69. package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
  70. package/dist/Selector/selectorPresentation.stylex.js +21 -0
  71. package/dist/Selector/useSelectorPresentation.d.ts +32 -0
  72. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
  73. package/dist/Selector/useSelectorPresentation.js +90 -0
  74. package/dist/Toast/useToastGesture.d.ts.map +1 -1
  75. package/dist/Toast/useToastGesture.js +46 -9
  76. package/dist/astryx.css +19 -1
  77. package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
  78. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
  79. package/dist/hooks/useAdaptivePresentation.js +19 -0
  80. package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
  81. package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
  82. package/dist/hooks/useFocusReturnVisibility.js +35 -0
  83. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  84. package/dist/hooks/useFocusTrap.js +12 -0
  85. package/dist/i18n/InternationalizationContext.d.ts +2 -0
  86. package/dist/i18n/InternationalizationContext.d.ts.map +1 -1
  87. package/dist/i18n/InternationalizationContext.js +3 -1
  88. package/dist/i18n/InternationalizationProvider.d.ts.map +1 -1
  89. package/dist/i18n/InternationalizationProvider.js +3 -1
  90. package/dist/i18n/resolve.d.ts +1 -1
  91. package/dist/i18n/resolve.d.ts.map +1 -1
  92. package/dist/i18n/resolve.js +37 -38
  93. package/dist/i18n/useTranslator.d.ts.map +1 -1
  94. package/dist/i18n/useTranslator.js +2 -3
  95. package/locales/af-ZA.json +0 -4
  96. package/locales/ar-SA.json +0 -4
  97. package/locales/ca-ES.json +0 -4
  98. package/locales/cs-CZ.json +0 -4
  99. package/locales/da-DK.json +0 -4
  100. package/locales/de-DE.json +0 -4
  101. package/locales/el-GR.json +0 -4
  102. package/locales/en.json +20 -0
  103. package/locales/es-ES.json +0 -4
  104. package/locales/fi-FI.json +0 -4
  105. package/locales/fr-FR.json +0 -4
  106. package/locales/he-IL.json +0 -4
  107. package/locales/hu-HU.json +0 -4
  108. package/locales/it-IT.json +0 -4
  109. package/locales/ja-JP.json +0 -4
  110. package/locales/ko-KR.json +0 -4
  111. package/locales/nl-NL.json +0 -4
  112. package/locales/no-NO.json +0 -4
  113. package/locales/pl-PL.json +0 -4
  114. package/locales/pseudo.json +15 -0
  115. package/locales/pt-BR.json +0 -4
  116. package/locales/pt-PT.json +0 -4
  117. package/locales/ro-RO.json +0 -4
  118. package/locales/ru-RU.json +0 -4
  119. package/locales/sr-SP.json +0 -4
  120. package/locales/sv-SE.json +0 -4
  121. package/locales/tr-TR.json +0 -4
  122. package/locales/uk-UA.json +0 -4
  123. package/locales/vi-VN.json +0 -4
  124. package/locales/zh-CN.json +0 -4
  125. package/locales/zh-TW.json +0 -4
  126. package/package.json +3 -3
  127. package/src/BottomSheet/BottomSheet.doc.mjs +37 -0
  128. package/src/BottomSheet/BottomSheet.spec.md +177 -0
  129. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  130. package/src/BottomSheet/BottomSheet.tsx +7 -4
  131. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
  132. package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
  133. package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
  134. package/src/Button/Button.doc.mjs +56 -0
  135. package/src/Button/Button.test.tsx +10 -0
  136. package/src/Button/Button.tsx +13 -5
  137. package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
  138. package/src/Calendar/Calendar.doc.mjs +1 -1
  139. package/src/Carousel/Carousel.test.tsx +83 -0
  140. package/src/Carousel/Carousel.tsx +8 -2
  141. package/src/Chat/ChatToolCalls.test.tsx +44 -0
  142. package/src/Chat/ChatToolCalls.tsx +36 -15
  143. package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
  144. package/src/CheckboxList/CheckboxList.doc.mjs +173 -32
  145. package/src/CheckboxList/CheckboxList.spec.md +214 -0
  146. package/src/CommandPalette/CommandPalette.doc.mjs +72 -0
  147. package/src/CommandPalette/CommandPalette.spec.md +223 -0
  148. package/src/ComplexSelector/ComplexSelector.doc.mjs +44 -2
  149. package/src/ComplexSelector/ComplexSelector.spec.md +189 -0
  150. package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
  151. package/src/ContextMenu/ContextMenu.test.tsx +131 -1
  152. package/src/ContextMenu/ContextMenu.tsx +199 -32
  153. package/src/ContextMenu/index.ts +1 -0
  154. package/src/DateInput/DateInput.doc.mjs +1 -1
  155. package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
  156. package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
  157. package/src/Divider/Divider.doc.mjs +24 -0
  158. package/src/Divider/Divider.spec.md +150 -0
  159. package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
  160. package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
  161. package/src/DropdownMenu/DropdownMenu.tsx +459 -28
  162. package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
  163. package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
  164. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
  165. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
  166. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
  167. package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
  168. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
  169. package/src/DropdownMenu/index.ts +2 -0
  170. package/src/DropdownMenu/menuWidth.ts +51 -0
  171. package/src/DropdownMenu/useMenuOverflow.ts +77 -0
  172. package/src/Field/Field.doc.mjs +1 -1
  173. package/src/FieldStatus/FieldStatus.doc.mjs +22 -1
  174. package/src/FieldStatus/FieldStatus.spec.md +212 -0
  175. package/src/FileInput/FileInput.doc.mjs +1 -1
  176. package/src/Grid/Grid.doc.mjs +19 -0
  177. package/src/Grid/Grid.spec.md +161 -0
  178. package/src/Icon/Icon.doc.mjs +196 -48
  179. package/src/Icon/Icon.spec.md +191 -0
  180. package/src/IconButton/IconButton.doc.mjs +38 -0
  181. package/src/Indicator/Indicator.doc.mjs +1 -1
  182. package/src/InputGroup/InputGroup.doc.mjs +1 -1
  183. package/src/Kbd/Kbd.doc.mjs +18 -0
  184. package/src/Kbd/Kbd.spec.md +141 -0
  185. package/src/Layout/Layout.doc.mjs +32 -0
  186. package/src/Layout/Layout.spec.md +199 -0
  187. package/src/Lightbox/Lightbox.doc.mjs +149 -33
  188. package/src/Lightbox/Lightbox.spec.md +197 -0
  189. package/src/Markdown/Markdown.doc.mjs +60 -0
  190. package/src/Markdown/Markdown.spec.md +200 -0
  191. package/src/MobileNav/MobileNav.doc.mjs +140 -32
  192. package/src/MobileNav/MobileNav.spec.md +208 -0
  193. package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
  194. package/src/MoreMenu/MoreMenu.test.tsx +47 -0
  195. package/src/MoreMenu/MoreMenu.tsx +13 -1
  196. package/src/MultiSelector/MultiSelector.doc.mjs +129 -1
  197. package/src/MultiSelector/MultiSelector.spec.md +266 -0
  198. package/src/MultiSelector/MultiSelector.test.tsx +127 -0
  199. package/src/MultiSelector/MultiSelector.tsx +121 -73
  200. package/src/MultiSelector/index.ts +1 -0
  201. package/src/NavIcon/NavIcon.doc.mjs +17 -0
  202. package/src/NavIcon/NavIcon.spec.md +133 -0
  203. package/src/NavMenu/NavHeadingMenu.spec.md +205 -0
  204. package/src/NavMenu/NavMenu.doc.mjs +37 -0
  205. package/src/NumberInput/NumberInput.doc.mjs +1 -1
  206. package/src/Outline/Outline.doc.mjs +35 -0
  207. package/src/Outline/Outline.spec.md +161 -0
  208. package/src/OverflowList/OverflowList.doc.mjs +24 -0
  209. package/src/OverflowList/OverflowList.spec.md +156 -0
  210. package/src/Pagination/Pagination.doc.mjs +74 -0
  211. package/src/Pagination/Pagination.spec.md +225 -0
  212. package/src/Popover/Popover.doc.mjs +11 -11
  213. package/src/Popover/Popover.test.tsx +397 -2
  214. package/src/Popover/Popover.tsx +228 -21
  215. package/src/Popover/usePopover.tsx +92 -15
  216. package/src/PowerSearch/PowerSearch.doc.mjs +1 -1
  217. package/src/ProgressBar/ProgressBar.doc.mjs +198 -38
  218. package/src/ProgressBar/ProgressBar.spec.md +260 -0
  219. package/src/RadioList/RadioList.doc.mjs +1 -1
  220. package/src/Resizable/Resizable.doc.mjs +59 -16
  221. package/src/Resizable/Resizable.spec.md +194 -0
  222. package/src/Section/Section.doc.mjs +17 -0
  223. package/src/Section/Section.spec.md +138 -0
  224. package/src/SegmentedControl/SegmentedControl.doc.mjs +83 -0
  225. package/src/SegmentedControl/SegmentedControl.spec.md +151 -0
  226. package/src/Selector/Selector.doc.mjs +13 -1
  227. package/src/Selector/Selector.test.tsx +135 -0
  228. package/src/Selector/Selector.tsx +141 -82
  229. package/src/Selector/SelectorBottomSheet.tsx +97 -0
  230. package/src/Selector/index.ts +1 -0
  231. package/src/Selector/selectorPresentation.stylex.ts +20 -0
  232. package/src/Selector/useSelectorPresentation.ts +147 -0
  233. package/src/Skeleton/Skeleton.doc.mjs +12 -0
  234. package/src/Skeleton/Skeleton.spec.md +126 -0
  235. package/src/Slider/Slider.doc.mjs +186 -32
  236. package/src/Slider/Slider.spec.md +223 -0
  237. package/src/Stack/Stack.doc.mjs +22 -0
  238. package/src/Stack/Stack.spec.md +154 -0
  239. package/src/StatusDot/StatusDot.doc.mjs +17 -0
  240. package/src/StatusDot/StatusDot.spec.md +137 -0
  241. package/src/Switch/Switch.doc.mjs +41 -1
  242. package/src/Switch/Switch.spec.md +174 -0
  243. package/src/Text/Text.doc.mjs +190 -43
  244. package/src/Text/Text.spec.md +205 -0
  245. package/src/TextArea/TextArea.doc.mjs +256 -46
  246. package/src/TextArea/TextArea.spec.md +204 -0
  247. package/src/TextInput/TextInput.doc.mjs +1 -1
  248. package/src/TimeInput/TimeInput.doc.mjs +1 -1
  249. package/src/Toast/ToastViewport.test.tsx +118 -4
  250. package/src/Toast/useToastGesture.ts +61 -8
  251. package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
  252. package/src/Tokenizer/Tokenizer.doc.mjs +1 -1
  253. package/src/Toolbar/Toolbar.doc.mjs +19 -0
  254. package/src/Toolbar/Toolbar.spec.md +173 -0
  255. package/src/Tooltip/Tooltip.doc.mjs +17 -0
  256. package/src/Tooltip/Tooltip.spec.md +141 -0
  257. package/src/TreeList/TreeList.doc.mjs +58 -0
  258. package/src/TreeList/TreeList.spec.md +220 -0
  259. package/src/Typeahead/Typeahead.doc.mjs +84 -2
  260. package/src/Typeahead/Typeahead.spec.md +250 -0
  261. package/src/hooks/useAdaptivePresentation.ts +35 -0
  262. package/src/hooks/useFocusReturnVisibility.ts +45 -0
  263. package/src/hooks/useFocusTrap.test.tsx +40 -0
  264. package/src/hooks/useFocusTrap.ts +13 -0
  265. package/src/i18n/InternationalizationContext.ts +3 -0
  266. package/src/i18n/InternationalizationProvider.tsx +8 -1
  267. package/src/i18n/__tests__/resolve.test.ts +23 -1
  268. package/src/i18n/resolve.ts +46 -49
  269. package/src/i18n/useTranslator.ts +2 -7
  270. package/src/theme/themingTargets.test.ts +91 -25
@@ -1,12 +1,123 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Field',
7
+ required: false,
8
+ description:
9
+ 'Standalone Field shell that provides the label and optional supporting content; omitted inside InputGroup.',
10
+ },
11
+ {
12
+ name: 'Trigger',
13
+ required: true,
14
+ description:
15
+ 'Painted control that displays the current selection or placeholder and opens the selection surface.',
16
+ },
17
+ {
18
+ name: 'Icon-rendered start icon',
19
+ required: false,
20
+ description:
21
+ 'Optional leading semantic icon or icon component rendered through Icon.',
22
+ },
23
+ {
24
+ name: 'Caller-rendered start content',
25
+ required: false,
26
+ description:
27
+ 'Optional arbitrary React content rendered directly at the start of the trigger.',
28
+ },
29
+ {
30
+ name: 'Trigger clear button',
31
+ required: false,
32
+ description:
33
+ 'Shared clear action that removes every selected value when hasClear is enabled.',
34
+ },
35
+ {
36
+ name: 'Status icon',
37
+ required: false,
38
+ description:
39
+ 'Status glyph shown in place of the disclosure indicator for attached or tooltip status.',
40
+ },
41
+ {
42
+ name: 'Indicator icon',
43
+ required: false,
44
+ description:
45
+ 'Trailing chevron shown when status presentation does not replace it; reflects collapsed or expanded state.',
46
+ },
47
+ {
48
+ name: 'Search row',
49
+ required: false,
50
+ description:
51
+ 'Panel header with a borderless search input and optional clear action.',
52
+ },
53
+ {
54
+ name: 'Search icon',
55
+ required: false,
56
+ description:
57
+ 'Leading magnifier rendered through Icon inside the search row.',
58
+ },
59
+ {
60
+ name: 'Search clear button',
61
+ required: false,
62
+ description:
63
+ 'Shared clear action shown in the search row while a query is present.',
64
+ },
65
+ {
66
+ name: 'Option row',
67
+ required: false,
68
+ description:
69
+ 'Selectable row for an option or the optional select-all choice.',
70
+ },
71
+ {
72
+ name: 'Option checkbox indicator',
73
+ required: false,
74
+ description:
75
+ 'CheckboxInput indicator that presents each row’s selected, unselected, or indeterminate state.',
76
+ },
77
+ {
78
+ name: 'Option divider',
79
+ required: false,
80
+ description:
81
+ 'Divider supplied in the public options data to separate adjacent option groups.',
82
+ },
83
+ {
84
+ name: 'Section heading',
85
+ required: false,
86
+ description: 'Visible heading for a labeled group of option rows.',
87
+ },
88
+ {
89
+ name: 'Empty state',
90
+ required: false,
91
+ description:
92
+ 'Message shown when the shared panel content has no options or no search matches.',
93
+ },
94
+ {
95
+ name: 'Pointer popup',
96
+ required: false,
97
+ description:
98
+ 'Anchored painted surface that hosts the shared panel content for popover presentation.',
99
+ },
100
+ {
101
+ name: 'Touch sheet heading',
102
+ required: false,
103
+ description:
104
+ 'Heading above the shared panel content in bottom-sheet presentation.',
105
+ },
106
+ {
107
+ name: 'Touch sheet',
108
+ required: false,
109
+ description:
110
+ 'BottomSheet surface that hosts the same panel content for bottom-sheet presentation.',
111
+ },
112
+ ];
113
+
3
114
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
115
 
5
116
  export const docs = {
6
117
  name: 'MultiSelector',
7
118
  displayName: 'Multi Selector',
8
119
  group: 'Selector',
9
- category: 'Data Input',
120
+ category: 'Form Controls',
10
121
  keywords: [
11
122
  'multiselect',
12
123
  'checkbox',
@@ -224,6 +335,13 @@ export const docs = {
224
335
  'Which edge of the option row carries the checkbox. end pushes it to the far edge of the row, including on the select-all row.',
225
336
  default: "'start'",
226
337
  },
338
+ {
339
+ name: 'presentation',
340
+ type: "'popover' | 'bottom-sheet' | 'adaptive'",
341
+ description:
342
+ 'How the option list is presented. adaptive uses a bottom sheet on compact touch screens and an anchored popover otherwise.',
343
+ default: "'popover'",
344
+ },
227
345
  {
228
346
  name: 'width',
229
347
  type: 'SizeValue',
@@ -257,6 +375,7 @@ export const docs = {
257
375
  },
258
376
  ],
259
377
  usage: {
378
+ anatomy,
260
379
  description:
261
380
  'A checkbox dropdown for selecting multiple values from a list. Selected items can display as a count, labels, or badges. Use it for filtering or when presenting a finite set of options where multiple choices are needed.',
262
381
  bestPractices: [
@@ -290,6 +409,11 @@ export const docs = {
290
409
  description:
291
410
  'Use variant="ghost" when a multi-selector sits in a toolbar with ghost buttons. If validation status is needed there, prefer statusVariant="tooltip" so the toolbar height stays compact.',
292
411
  },
412
+ {
413
+ guidance: true,
414
+ description:
415
+ 'Use presentation="adaptive" when the multi-selector should become a bottom sheet on compact touch screens.',
416
+ },
293
417
  {
294
418
  guidance: false,
295
419
  description: 'Use for single-value selection; use Selector instead.',
@@ -349,6 +473,8 @@ export const docsZh = {
349
473
  '状态消息的放置方式:attached 直接叠加在输入框下方;detached 作为独立元素浮于下方并留有间距。',
350
474
  renderOption:
351
475
  '每个可选选项的自定义渲染函数。不会用于分隔线、分组或全选行。',
476
+ presentation:
477
+ '选项列表的呈现方式:锚定弹出框、底部抽屉,或根据紧凑触控屏自适应。',
352
478
  xstyle: '布局自定义的 StyleX 样式,必须是 stylex.create() 值。',
353
479
  },
354
480
  },
@@ -487,6 +613,8 @@ export const docsDense = {
487
613
  'status message placement; ghost detaches attached by default; use tooltip for compact toolbars.',
488
614
  renderOption:
489
615
  'custom render fn per selectable option; not dividers/sections/select-all',
616
+ presentation:
617
+ 'popover, bottom-sheet, or compact-touch adaptive presentation',
490
618
  xstyle: 'StyleX layout styles; stylex.create() only',
491
619
  },
492
620
  },
@@ -0,0 +1,266 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 3
4
+ kind: component
5
+ id: component:MultiSelector
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/MultiSelector/MultiSelector.test.tsx,
16
+ packages/core/src/BottomSheet/BottomSheet.test.tsx,
17
+ packages/core/src/CheckboxInput/CheckboxInput.test.tsx,
18
+ packages/core/src/Divider/Divider.test.tsx,
19
+ packages/core/src/Field/Field.test.tsx,
20
+ packages/core/src/Icon/Icon.test.tsx,
21
+ packages/core/src/Text/Text.test.tsx,
22
+ scripts/check-knowledge.mjs,
23
+ ]
24
+ families: [family:overlay-dismissal]
25
+ design_specs: []
26
+ architecture:
27
+ [architecture:component-theming-surface, architecture:layer-runtime]
28
+ contributing: []
29
+ system_specs: []
30
+ ---
31
+
32
+ # MultiSelector component contract
33
+
34
+ ## Intent
35
+
36
+ MultiSelector renders a multi-selection control with an optional standalone Field
37
+ shell and one shared panel content tree in either an anchored pointer popup or a
38
+ touch-oriented BottomSheet.
39
+ This draft records current consumer anatomy and theming ownership without changing
40
+ runtime behavior, styling, targets, or public API.
41
+
42
+ ## Compatibility and migration
43
+
44
+ - Released default preserved: `yes`
45
+ - Compatibility class: additive documentation only; runtime, DOM, styling,
46
+ targets, and public API remain unchanged
47
+ - Controlled/uncontrolled behavior: unchanged
48
+ - Migration decision: none
49
+
50
+ Consumer migration instructions belong in consumer docs and release notes.
51
+
52
+ ## Ownership boundary
53
+
54
+ **Owns**
55
+
56
+ - The painted Trigger and its current `multi-selector` target.
57
+ - Indicator icon, Search row, Option row, Section heading, Empty state, and
58
+ Pointer popup presentation through the other six current MultiSelector targets.
59
+ - One shared panel-content tree containing optional search, option, divider,
60
+ section, and empty-state content regardless of which presentation hosts it.
61
+
62
+ **Does not own / non-goals**
63
+
64
+ - Standalone field-shell presentation — rendered by Field and themed through
65
+ Field's current `field` target; omitted when MultiSelector is inside InputGroup.
66
+ - Shared clear actions — rendered by Field's InputClearButton and themed through
67
+ Field's current `input-clear-button` target.
68
+ - Option checkbox presentation — rendered by CheckboxInput and themed through
69
+ CheckboxInput's current `checkbox-indicator` target.
70
+ - Public option dividers — rendered by Divider and themed through Divider's
71
+ current `divider` target.
72
+ - General Icon presentation for start, search, and status icons — rendered by
73
+ Icon and themed through Icon's current `icon` target.
74
+ - The Touch sheet heading — rendered by Heading and themed through Text's current
75
+ `heading` target.
76
+ - The Touch sheet panel — rendered by BottomSheet and themed through
77
+ BottomSheet's current `bottom-sheet` target.
78
+ - Arbitrary ReactNode start content supplied by the caller.
79
+ - Shared layer lifecycle, positioning, and dismissal behavior.
80
+
81
+ ## Public concepts
82
+
83
+ No new public concept is introduced. Consumer props and usage remain documented
84
+ in `MultiSelector.doc.mjs`.
85
+
86
+ ## Behavioral and layout contract
87
+
88
+ | ID | Candidate invariant | Basis | Draft review state |
89
+ | --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------------------------------------------------- |
90
+ | FR1 | The current standalone presentation contains a Field and painted Trigger; the supported InputGroup path renders the same Trigger without a Field. Optional start content, clear action, status icon, or Indicator icon follows supplied props and state. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
91
+ | FR2 | The shared panel content contains the optional Search row, zero or more Option rows, optional public Option dividers and Section headings, and an Empty state when the applicable result set is empty and the value is not loading. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
92
+ | FR3 | Trigger, Indicator icon, Search row, Option row, Section heading, Empty state, and Pointer popup carry the seven current non-deprecated MultiSelector targets documented in `MultiSelector.doc.mjs`. | Current source, docs, and tests | Verified current behavior; no target change |
93
+ | FR4 | When present, Field, shared clear actions, option checkbox indicators, public Option dividers, general icons, Touch sheet heading, and Touch sheet retain their Field, CheckboxInput, Divider, Icon, Text, and BottomSheet theming owners. | Current composition and owners | Verified current behavior; no target change |
94
+ | FR5 | Pointer popup and Touch sheet are separate surface anatomy rows, but both host the same `panelContent` tree; the Touch sheet additionally renders its Heading. Changing presentation does not create a second search, option, divider, section, or empty-state content model. | Current source and tests | Verified current behavior; no normalization |
95
+ | FR6 | `multi-selector-clear-icon` remains a deprecated compatibility alias for `input-clear-icon`; it is not a current target and does not claim a separate anatomy part. | Current public target metadata | Verified current compatibility state |
96
+
97
+ ### Allowed variation
98
+
99
+ - Standalone MultiSelector renders Field; the supported InputGroup path omits it.
100
+ - `presentation="popover"` uses the Pointer popup; `presentation="bottom-sheet"`
101
+ uses the Touch sheet and its Heading; `presentation="adaptive"` selects between
102
+ those existing branches from current modality and viewport behavior.
103
+ - Search row, Option divider, Section heading, Empty state, select-all Option row,
104
+ and both clear actions are optional under their documented prop and data conditions.
105
+ - Semantic names and icon component types render through Icon. Arbitrary ReactNode
106
+ start content renders directly and stays caller-owned.
107
+ - Trigger text, labels, badges, and custom option content may vary inside the
108
+ Trigger or Option row without creating another MultiSelector target.
109
+
110
+ ### Representative states
111
+
112
+ | State | Required invariant | Allowed variation |
113
+ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
114
+ | Standalone or InputGroup | Field is present only standalone; Trigger anatomy and targets remain the same in both paths. | InputGroup owns grouped layout and labeling support. |
115
+ | Pointer presentation | Pointer popup carries `multi-selector-popup` and hosts the shared panel content. | Search, groups, options, and empty content follow their ordinary states. |
116
+ | Touch presentation | BottomSheet owns the Sheet panel, Text owns its Heading, and the sheet hosts the same shared panel content. | Adaptive or explicit selection may choose this branch. |
117
+ | Search enabled with a query | Search row owns `multi-selector-search`; its icons/actions keep their shared owners. | Results may be flat, grouped, or empty. |
118
+ | Option, select all, selected, disabled | Option row owns `multi-selector-option` and reflects its current size/state metadata. | Checkbox state and custom row content vary independently. |
119
+ | Attached or tooltip status | Status icon replaces the Indicator icon. | Tooltip status wraps the icon in a button without changing Icon owner. |
120
+
121
+ ### Transformation and precedence order
122
+
123
+ - No new search, selection, presentation, or styling precedence rule is introduced.
124
+
125
+ ### Performance and resources
126
+
127
+ - No new performance or resource rule is introduced.
128
+
129
+ ## Accessibility contract
130
+
131
+ This draft does not change or extend MultiSelector's existing field, combobox,
132
+ listbox, checkbox, live-region, focus, or dismissal behavior.
133
+
134
+ ## Design relationships
135
+
136
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
137
+ | ----------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------ | ----------------- | ------------------ |
138
+ | Field | Provides the label and optional supporting content outside InputGroup; omitted inside InputGroup. | Field component | Supporting | FR1, FR4 |
139
+ | Trigger | Presents the current selection or placeholder and opens the active presentation. | Current source and public docs | Prominent | FR1, FR3 |
140
+ | Icon-rendered start icon | Presents a semantic or component icon through Icon. | Icon component | Supporting | FR1, FR4 |
141
+ | Caller-rendered start content | Presents arbitrary caller content directly in the trigger's start slot. | Caller-supplied content | Context-dependent | FR1 |
142
+ | Trigger clear button | Removes every selected value through the shared field clear action. | Field component | Supporting | FR1, FR4 |
143
+ | Status icon | Presents attached or tooltip validation state through Icon. | Icon component | Supporting | FR1, FR4 |
144
+ | Indicator icon | Presents disclosure state when status presentation does not replace it. | Current source and public docs | Supporting | FR1, FR3 |
145
+ | Search row | Provides query entry at the top of the shared panel content. | Current source and public docs | Supporting | FR2, FR3, FR5 |
146
+ | Search icon | Presents the search affordance through Icon. | Icon component | Supporting | FR2, FR4 |
147
+ | Search clear button | Clears a present query through the shared field clear action. | Field component | Supporting | FR2, FR4 |
148
+ | Option row | Presents one option or the select-all choice and owns row interaction/state. | Current source and public docs | Prominent | FR2, FR3 |
149
+ | Option checkbox indicator | Presents the option's selected, unselected, or indeterminate state through CheckboxInput. | CheckboxInput component | Supporting | FR2, FR4 |
150
+ | Option divider | Separates adjacent groups when a public divider entry is present in the options data. | Divider component | Supporting | FR2, FR4 |
151
+ | Section heading | Labels a visible group of option rows. | Current source and public docs | Supporting | FR2, FR3 |
152
+ | Empty state | Presents no-options or no-results content when the panel has no selectable rows to show. | Current source and public docs | Prominent | FR2, FR3 |
153
+ | Pointer popup | Paints the anchored pointer surface around the shared panel content. | Current source and public docs | Prominent | FR3, FR5 |
154
+ | Touch sheet heading | Names the bottom-sheet presentation above the shared panel content. | Text component | Prominent | FR4, FR5 |
155
+ | Touch sheet | Paints the BottomSheet surface around the same shared panel content. | BottomSheet component | Prominent | FR4, FR5 |
156
+
157
+ The seven current MultiSelector targets remain on their shipped visible elements.
158
+ Nested shared primitives keep their own owners. The Pointer popup and Touch sheet
159
+ are intentionally separate rows because they use different surface owners; the
160
+ Touch sheet adds a Heading owned by Text. Search, option, divider, section, and
161
+ empty-state content remains one shared tree.
162
+
163
+ ### Theming anatomy
164
+
165
+ <!-- anatomy-theming:v1 -->
166
+
167
+ ```json
168
+ {
169
+ "Field": {"delegatesTo": {"owner": "component:Field", "target": "field"}},
170
+ "Trigger": {"target": "multi-selector"},
171
+ "Icon-rendered start icon": {
172
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
173
+ },
174
+ "Caller-rendered start content": {
175
+ "none": {
176
+ "reason": "intentional: Arbitrary ReactNode content is caller-owned and receives no MultiSelector target."
177
+ }
178
+ },
179
+ "Trigger clear button": {
180
+ "delegatesTo": {
181
+ "owner": "component:Field",
182
+ "target": "input-clear-button"
183
+ }
184
+ },
185
+ "Status icon": {
186
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
187
+ },
188
+ "Indicator icon": {"target": "multi-selector-indicator-icon"},
189
+ "Search row": {"target": "multi-selector-search"},
190
+ "Search icon": {
191
+ "delegatesTo": {"owner": "component:Icon", "target": "icon"}
192
+ },
193
+ "Search clear button": {
194
+ "delegatesTo": {
195
+ "owner": "component:Field",
196
+ "target": "input-clear-button"
197
+ }
198
+ },
199
+ "Option row": {"target": "multi-selector-option"},
200
+ "Option checkbox indicator": {
201
+ "delegatesTo": {
202
+ "owner": "component:CheckboxInput",
203
+ "target": "checkbox-indicator"
204
+ }
205
+ },
206
+ "Option divider": {
207
+ "delegatesTo": {"owner": "component:Divider", "target": "divider"}
208
+ },
209
+ "Section heading": {"target": "multi-selector-section-heading"},
210
+ "Empty state": {"target": "multi-selector-empty-state"},
211
+ "Pointer popup": {"target": "multi-selector-popup"},
212
+ "Touch sheet heading": {
213
+ "delegatesTo": {"owner": "component:Text", "target": "heading"}
214
+ },
215
+ "Touch sheet": {
216
+ "delegatesTo": {
217
+ "owner": "component:BottomSheet",
218
+ "target": "bottom-sheet"
219
+ }
220
+ }
221
+ }
222
+ ```
223
+
224
+ ## Family and system relationships
225
+
226
+ - `architecture:component-theming-surface` owns anatomy qualification, local
227
+ target mapping, delegation, and exclusion of deprecated aliases.
228
+ - `architecture:layer-runtime` owns the current Popover host and the distinction
229
+ between browser popover and native-dialog sheet presentation.
230
+ - `family:overlay-dismissal` owns shared Escape and platform-close ordering. The
231
+ Pointer popup participates through its composed Popover owner; the Touch sheet
232
+ inherits BottomSheet's current local-only adoption gap.
233
+ - Field, CheckboxInput, Divider, Icon, Text, and BottomSheet retain ownership of
234
+ their delegated targets and presentation.
235
+
236
+ ## Verification map
237
+
238
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
239
+ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
240
+ | FR1, FR2 | `MultiSelector.test.tsx` render, search, divider, grouping, status, select-all, and empty-state suites plus InputGroup source inspection | Standalone/InputGroup, search, divided, selected, empty, status | Removing a documented part or misreporting Field presence conflicts with existing structure, content assertions, or source. | `audit:MultiSelector/anatomy` |
241
+ | FR3, FR6 | Component target suites, source inspection, and public target inventory | All seven current targets and deprecated alias | A current target is missed, invented, moved, or confused with the deprecated clear-icon alias. | `audit:MultiSelector/theming` |
242
+ | FR4 | Composed owner source/tests for Field, CheckboxInput, Divider, Icon, Text, and BottomSheet | Field omission/presence, clear actions, checkbox, divider, icons, heading, sheet | A shared part gains the wrong local owner or its documented owner no longer renders it. | `audit:MultiSelector/theming` |
243
+ | FR5 | `MultiSelector.test.tsx` presentation suites and `panelContent` source inspection | Explicit popover, explicit sheet, adaptive touch | A presentation stops using its owner or receives a divergent panel-content implementation. | `audit:MultiSelector/overlay` |
244
+ | 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:MultiSelector/theming` |
245
+
246
+ Existing component tests directly assert the Trigger, Indicator icon, Search row,
247
+ Option row, Section heading, Empty state, and Pointer popup targets. Presentation
248
+ tests directly distinguish the Popover and BottomSheet branches. Source inspection
249
+ distinguishes standalone Field presence from InputGroup omission and confirms that both surface branches receive the same `panelContent`, while the touch branch
250
+ adds Heading; Divider, Text, BottomSheet, and other shared primitive tests provide
251
+ owner evidence rather than new MultiSelector targets.
252
+
253
+ ## Decision log
254
+
255
+ None. This draft records current facts and introduces no component-local design,
256
+ API, theming, or layer-system decision.
257
+
258
+ ## Open questions
259
+
260
+ None.
261
+
262
+ ## Content boundary
263
+
264
+ This file does not duplicate consumer prop tables, examples, search or selection
265
+ algorithms, implementation steps, or shared layer and theming rules. It links to
266
+ their owners.
@@ -19,12 +19,15 @@ import {
19
19
  within,
20
20
  } from '@testing-library/react';
21
21
  import userEvent from '@testing-library/user-event';
22
+ import * as stylex from '@stylexjs/stylex';
22
23
  import {MultiSelector} from './MultiSelector';
23
24
  import {Icon} from '../Icon';
24
25
  import {InternationalizationProvider} from '../i18n';
25
26
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
27
+ import {__resetInteractionModalityForTest} from '../utils/interactionModality';
26
28
  import {defineTheme} from '../theme/defineTheme';
27
29
  import {generateThemeCSS} from '../theme/generateThemeRules';
30
+ import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
28
31
 
29
32
  function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
30
33
  const {prose, component} = generateThemeCSS(theme);
@@ -40,6 +43,28 @@ function politeRegion(): HTMLElement | null {
40
43
 
41
44
  // Mock showPopover and hidePopover methods since they're not implemented in jsdom
42
45
  beforeEach(() => {
46
+ __resetInteractionModalityForTest();
47
+ HTMLDialogElement.prototype.showModal = vi.fn(function (
48
+ this: HTMLDialogElement,
49
+ ) {
50
+ this.setAttribute('open', '');
51
+ });
52
+ HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
53
+ this.removeAttribute('open');
54
+ });
55
+ vi.stubGlobal(
56
+ 'matchMedia',
57
+ vi.fn().mockImplementation((query: string) => ({
58
+ matches: false,
59
+ media: query,
60
+ onchange: null,
61
+ addEventListener: vi.fn(),
62
+ removeEventListener: vi.fn(),
63
+ addListener: vi.fn(),
64
+ removeListener: vi.fn(),
65
+ dispatchEvent: vi.fn(),
66
+ })),
67
+ );
43
68
  HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
44
69
  this.setAttribute('popover-open', '');
45
70
  const event = new Event('toggle', {bubbles: false});
@@ -75,6 +100,108 @@ const h = {hidden: true} as const;
75
100
  describe('MultiSelector', () => {
76
101
  const defaultOptions = ['Apple', 'Banana', 'Orange'];
77
102
 
103
+ it('uses a bottom sheet and stays open while toggling options', async () => {
104
+ const user = userEvent.setup();
105
+ const onChange = vi.fn();
106
+ render(
107
+ <MultiSelector
108
+ label="Fruit"
109
+ options={defaultOptions}
110
+ value={[]}
111
+ onChange={onChange}
112
+ presentation="bottom-sheet"
113
+ />,
114
+ );
115
+
116
+ const trigger = screen.getByRole('combobox');
117
+ await user.click(trigger);
118
+
119
+ expect(
120
+ await screen.findByRole('dialog', {name: 'Fruit'}),
121
+ ).toBeInTheDocument();
122
+ expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
123
+
124
+ await user.click(screen.getByRole('option', {name: 'Banana'}));
125
+ expect(onChange).toHaveBeenCalledWith(['Banana']);
126
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
127
+ });
128
+
129
+ it('uses a bottom sheet for adaptive presentation on compact touch', async () => {
130
+ vi.stubGlobal(
131
+ 'matchMedia',
132
+ vi.fn().mockImplementation((query: string) => ({
133
+ matches: query === '(max-width: 768px) and (pointer: coarse)',
134
+ media: query,
135
+ onchange: null,
136
+ addEventListener: vi.fn(),
137
+ removeEventListener: vi.fn(),
138
+ addListener: vi.fn(),
139
+ removeListener: vi.fn(),
140
+ dispatchEvent: vi.fn(),
141
+ })),
142
+ );
143
+ const user = userEvent.setup();
144
+ render(
145
+ <MultiSelector
146
+ label="Fruit"
147
+ options={defaultOptions}
148
+ value={[]}
149
+ onChange={() => {}}
150
+ presentation="adaptive"
151
+ />,
152
+ );
153
+
154
+ await user.click(screen.getByRole('combobox'));
155
+ expect(
156
+ await screen.findByRole('dialog', {name: 'Fruit'}),
157
+ ).toBeInTheDocument();
158
+ expect(HTMLElement.prototype.showPopover).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it('moves keyboard focus into a bottom-sheet listbox', async () => {
162
+ const user = userEvent.setup();
163
+ render(
164
+ <MultiSelector
165
+ label="Fruit"
166
+ options={defaultOptions}
167
+ value={[]}
168
+ onChange={() => {}}
169
+ presentation="bottom-sheet"
170
+ />,
171
+ );
172
+
173
+ const trigger = screen.getByRole('combobox');
174
+ trigger.focus();
175
+ await user.keyboard('{Enter}');
176
+
177
+ await waitFor(() => expect(screen.getByRole('listbox')).toHaveFocus());
178
+ });
179
+
180
+ it('restores touch focus without painting a trigger focus ring', async () => {
181
+ render(
182
+ <MultiSelector
183
+ label="Fruit"
184
+ options={defaultOptions}
185
+ value={[]}
186
+ onChange={() => {}}
187
+ presentation="bottom-sheet"
188
+ />,
189
+ );
190
+
191
+ const trigger = screen.getByRole('combobox');
192
+ fireEvent.pointerDown(trigger, {pointerType: 'touch'});
193
+ fireEvent.click(trigger, {detail: 1});
194
+ const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
195
+ fireEvent.pointerDown(dialog, {pointerType: 'touch'});
196
+ fireEvent.click(dialog, {detail: 1});
197
+
198
+ trigger.focus();
199
+ expect(trigger).toHaveFocus();
200
+ expect(trigger.parentElement).toHaveClass(
201
+ stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
202
+ );
203
+ });
204
+
78
205
  it('renders with label', () => {
79
206
  render(
80
207
  <MultiSelector