@astryxdesign/core 0.5.2-canary.c9c8564 → 0.5.2-canary.e4f8e4e

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 (146) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +3 -1
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +4 -3
  4. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
  5. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
  7. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
  9. package/dist/Button/Button.d.ts +2 -1
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.js +11 -4
  12. package/dist/Carousel/Carousel.d.ts.map +1 -1
  13. package/dist/Carousel/Carousel.js +2 -2
  14. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  15. package/dist/Chat/ChatToolCalls.js +34 -17
  16. package/dist/ContextMenu/ContextMenu.d.ts +14 -3
  17. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  18. package/dist/ContextMenu/ContextMenu.js +149 -26
  19. package/dist/ContextMenu/index.d.ts +1 -0
  20. package/dist/ContextMenu/index.d.ts.map +1 -1
  21. package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
  22. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  23. package/dist/DropdownMenu/DropdownMenu.js +340 -26
  24. package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
  25. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  26. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  27. package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
  28. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
  29. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
  31. package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
  32. package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
  33. package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
  34. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
  35. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
  36. package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
  37. package/dist/DropdownMenu/index.d.ts +2 -1
  38. package/dist/DropdownMenu/index.d.ts.map +1 -1
  39. package/dist/DropdownMenu/menuWidth.d.ts +14 -0
  40. package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
  41. package/dist/DropdownMenu/menuWidth.js +35 -0
  42. package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
  43. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
  44. package/dist/DropdownMenu/useMenuOverflow.js +60 -0
  45. package/dist/MoreMenu/MoreMenu.d.ts +8 -2
  46. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  47. package/dist/MoreMenu/MoreMenu.js +2 -0
  48. package/dist/MultiSelector/MultiSelector.d.ts +11 -1
  49. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  50. package/dist/MultiSelector/MultiSelector.js +97 -68
  51. package/dist/MultiSelector/index.d.ts +1 -1
  52. package/dist/MultiSelector/index.d.ts.map +1 -1
  53. package/dist/Popover/Popover.d.ts +10 -6
  54. package/dist/Popover/Popover.d.ts.map +1 -1
  55. package/dist/Popover/Popover.js +141 -16
  56. package/dist/Popover/usePopover.d.ts +4 -0
  57. package/dist/Popover/usePopover.d.ts.map +1 -1
  58. package/dist/Popover/usePopover.js +52 -5
  59. package/dist/Selector/Selector.d.ts +10 -0
  60. package/dist/Selector/Selector.d.ts.map +1 -1
  61. package/dist/Selector/Selector.js +106 -80
  62. package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
  63. package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
  64. package/dist/Selector/SelectorBottomSheet.js +83 -0
  65. package/dist/Selector/index.d.ts +1 -1
  66. package/dist/Selector/index.d.ts.map +1 -1
  67. package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
  68. package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
  69. package/dist/Selector/selectorPresentation.stylex.js +21 -0
  70. package/dist/Selector/useSelectorPresentation.d.ts +32 -0
  71. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
  72. package/dist/Selector/useSelectorPresentation.js +90 -0
  73. package/dist/Toast/useToastGesture.d.ts.map +1 -1
  74. package/dist/Toast/useToastGesture.js +46 -9
  75. package/dist/astryx.css +19 -1
  76. package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
  77. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
  78. package/dist/hooks/useAdaptivePresentation.js +19 -0
  79. package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
  80. package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
  81. package/dist/hooks/useFocusReturnVisibility.js +35 -0
  82. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  83. package/dist/hooks/useFocusTrap.js +12 -0
  84. package/locales/en.json +20 -0
  85. package/locales/pseudo.json +15 -0
  86. package/package.json +2 -2
  87. package/src/BottomSheet/BottomSheet.doc.mjs +6 -0
  88. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  89. package/src/BottomSheet/BottomSheet.tsx +7 -4
  90. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
  91. package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
  92. package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
  93. package/src/Button/Button.doc.mjs +56 -0
  94. package/src/Button/Button.test.tsx +10 -0
  95. package/src/Button/Button.tsx +13 -5
  96. package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
  97. package/src/Carousel/Carousel.test.tsx +83 -0
  98. package/src/Carousel/Carousel.tsx +8 -2
  99. package/src/Chat/ChatToolCalls.test.tsx +44 -0
  100. package/src/Chat/ChatToolCalls.tsx +36 -15
  101. package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
  102. package/src/ContextMenu/ContextMenu.test.tsx +131 -1
  103. package/src/ContextMenu/ContextMenu.tsx +199 -32
  104. package/src/ContextMenu/index.ts +1 -0
  105. package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
  106. package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
  107. package/src/DropdownMenu/DropdownMenu.tsx +459 -28
  108. package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
  109. package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
  110. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
  111. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
  112. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
  113. package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
  114. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
  115. package/src/DropdownMenu/index.ts +2 -0
  116. package/src/DropdownMenu/menuWidth.ts +51 -0
  117. package/src/DropdownMenu/useMenuOverflow.ts +77 -0
  118. package/src/IconButton/IconButton.doc.mjs +38 -0
  119. package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
  120. package/src/MoreMenu/MoreMenu.test.tsx +47 -0
  121. package/src/MoreMenu/MoreMenu.tsx +13 -1
  122. package/src/MultiSelector/MultiSelector.doc.mjs +16 -0
  123. package/src/MultiSelector/MultiSelector.test.tsx +127 -0
  124. package/src/MultiSelector/MultiSelector.tsx +121 -73
  125. package/src/MultiSelector/index.ts +1 -0
  126. package/src/Popover/Popover.doc.mjs +11 -11
  127. package/src/Popover/Popover.test.tsx +397 -2
  128. package/src/Popover/Popover.tsx +228 -21
  129. package/src/Popover/usePopover.tsx +92 -15
  130. package/src/ProgressBar/ProgressBar.spec.md +208 -0
  131. package/src/SegmentedControl/SegmentedControl.doc.mjs +56 -0
  132. package/src/Selector/Selector.doc.mjs +12 -0
  133. package/src/Selector/Selector.test.tsx +135 -0
  134. package/src/Selector/Selector.tsx +141 -82
  135. package/src/Selector/SelectorBottomSheet.tsx +97 -0
  136. package/src/Selector/index.ts +1 -0
  137. package/src/Selector/selectorPresentation.stylex.ts +20 -0
  138. package/src/Selector/useSelectorPresentation.ts +147 -0
  139. package/src/Toast/ToastViewport.test.tsx +118 -4
  140. package/src/Toast/useToastGesture.ts +61 -8
  141. package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
  142. package/src/hooks/useAdaptivePresentation.ts +35 -0
  143. package/src/hooks/useFocusReturnVisibility.ts +45 -0
  144. package/src/hooks/useFocusTrap.test.tsx +40 -0
  145. package/src/hooks/useFocusTrap.ts +13 -0
  146. 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,
