@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
@@ -28,7 +28,6 @@ import React, {
28
28
  type ReactNode,
29
29
  } from 'react';
30
30
  import * as stylex from '@stylexjs/stylex';
31
- import {usePopoverInternal} from '../Popover/usePopover';
32
31
  import {useTooltip} from '../Tooltip';
33
32
  import {Icon, renderIconSlot, type IconType} from '../Icon';
34
33
  import type {IconName} from '../Icon';
@@ -57,7 +56,6 @@ import {
57
56
  typographyVars,
58
57
  fontWeightVars,
59
58
  typeScaleVars,
60
- borderVars,
61
59
  } from '../theme/tokens.stylex';
62
60
  import type {
63
61
  MultiSelectorOptionType,
@@ -86,6 +84,10 @@ import {groupStyles} from '../InputGroup/groupStyles';
86
84
  import {useInputGroup} from '../InputGroup/InputGroupContext';
87
85
  import {VisuallyHidden} from '../VisuallyHidden';
88
86
  import {useTranslator} from '../i18n';
87
+ import type {AdaptivePresentation} from '../hooks/useAdaptivePresentation';
88
+ import {SelectorBottomSheet} from '../Selector/SelectorBottomSheet';
89
+ import {useSelectorPresentation} from '../Selector/useSelectorPresentation';
90
+ import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
89
91
 
90
92
  // Sentinel value for the select-all item in keyboard navigation
91
93
  const SELECT_ALL_VALUE = '__xds_select_all__';
@@ -245,6 +247,9 @@ const styles = stylex.create({
245
247
  overflowY: 'auto',
246
248
  padding: spacingVars['--spacing-1'],
247
249
  },
250
+ listbox: {
251
+ outline: 'none',
252
+ },
248
253
 
249
254
  // Popover container (for anchor positioning)
250
255
  popover: {
@@ -415,6 +420,8 @@ export type MultiSelectorSize = 'sm' | 'md' | 'lg';
415
420
 
416
421
  export type MultiSelectorVariant = 'input' | 'ghost';
417
422
 
423
+ export type MultiSelectorPresentation = AdaptivePresentation;
424
+
418
425
  export type MultiSelectorStatusType = 'warning' | 'error' | 'success';
419
426
 
420
427
  export type {MultiSelectorStatus};
@@ -658,6 +665,15 @@ export interface MultiSelectorProps<
658
665
  */
659
666
  indicatorPosition?: IndicatorPosition;
660
667
 
668
+ /**
669
+ * How the option list is presented.
670
+ * - 'popover': anchored to the trigger
671
+ * - 'bottom-sheet': modal sheet suited to compact touch screens
672
+ * - 'adaptive': bottom sheet on compact coarse-pointer screens, otherwise popover
673
+ * @default 'popover'
674
+ */
675
+ presentation?: MultiSelectorPresentation;
676
+
661
677
  /**
662
678
  * Whether the dropdown starts open on mount.
663
679
  * Useful for showcases and previews.
@@ -747,6 +763,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
747
763
  maxBadges = 3,
748
764
  renderOption,
749
765
  indicatorPosition = 'start',
766
+ presentation = 'popover',
750
767
  isDefaultOpen = false,
751
768
  'data-testid': testId,
752
769
  htmlName,
@@ -754,6 +771,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
754
771
  xstyle,
755
772
  className,
756
773
  style,
774
+ onFocus,
757
775
  }: MultiSelectorProps<T>) {
758
776
  const t = useTranslator();
759
777
  const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
@@ -780,6 +798,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
780
798
  const searchId = useId();
781
799
  const triggerRef = useRef<HTMLButtonElement>(null);
782
800
  const searchRef = useRef<HTMLInputElement>(null);
801
+ const listboxRef = useRef<HTMLDivElement>(null);
783
802
  const inputGroup = useInputGroup();
784
803
 
785
804
  const [searchQuery, setSearchQuery] = useState('');
@@ -935,27 +954,31 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
935
954
  // Clear any lingering result count when the popover closes so stale status
936
955
  // text does not linger in the a11y tree.
937
956
  announce('');
938
- triggerRef.current?.focus();
939
957
  }, [announce]);
940
958
 
941
- const popover = usePopoverInternal({
942
- hasLightDismiss: true,
959
+ const surface = useSelectorPresentation({
960
+ presentation,
943
961
  onHide: handleLayerHide,
944
- hasCloseButton: false,
945
- hasAutoFocus: false,
946
- // The popup's own role="listbox" is the exposed semantics; the trigger
947
- // keeps DOM focus, so wrapping it in a modal dialog would misrepresent it.
948
- role: 'none',
949
- // The theme target belongs on the SURFACE that paints the popup, which
950
- // `usePopover` owns — not on the scrolling list inside it.
951
- surfaceTarget: 'multi-selector-popup',
962
+ triggerRef,
963
+ popoverOptions: {
964
+ hasLightDismiss: true,
965
+ hasCloseButton: false,
966
+ hasAutoFocus: false,
967
+ // The popup's own role="listbox" is the exposed semantics; the trigger
968
+ // keeps DOM focus, so wrapping it in a modal dialog would misrepresent it.
969
+ role: 'none',
970
+ // The theme target belongs on the SURFACE that paints the popup, which
971
+ // `usePopover` owns — not on the scrolling list inside it.
972
+ surfaceTarget: 'multi-selector-popup',
973
+ },
952
974
  });
975
+ const {popover} = surface;
953
976
  const keepOpenProps = useKeepLayerOpenProps(popover.id, popover.isOpen);
954
977
 
955
978
  // Open dropdown on mount when isDefaultOpen is true
956
979
  useEffect(() => {
957
980
  if (isDefaultOpen) {
958
- popover.show();
981
+ surface.show();
959
982
  }
960
983
  // eslint-disable-next-line @eslint-react/exhaustive-deps -- mount-only: isDefaultOpen is not reactive
961
984
  }, []);
@@ -999,7 +1022,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
999
1022
  const announcedEmptyRef = useRef<string | null>(null);
1000
1023
  useEffect(() => {
1001
1024
  const isPanelEmpty =
1002
- popover.isOpen &&
1025
+ surface.isOpen &&
1003
1026
  !isLoading &&
1004
1027
  searchQuery === '' &&
1005
1028
  selectableItems.length === 0;
@@ -1013,7 +1036,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1013
1036
  announcedEmptyRef.current = emptyAnnouncement;
1014
1037
  announce(emptyAnnouncement);
1015
1038
  }, [
1016
- popover.isOpen,
1039
+ surface.isOpen,
1017
1040
  isLoading,
1018
1041
  searchQuery,
1019
1042
  selectableItems.length,
@@ -1158,24 +1181,24 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1158
1181
  onKeyDown,
1159
1182
  onItemMouseEnter,
1160
1183
  } = useMultiCombobox({
1161
- wasJustDismissed: popover.wasJustDismissed,
1184
+ wasJustDismissed: surface.wasJustDismissed,
1162
1185
  selectableItems: sortedItems,
1163
1186
  isDisabled,
1164
- isOpen: popover.isOpen,
1187
+ isOpen: surface.isOpen,
1165
1188
  hasSearch,
1166
1189
  onOpen: useCallback(() => {
1167
1190
  // Snapshot which items are selected at open time — sort is frozen until close
1168
1191
  setSelectedAtOpen(new Set(optimisticValue));
1169
1192
 
1170
- popover.show();
1193
+ surface.show();
1171
1194
  if (hasSearch) {
1172
1195
  // Focus search after popover opens
1173
1196
  requestAnimationFrame(() => {
1174
1197
  searchRef.current?.focus();
1175
1198
  });
1176
1199
  }
1177
- }, [popover, hasSearch, optimisticValue]),
1178
- onClose: popover.hide,
1200
+ }, [surface, hasSearch, optimisticValue]),
1201
+ onClose: surface.hide,
1179
1202
  onToggle: handleNavigableToggle,
1180
1203
  onClear: hasClear ? clearValues : undefined,
1181
1204
  hasValue,
@@ -1187,13 +1210,13 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1187
1210
  // cursor walks off-screen once navigation passes the visible window. Mirrors
1188
1211
  // CommandPaletteItem's scrollIntoView({block: 'nearest'}) behavior.
1189
1212
  useEffect(() => {
1190
- if (!popover.isOpen || highlightedIndex < 0) {
1213
+ if (!surface.isOpen || highlightedIndex < 0) {
1191
1214
  return;
1192
1215
  }
1193
1216
  document
1194
1217
  .getElementById(getItemId(highlightedIndex))
1195
1218
  ?.scrollIntoView?.({block: 'nearest'});
1196
- }, [popover.isOpen, highlightedIndex, getItemId]);
1219
+ }, [surface.isOpen, highlightedIndex, getItemId]);
1197
1220
 
1198
1221
  // Build trigger display content
1199
1222
  const selectedItems = useMemo(() => {
@@ -1288,11 +1311,11 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1288
1311
  // not the trigger — must be the combobox reporting the highlighted
1289
1312
  // option via aria-activedescendant (comboboxes-4).
1290
1313
  role="combobox"
1291
- aria-expanded={popover.isOpen}
1314
+ aria-expanded={surface.isOpen}
1292
1315
  aria-controls={listboxId}
1293
1316
  aria-autocomplete="list"
1294
1317
  aria-activedescendant={
1295
- popover.isOpen && highlightedIndex >= 0
1318
+ surface.isOpen && highlightedIndex >= 0
1296
1319
  ? getItemId(highlightedIndex)
1297
1320
  : undefined
1298
1321
  }
@@ -1355,7 +1378,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1355
1378
  searchPlaceholder,
1356
1379
  handleSearchChange,
1357
1380
  onKeyDown,
1358
- popover.isOpen,
1381
+ surface.isOpen,
1359
1382
  highlightedIndex,
1360
1383
  getItemId,
1361
1384
  t,
@@ -1608,6 +1631,64 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1608
1631
  const showStatusTooltip =
1609
1632
  status != null && effectiveStatusVariant === 'tooltip' && !!status.message;
1610
1633
 
1634
+ const panelContent = hasSearch ? (
1635
+ <div>
1636
+ {renderSearch()}
1637
+ <Divider />
1638
+ <div {...stylex.props(styles.dropdown)}>
1639
+ <div
1640
+ ref={listboxRef}
1641
+ id={listboxId}
1642
+ role="listbox"
1643
+ aria-multiselectable="true"
1644
+ aria-labelledby={triggerId}
1645
+ {...stylex.props(styles.listbox)}>
1646
+ {renderOptions()}
1647
+ </div>
1648
+ </div>
1649
+ </div>
1650
+ ) : (
1651
+ <div {...stylex.props(styles.dropdown)}>
1652
+ <div
1653
+ ref={listboxRef}
1654
+ id={listboxId}
1655
+ role="listbox"
1656
+ aria-multiselectable="true"
1657
+ aria-labelledby={triggerId}
1658
+ aria-activedescendant={
1659
+ surface.isOpen && highlightedIndex >= 0
1660
+ ? getItemId(highlightedIndex)
1661
+ : undefined
1662
+ }
1663
+ tabIndex={surface.activePresentation === 'bottom-sheet' ? 0 : undefined}
1664
+ onKeyDown={
1665
+ surface.activePresentation === 'bottom-sheet' ? onKeyDown : undefined
1666
+ }
1667
+ {...stylex.props(styles.listbox)}>
1668
+ {renderOptions()}
1669
+ </div>
1670
+ </div>
1671
+ );
1672
+
1673
+ const selectionSurface =
1674
+ surface.activePresentation === 'bottom-sheet' ? (
1675
+ <SelectorBottomSheet
1676
+ isOpen={surface.isSheetOpen}
1677
+ onOpenChange={surface.onSheetOpenChange}
1678
+ finalFocusRef={triggerRef}
1679
+ initialFocusRef={hasSearch ? searchRef : listboxRef}
1680
+ label={label}>
1681
+ {panelContent}
1682
+ </SelectorBottomSheet>
1683
+ ) : (
1684
+ popover.render(panelContent, {
1685
+ placement: 'below',
1686
+ alignment: 'start',
1687
+ offset: spacingVars['--spacing-1'],
1688
+ xstyle: styles.popover,
1689
+ })
1690
+ );
1691
+
1611
1692
  const multiSelectorContent = (
1612
1693
  <>
1613
1694
  <div
@@ -1634,6 +1715,8 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1634
1715
  variant === 'ghost' && styles.triggerGhost,
1635
1716
  variant === 'ghost' && interactionOverlayStyles.backgroundImage,
1636
1717
  variant === 'ghost' && focusOutlineStyles.focusWithin,
1718
+ surface.isTriggerFocusRingSuppressed &&
1719
+ selectorPresentationStyles.pointerRestoredFocus,
1637
1720
  isDisabled && inputWrapperStyles.disabled,
1638
1721
  variant === 'ghost' && isDisabled && styles.triggerGhostDisabled,
1639
1722
  optimisticValue.length === 0 && styles.triggerPlaceholder,
@@ -1663,11 +1746,13 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1663
1746
  // focus + aria-activedescendant, comboboxes-4), so the trigger is a
1664
1747
  // plain button that opens the listbox — not a second combobox.
1665
1748
  role={hasSearch ? undefined : 'combobox'}
1666
- aria-haspopup="listbox"
1667
- aria-expanded={popover.isOpen}
1749
+ aria-haspopup={
1750
+ surface.activePresentation === 'bottom-sheet' ? 'dialog' : 'listbox'
1751
+ }
1752
+ aria-expanded={surface.isOpen}
1668
1753
  aria-controls={listboxId}
1669
1754
  aria-activedescendant={
1670
- !hasSearch && popover.isOpen && highlightedIndex >= 0
1755
+ !hasSearch && surface.isOpen && highlightedIndex >= 0
1671
1756
  ? getItemId(highlightedIndex)
1672
1757
  : undefined
1673
1758
  }
@@ -1682,6 +1767,10 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1682
1767
  disabled={isDisabled && !showsDisabledMessage}
1683
1768
  aria-disabled={showsDisabledMessage ? 'true' : undefined}
1684
1769
  onKeyDown={onKeyDown}
1770
+ onFocus={event => {
1771
+ onFocus?.(event);
1772
+ surface.onTriggerFocus(event);
1773
+ }}
1685
1774
  tabIndex={isDisabled && !showsDisabledMessage ? -1 : 0}
1686
1775
  {...stylex.props(styles.trigger)}>
1687
1776
  <span {...stylex.props(styles.triggerContent)}>
@@ -1754,7 +1843,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1754
1843
  xstyle={[
1755
1844
  styles.triggerIcon,
1756
1845
  styles.triggerIconRotation,
1757
- popover.isOpen && styles.triggerIconOpen,
1846
+ surface.isOpen && styles.triggerIconOpen,
1758
1847
  ]}
1759
1848
  // Stable theme target on the chevron glyph itself, so a theme can
1760
1849
  // restyle just this icon (color, size, hover) — and its
@@ -1762,54 +1851,13 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1762
1851
  // @layer astryx-theme win over the icon's own base color/size,
1763
1852
  // which a button-level target could not reach.
1764
1853
  {...themeProps('multi-selector-indicator-icon', {
1765
- state: popover.isOpen ? 'expanded' : 'collapsed',
1854
+ state: surface.isOpen ? 'expanded' : 'collapsed',
1766
1855
  })}
1767
1856
  />
1768
1857
  )}
1769
1858
  </div>
1770
1859
 
1771
- {popover.render(
1772
- hasSearch ? (
1773
- // With a search row the panel splits: the header stays put while the
1774
- // options scroll under it, so the field does not slide out of reach
1775
- // in a long list. Without one the panel is a single scroll container,
1776
- // exactly as before.
1777
- <div>
1778
- {renderSearch()}
1779
- {/*
1780
- Separates the header from the options and spans the panel: the
1781
- search row and the option list each hold their own inline
1782
- padding, the line does not, so it reads as the panel's own edge.
1783
- */}
1784
- <Divider />
1785
- <div {...stylex.props(styles.dropdown)}>
1786
- <div
1787
- id={listboxId}
1788
- role="listbox"
1789
- aria-multiselectable="true"
1790
- aria-labelledby={triggerId}>
1791
- {renderOptions()}
1792
- </div>
1793
- </div>
1794
- </div>
1795
- ) : (
1796
- <div {...stylex.props(styles.dropdown)}>
1797
- <div
1798
- id={listboxId}
1799
- role="listbox"
1800
- aria-multiselectable="true"
1801
- aria-labelledby={triggerId}>
1802
- {renderOptions()}
1803
- </div>
1804
- </div>
1805
- ),
1806
- {
1807
- placement: 'below',
1808
- alignment: 'start',
1809
- offset: spacingVars['--spacing-1'],
1810
- xstyle: styles.popover,
1811
- },
1812
- )}
1860
+ {selectionSurface}
1813
1861
 