@@ -64,7 +64,7 @@ export const docs = {
64
64
  {
65
65
  name: 'width',
66
66
  type: 'number | string',
67
- description: 'Width of the popover container.',
67
+ description: 'Width of the popover container. The layer still caps to the viewport with alignment-aware safe-area gutters before scrolling long content.',
68
68
  default: "'auto'",
69
69
  },
70
70
  {
@@ -101,7 +101,7 @@ export const docs = {
101
101
  {
102
102
  name: 'hasAutoFocus',
103
103
  type: 'boolean',
104
- description: 'Whether to auto-focus the first focusable element when the popover opens. Set to false for inline showcases or documentation previews.',
104
+ description: 'Whether to move focus into the popover when it opens. Keyboard activation focuses the first content control; pointer activation focuses the labeled dialog container so an action does not appear preselected. Set to false for inline showcases or documentation previews.',
105
105
  default: 'true',
106
106
  },
107
107
  {
@@ -149,8 +149,8 @@ export const docs = {
149
149
  { guidance: true, description: 'Keep popover content focused on a single task or piece of information.' },
150
150
  { guidance: true, description: 'Provide a clear way to close: either by clicking outside or with an explicit close button.' },
151
151
  { guidance: false, description: 'Nest popovers inside other popovers; it creates confusing focus and navigation.' },
152
- { guidance: false, description: 'Use a popover for content that requires heavy user input; use a Dialog instead.' },
153
- { guidance: false, description: 'Put too much content in a popover; if it needs scrolling, use a Dialog instead.' },
152
+ { guidance: false, description: 'Assume input complexity alone determines the presentation; evaluate the task’s focus, space, and interaction requirements.' },
153
+ { guidance: false, description: 'Assume scrolling alone means Popover is the wrong component; a bounded Popover may scroll while a focused anchored interaction remains appropriate.' },
154
154
  ],
155
155
  anatomy: [
156
156
  {name: 'Header', required: true, description: 'Contains the title, optional subheader, and close button.'},
@@ -220,7 +220,7 @@ export const docsZh = {
220
220
  {
221
221
  name: 'width',
222
222
  type: 'number | string',
223
- description: '弹出框容器的宽度。',
223
+ description: '弹出框容器的宽度。弹出层仍会限制在视口和安全区域留白内,长内容再滚动。',
224
224
  default: "'auto'",
225
225
  },
226
226
  {
@@ -294,8 +294,8 @@ export const docsZh = {
294
294
  { guidance: true, description: 'Keep popover content focused on a single task or piece of information.' },
295
295
  { guidance: true, description: 'Provide a clear way to close: either by clicking outside or with an explicit close button.' },
296
296
  { guidance: false, description: 'Nest popovers inside other popovers; it creates confusing focus and navigation.' },
297
- { guidance: false, description: 'Use a popover for content that requires heavy user input; use a Dialog instead.' },
298
- { guidance: false, description: 'Put too much content in a popover; if it needs scrolling, use a Dialog instead.' },
297
+ { guidance: false, description: 'Assume input complexity alone determines the presentation; evaluate the task’s focus, space, and interaction requirements.' },
298
+ { guidance: false, description: 'Assume scrolling alone means Popover is the wrong component; a bounded Popover may scroll while a focused anchored interaction remains appropriate.' },
299
299
  ],
300
300
  anatomy: [
301
301
  {name: 'Header', required: true, description: 'Contains the title, optional subheader, and close button.'},
@@ -316,8 +316,8 @@ export const docsDense = {
316
316
  { guidance: true, description: 'Keep popover content focused on a single task or piece of information.' },
317
317
  { guidance: true, description: 'Provide a clear way to close: either by clicking outside or with an explicit close button.' },
318
318
  { guidance: false, description: 'Nest popovers inside other popovers; it creates confusing focus and navigation.' },
319
- { guidance: false, description: 'Use a popover for content that requires heavy user input; use a Dialog instead.' },
320
- { guidance: false, description: 'Put too much content in a popover; if it needs scrolling, use a Dialog instead.' },
319
+ { guidance: false, description: 'Assume input complexity alone determines the presentation; evaluate the task’s focus, space, and interaction requirements.' },
320
+ { guidance: false, description: 'Assume scrolling alone means Popover is the wrong component; a bounded Popover may scroll while a focused anchored interaction remains appropriate.' },
321
321
  ],
322
322
  anatomy: [
323
323
  {name: 'Header', required: true, description: 'Contains the title, optional subheader, and close button.'},
@@ -340,11 +340,11 @@ export const docsDense = {
340
340
  isOpen: 'Whether popover shown in controlled mode.',
341
341
  onOpenChange: 'Callback fired when popover visibility changes.',
342
342
  isEnabled: 'When false, trigger interactions ignored.',
343
- width: 'Popover container width.',
343
+ width: 'Popover container width; capped to alignment-aware viewport/safe-area gutters before long content scrolls.',
344
344
  label: 'Accessible label for popover dialog.',
345
345
  hasCloseButton: 'Whether to include hidden close button for accessibility.',
346
346
  closeButtonLabel: 'Label for hidden close button.',
347
- hasAutoFocus: 'Auto-focus first element on open; false for showcases.',
347
+ hasAutoFocus: 'Move focus into the popover on open; keyboard targets the first control and pointer targets the dialog container.',
348
348
  hasLightDismiss: 'Outside click dismisses; false for explicit-dismiss surfaces (coachmarks).',
349
349
  hasEscapeDismiss: 'Escape dismisses; full effect only with hasLightDismiss=false.',
350
350
  },