1814
1862
  {showStatusTooltip && statusTooltip.renderTooltip(status?.message ?? '')}
1815
1863
 
@@ -11,6 +11,7 @@
11
11
  export {
12
12
  MultiSelector,
13
13
  type MultiSelectorProps,
14
+ type MultiSelectorPresentation,
14
15
  type MultiSelectorSize,
15
16
  type MultiSelectorStatusType,
16
17
  type MultiSelectorSelectedItem,
@@ -1,5 +1,19 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
+ /** @type {import('@astryxdesign/cli/authoring').ComponentAnatomyElement[]} */
4
+ const anatomy = [
5
+ {
6
+ name: 'Container',
7
+ required: true,
8
+ description: 'Circular painted container for the supplied icon.',
9
+ },
10
+ {
11
+ name: 'Icon',
12
+ required: true,
13
+ description: 'Caller-supplied visual content rendered inside the container.',
14
+ },
15
+ ];
16
+
3
17
  /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
4
18
 
5
19
  export const docs = {
@@ -29,6 +43,7 @@ export const docs = {
29
43
  ],
30
44
  },
31
45
  usage: {
46
+ anatomy,
32
47
  description:
33
48
  'NavIcon is a circular icon container with an accent-colored background. Use it in navigation headers such as TopNavHeading and PageNavHeader to visually identify a section or application.',
34
49
  bestPractices: [
@@ -61,6 +76,7 @@ export const docsZh = {
61
76
  ],
62
77
  },
63
78
  usage: {
79
+ anatomy,
64
80
  description:
65
81
  'NavIcon is a circular icon container with an accent-colored background. Use it in navigation headers such as TopNavHeading and PageNavHeader to visually identify a section or application.',
66
82
  bestPractices: [
@@ -76,6 +92,7 @@ export const docsDense = {
76
92
  description:
77
93
  'Circular icon container w/ accent background for navigation headers.',
78
94
  usage: {
95
+ anatomy,
79
96
  description:
80
97
  'NavIcon is a circular icon container with an accent-colored background. Use it in navigation headers such as TopNavHeading and PageNavHeader to visually identify a section or application.',
81
98
  bestPractices: [
@@ -0,0 +1,133 @@
1
+ ---
2
+ schema_version: 1
3
+ template_version: 3
4
+ kind: component
5
+ id: component:NavIcon
6
+ authority: draft
7
+ archive_reason: null
8
+ superseded_by: null
9
+ approved_by: null
10
+ approved_at: null
11
+ owners: [cixzhang]
12
+ review_triggers: [theming]
13
+ verified_by:
14
+ [packages/core/src/NavIcon/NavIcon.test.tsx, scripts/check-knowledge.mjs]
15
+ families: []
16
+ design_specs: []
17
+ architecture:
18
+ [architecture:component-theming-surface, architecture:public-component-api]
19
+ contributing: []
20
+ system_specs: []
21
+ ---
22
+
23
+ # NavIcon component contract
24
+
25
+ ## Intent
26
+
27
+ NavIcon renders caller-supplied icon content inside a circular navigation
28
+ container. This draft records the current consumer anatomy and theming ownership
29
+ without changing runtime behavior or target compatibility.
30
+
31
+ ## Compatibility and migration
32
+
33
+ - Released default preserved: `yes`
34
+ - Compatibility class: additive documentation only; runtime, DOM, styling,
35
+ targets, aliases, and public API remain unchanged
36
+ - Controlled/uncontrolled behavior: not applicable
37
+ - Migration decision: none
38
+
39
+ Consumer migration instructions belong in consumer docs and release notes.
40
+
41
+ ## Ownership boundary
42
+
43
+ **Owns**
44
+
45
+ - The circular container and its current `nav-icon` theming target.
46
+
47
+ **Does not own / non-goals**
48
+
49
+ - The artwork or component supplied through `icon` — owned by the caller.
50
+ - Interaction semantics — NavIcon remains a display-only container.
51
+
52
+ ## Public concepts
53
+
54
+ No new public concept is introduced. Consumer props and usage remain documented
55
+ in `NavIcon.doc.mjs`.
56
+
57
+ ## Behavioral and layout contract
58
+
59
+ | ID | Candidate invariant | Basis | Draft review state |
60
+ | --- | ------------------------------------------------------------------------------------------------ | ------------------------------- | -------------------------------------------------- |
61
+ | FR1 | The current render contains a circular container and the caller-supplied icon content. | Current source, docs, and tests | Verified current behavior; no new behavior decided |
62
+ | FR2 | The container carries `nav-icon`; the deprecated `navicon` alias remains compatibility metadata. | Current source, docs, and tests | Verified current behavior; no target change |
63
+
64
+ ### Allowed variation
65
+
66
+ - Caller-supplied icon artwork and implementation may vary without becoming
67
+ NavIcon-owned anatomy.
68
+
69
+ ### Representative states
70
+
71
+ - The documented anatomy is the same for every caller-supplied icon.
72
+
73
+ ### Transformation and precedence order
74
+
75
+ - No new ordering rule is introduced.
76
+
77
+ ### Performance and resources
78
+
79
+ - No new performance or resource rule is introduced.
80
+
81
+ ## Accessibility contract
82
+
83
+ This draft does not change or extend NavIcon's existing accessibility behavior.
84
+
85
+ ## Design relationships
86
+
87
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
88
+ | ---------------- | ---------------------------------------------- | ------------------------------ | ----------------- | ------------------ |
89
+ | Container | Presents the current circular painted surface. | Current source and public docs | Supporting | FR1, FR2 |
90
+ | Icon | Presents caller-supplied visual content. | Caller-supplied content | Context-dependent | FR1 |
91
+
92
+ The deprecated `navicon` alias is compatibility metadata, not a separate anatomy
93
+ part or current target.
94
+
95
+ ### Theming anatomy
96
+
97
+ <!-- anatomy-theming:v1 -->
98
+
99
+ ```json
100
+ {
101
+ "Container": {"target": "nav-icon"},
102
+ "Icon": {"inherits": "nav-icon"}
103
+ }
104
+ ```
105
+
106
+ ## Family and system relationships
107
+
108
+ This draft references the current shared owners without restating their rules:
109
+ `architecture:component-theming-surface` and
110
+ `architecture:public-component-api`. The deprecated `navicon` alias remains
111
+ compatibility metadata rather than separate anatomy.
112
+
113
+ ## Verification map
114
+
115
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
116
+ | ------------------- | ------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------- |
117
+ | FR1 | `NavIcon.test.tsx` content suite | Caller-supplied icon | Removing the container or supplied content fails existing render and ref assertions. | `audit:NavIcon/anatomy` |
118
+ | FR2 | `NavIcon.test.tsx` target-name suite | Current and deprecated target classes | Removing either emitted compatibility class fails the existing target assertion. | `audit:NavIcon/theming` |
119
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and current target | Missing, extra, prefixed, stale, or alias-backed mappings fail repository validation. | `audit:NavIcon/theming` |
120
+
121
+ ## Decision log
122
+
123
+ None. This draft records current facts and introduces no component-local design
124
+ decision.
125
+
126
+ ## Open questions
127
+
128
+ None.
129
+
130
+ ## Content boundary
131
+
132
+ This file does not duplicate consumer prop tables, examples, implementation
133
+ steps, or system rules. It links to their owners.