@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.
- package/dist/BottomSheet/BottomSheet.d.ts +3 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +4 -3
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
- package/dist/Button/Button.d.ts +2 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -4
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +2 -2
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +34 -17
- package/dist/ContextMenu/ContextMenu.d.ts +14 -3
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +149 -26
- package/dist/ContextMenu/index.d.ts +1 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +340 -26
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
- package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
- package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
- package/dist/DropdownMenu/index.d.ts +2 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/menuWidth.d.ts +14 -0
- package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
- package/dist/DropdownMenu/menuWidth.js +35 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
- package/dist/DropdownMenu/useMenuOverflow.js +60 -0
- package/dist/MoreMenu/MoreMenu.d.ts +8 -2
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +2 -0
- package/dist/MultiSelector/MultiSelector.d.ts +11 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +97 -68
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/Popover/Popover.d.ts +10 -6
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +141 -16
- package/dist/Popover/usePopover.d.ts +4 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +52 -5
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +106 -80
- package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
- package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
- package/dist/Selector/SelectorBottomSheet.js +83 -0
- package/dist/Selector/index.d.ts +1 -1
- package/dist/Selector/index.d.ts.map +1 -1
- package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
- package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
- package/dist/Selector/selectorPresentation.stylex.js +21 -0
- package/dist/Selector/useSelectorPresentation.d.ts +32 -0
- package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
- package/dist/Selector/useSelectorPresentation.js +90 -0
- package/dist/Toast/useToastGesture.d.ts.map +1 -1
- package/dist/Toast/useToastGesture.js +46 -9
- package/dist/astryx.css +19 -1
- package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
- package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
- package/dist/hooks/useAdaptivePresentation.js +19 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
- package/dist/hooks/useFocusReturnVisibility.js +35 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +12 -0
- package/locales/en.json +20 -0
- package/locales/pseudo.json +15 -0
- package/package.json +2 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +6 -0
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +7 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
- package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
- package/src/Button/Button.doc.mjs +56 -0
- package/src/Button/Button.test.tsx +10 -0
- package/src/Button/Button.tsx +13 -5
- package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
- package/src/Carousel/Carousel.test.tsx +83 -0
- package/src/Carousel/Carousel.tsx +8 -2
- package/src/Chat/ChatToolCalls.test.tsx +44 -0
- package/src/Chat/ChatToolCalls.tsx +36 -15
- package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
- package/src/ContextMenu/ContextMenu.test.tsx +131 -1
- package/src/ContextMenu/ContextMenu.tsx +199 -32
- package/src/ContextMenu/index.ts +1 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
- package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
- package/src/DropdownMenu/DropdownMenu.tsx +459 -28
- package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
- package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
- package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
- package/src/DropdownMenu/index.ts +2 -0
- package/src/DropdownMenu/menuWidth.ts +51 -0
- package/src/DropdownMenu/useMenuOverflow.ts +77 -0
- package/src/IconButton/IconButton.doc.mjs +38 -0
- package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
- package/src/MoreMenu/MoreMenu.test.tsx +47 -0
- package/src/MoreMenu/MoreMenu.tsx +13 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +16 -0
- package/src/MultiSelector/MultiSelector.test.tsx +127 -0
- package/src/MultiSelector/MultiSelector.tsx +121 -73
- package/src/MultiSelector/index.ts +1 -0
- package/src/Popover/Popover.doc.mjs +11 -11
- package/src/Popover/Popover.test.tsx +397 -2
- package/src/Popover/Popover.tsx +228 -21
- package/src/Popover/usePopover.tsx +92 -15
- package/src/ProgressBar/ProgressBar.spec.md +208 -0
- package/src/SegmentedControl/SegmentedControl.doc.mjs +56 -0
- package/src/Selector/Selector.doc.mjs +12 -0
- package/src/Selector/Selector.test.tsx +135 -0
- package/src/Selector/Selector.tsx +141 -82
- package/src/Selector/SelectorBottomSheet.tsx +97 -0
- package/src/Selector/index.ts +1 -0
- package/src/Selector/selectorPresentation.stylex.ts +20 -0
- package/src/Selector/useSelectorPresentation.ts +147 -0
- package/src/Toast/ToastViewport.test.tsx +118 -4
- package/src/Toast/useToastGesture.ts +61 -8
- package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
- package/src/hooks/useAdaptivePresentation.ts +35 -0
- package/src/hooks/useFocusReturnVisibility.ts +45 -0
- package/src/hooks/useFocusTrap.test.tsx +40 -0
- package/src/hooks/useFocusTrap.ts +13 -0
- package/src/theme/themingTargets.test.ts +91 -25
|
@@ -29,7 +29,6 @@ import React, {
|
|
|
29
29
|
type ReactNode,
|
|
30
30
|
} from 'react';
|
|
31
31
|
import * as stylex from '@stylexjs/stylex';
|
|
32
|
-
import {usePopoverInternal} from '../Popover/usePopover';
|
|
33
32
|
import {useTooltip} from '../Tooltip';
|
|
34
33
|
import {Icon, renderIconSlot, type IconType} from '../Icon';
|
|
35
34
|
import {useIndicator} from '../Indicator';
|
|
@@ -86,6 +85,10 @@ import {groupStyles} from '../InputGroup/groupStyles';
|
|
|
86
85
|
import {useInputGroup} from '../InputGroup/InputGroupContext';
|
|
87
86
|
import {VisuallyHidden} from '../VisuallyHidden';
|
|
88
87
|
import {useTranslator} from '../i18n';
|
|
88
|
+
import type {AdaptivePresentation} from '../hooks/useAdaptivePresentation';
|
|
89
|
+
import {SelectorBottomSheet} from './SelectorBottomSheet';
|
|
90
|
+
import {useSelectorPresentation} from './useSelectorPresentation';
|
|
91
|
+
import {selectorPresentationStyles} from './selectorPresentation.stylex';
|
|
89
92
|
|
|
90
93
|
const styles = stylex.create({
|
|
91
94
|
// Trigger container — the enhanced click target wrapping the combobox button and clear button as siblings
|
|
@@ -265,6 +268,7 @@ const styles = stylex.create({
|
|
|
265
268
|
boxSizing: 'border-box',
|
|
266
269
|
maxHeight: '300px',
|
|
267
270
|
overflowY: 'auto',
|
|
271
|
+
outline: 'none',
|
|
268
272
|
paddingBlock: spacingVars['--spacing-1'],
|
|
269
273
|
paddingInline: spacingVars['--spacing-1'],
|
|
270
274
|
opacity: 1,
|
|
@@ -450,6 +454,8 @@ export type SelectorSize = 'sm' | 'md' | 'lg';
|
|
|
450
454
|
|
|
451
455
|
export type SelectorVariant = 'input' | 'ghost';
|
|
452
456
|
|
|
457
|
+
export type SelectorPresentation = AdaptivePresentation;
|
|
458
|
+
|
|
453
459
|
export type SelectorStatusType = 'warning' | 'error' | 'success';
|
|
454
460
|
|
|
455
461
|
export interface SelectorStatus {
|
|
@@ -672,6 +678,15 @@ interface SelectorPropsBase<
|
|
|
672
678
|
*/
|
|
673
679
|
placement?: LayerPlacement;
|
|
674
680
|
|
|
681
|
+
/**
|
|
682
|
+
* How the option list is presented.
|
|
683
|
+
* - 'popover': anchored to the trigger
|
|
684
|
+
* - 'bottom-sheet': modal sheet suited to compact touch screens
|
|
685
|
+
* - 'adaptive': bottom sheet on compact coarse-pointer screens, otherwise popover
|
|
686
|
+
* @default 'popover'
|
|
687
|
+
*/
|
|
688
|
+
presentation?: SelectorPresentation;
|
|
689
|
+
|
|
675
690
|
/**
|
|
676
691
|
* Whether the dropdown starts open on mount.
|
|
677
692
|
* Useful for showcases and previews.
|
|
@@ -811,12 +826,14 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
811
826
|
emptyText: emptyTextFromProps,
|
|
812
827
|
emptySearchText: emptySearchTextFromProps,
|
|
813
828
|
placement,
|
|
829
|
+
presentation = 'popover',
|
|
814
830
|
isDefaultOpen = false,
|
|
815
831
|
'data-testid': testId,
|
|
816
832
|
width,
|
|
817
833
|
xstyle,
|
|
818
834
|
className,
|
|
819
835
|
style,
|
|
836
|
+
onFocus,
|
|
820
837
|
hasClear: hasClearProp,
|
|
821
838
|
id,
|
|
822
839
|
...rest
|
|
@@ -949,27 +966,31 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
949
966
|
// Clear any lingering result count when the popover closes so stale status
|
|
950
967
|
// text does not linger in the a11y tree.
|
|
951
968
|
announce('');
|
|
952
|
-
triggerRef.current?.focus();
|
|
953
969
|
}, [announce]);
|
|
954
970
|
|
|
955
|
-
const
|
|
971
|
+
const surface = useSelectorPresentation({
|
|
972
|
+
presentation,
|
|
956
973
|
onHide: handleLayerHide,
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
974
|
+
triggerRef,
|
|
975
|
+
popoverOptions: {
|
|
976
|
+
hasLightDismiss: true,
|
|
977
|
+
hasCloseButton: false,
|
|
978
|
+
hasAutoFocus: false,
|
|
979
|
+
// The popup's own role="listbox" is the exposed semantics; the trigger
|
|
980
|
+
// keeps DOM focus, so wrapping it in a modal dialog would misrepresent it.
|
|
981
|
+
role: 'none',
|
|
982
|
+
// The theme target belongs on the SURFACE that paints the popup, which
|
|
983
|
+
// `usePopover` owns — not on the scrolling list inside it.
|
|
984
|
+
surfaceTarget: 'selector-popup',
|
|
985
|
+
},
|
|
966
986
|
});
|
|
987
|
+
const {popover} = surface;
|
|
967
988
|
const keepOpenProps = useKeepLayerOpenProps(popover.id, popover.isOpen);
|
|
968
989
|
|
|
969
990
|
// Open dropdown on mount when isDefaultOpen is true
|
|
970
991
|
useEffect(() => {
|
|
971
992
|
if (isDefaultOpen) {
|
|
972
|
-
|
|
993
|
+
surface.show();
|
|
973
994
|
}
|
|
974
995
|
// eslint-disable-next-line @eslint-react/exhaustive-deps -- mount-only: isDefaultOpen is not reactive
|
|
975
996
|
}, []);
|
|
@@ -1022,7 +1043,7 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1022
1043
|
const announcedEmptyRef = useRef<string | null>(null);
|
|
1023
1044
|
useEffect(() => {
|
|
1024
1045
|
const isPanelEmpty =
|
|
1025
|
-
|
|
1046
|
+
surface.isOpen &&
|
|
1026
1047
|
!isLoading &&
|
|
1027
1048
|
searchQuery === '' &&
|
|
1028
1049
|
selectableItems.length === 0;
|
|
@@ -1036,7 +1057,7 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1036
1057
|
announcedEmptyRef.current = emptyAnnouncement;
|
|
1037
1058
|
announce(emptyAnnouncement);
|
|
1038
1059
|
}, [
|
|
1039
|
-
|
|
1060
|
+
surface.isOpen,
|
|
1040
1061
|
isLoading,
|
|
1041
1062
|
searchQuery,
|
|
1042
1063
|
selectableItems.length,
|
|
@@ -1118,17 +1139,17 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1118
1139
|
onItemMouseEnter,
|
|
1119
1140
|
} = useCombobox({
|
|
1120
1141
|
selectableItems: filteredItems,
|
|
1121
|
-
wasJustDismissed:
|
|
1142
|
+
wasJustDismissed: surface.wasJustDismissed,
|
|
1122
1143
|
// The optimistic value, not the raw prop: with a pending changeAction the
|
|
1123
1144
|
// prop still holds the old selection, so the popup would open with the
|
|
1124
1145
|
// highlight on it and Delete/Backspace could clear a value the action has
|
|
1125
1146
|
// already replaced.
|
|
1126
1147
|
value: optimisticValue,
|
|
1127
1148
|
isDisabled,
|
|
1128
|
-
isOpen:
|
|
1149
|
+
isOpen: surface.isOpen,
|
|
1129
1150
|
hasSearch,
|
|
1130
1151
|
onOpen: useCallback(() => {
|
|
1131
|
-
|
|
1152
|
+
surface.show();
|
|
1132
1153
|
if (hasSearch) {
|
|
1133
1154
|
requestAnimationFrame(() => {
|
|
1134
1155
|
const input = searchRef.current;
|
|
@@ -1140,8 +1161,8 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1140
1161
|
}
|
|
1141
1162
|
});
|
|
1142
1163
|
}
|
|
1143
|
-
}, [
|
|
1144
|
-
onClose:
|
|
1164
|
+
}, [surface, hasSearch]),
|
|
1165
|
+
onClose: surface.hide,
|
|
1145
1166
|
onSelect: commitValue,
|
|
1146
1167
|
onClear: hasClear ? clearValue : undefined,
|
|
1147
1168
|
onSearchSeed: appendSearchQuery,
|
|
@@ -1161,10 +1182,10 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1161
1182
|
// cycling on the first match. -1 means nothing is selected or highlighted,
|
|
1162
1183
|
// which the hook reads as "search from the top".
|
|
1163
1184
|
getCurrentIndex: () =>
|
|
1164
|
-
|
|
1185
|
+
surface.isOpen ? highlightedIndex : selectedItemIndex,
|
|
1165
1186
|
onMatch: index => {
|
|
1166
1187
|
const item = selectableItems[index];
|
|
1167
|
-
if (
|
|
1188
|
+
if (surface.isOpen) {
|
|
1168
1189
|
setHighlightedIndex(index);
|
|
1169
1190
|
} else if (item.value !== optimisticValue) {
|
|
1170
1191
|
commitValue(item.value);
|
|
@@ -1191,13 +1212,13 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1191
1212
|
// cursor walks off-screen once navigation passes the visible window. Mirrors
|
|
1192
1213
|
// CommandPaletteItem's scrollIntoView({block: 'nearest'}) behavior.
|
|
1193
1214
|
useEffect(() => {
|
|
1194
|
-
if (!
|
|
1215
|
+
if (!surface.isOpen || highlightedIndex < 0) {
|
|
1195
1216
|
return;
|
|
1196
1217
|
}
|
|
1197
1218
|
document
|
|
1198
1219
|
.getElementById(getItemId(highlightedIndex))
|
|
1199
1220
|
?.scrollIntoView?.({block: 'nearest'});
|
|
1200
|
-
}, [
|
|
1221
|
+
}, [surface.isOpen, highlightedIndex, getItemId]);
|
|
1201
1222
|
|
|
1202
1223
|
// Handle clear button click
|
|
1203
1224
|
const handleClear = useCallback(
|
|
@@ -1229,17 +1250,21 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1229
1250
|
label: t('@astryx.selector.searchOptions'),
|
|
1230
1251
|
})}
|
|
1231
1252
|
{...themeProps('selector-search')}
|
|
1232
|
-
xstyle={
|
|
1253
|
+
xstyle={
|
|
1254
|
+
surface.activePresentation === 'popover' &&
|
|
1255
|
+
variant !== 'ghost' &&
|
|
1256
|
+
styles.searchRowInput
|
|
1257
|
+
}
|
|
1233
1258
|
// When hasSearch is set, focus moves into this input on open, so it —
|
|
1234
1259
|
// not the trigger — must be the combobox that reports the highlighted
|
|
1235
1260
|
// option via aria-activedescendant (comboboxes-4). A bare searchbox
|
|
1236
1261
|
// left the highlight silent to screen readers.
|
|
1237
1262
|
role="combobox"
|
|
1238
|
-
aria-expanded={
|
|
1263
|
+
aria-expanded={surface.isOpen}
|
|
1239
1264
|
aria-controls={listboxId}
|
|
1240
1265
|
aria-autocomplete="list"
|
|
1241
1266
|
aria-activedescendant={
|
|
1242
|
-
|
|
1267
|
+
surface.isOpen && highlightedIndex >= 0
|
|
1243
1268
|
? getItemId(highlightedIndex)
|
|
1244
1269
|
: undefined
|
|
1245
1270
|
}
|
|
@@ -1302,7 +1327,8 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1302
1327
|
searchPlaceholder,
|
|
1303
1328
|
handleSearchChange,
|
|
1304
1329
|
onKeyDown,
|
|
1305
|
-
|
|
1330
|
+
surface.isOpen,
|
|
1331
|
+
surface.activePresentation,
|
|
1306
1332
|
highlightedIndex,
|
|
1307
1333
|
getItemId,
|
|
1308
1334
|
variant,
|
|
@@ -1554,6 +1580,79 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1554
1580
|
</>
|
|
1555
1581
|
);
|
|
1556
1582
|
|
|
1583
|
+
const panelContent = hasSearch ? (
|
|
1584
|
+
<div>
|
|
1585
|
+
{renderSearch()}
|
|
1586
|
+
<Divider />
|
|
1587
|
+
<div
|
|
1588
|
+
ref={listboxRef}
|
|
1589
|
+
id={listboxId}
|
|
1590
|
+
role="listbox"
|
|
1591
|
+
aria-labelledby={triggerId}
|
|
1592
|
+
{...stylex.props(
|
|
1593
|
+
styles.dropdown,
|
|
1594
|
+
surface.activePresentation === 'popover' &&
|
|
1595
|
+
variant !== 'ghost' &&
|
|
1596
|
+
styles.dropdownInput,
|
|
1597
|
+
)}>
|
|
1598
|
+
{renderOptions()}
|
|
1599
|
+
</div>
|
|
1600
|
+
</div>
|
|
1601
|
+
) : (
|
|
1602
|
+
<div
|
|
1603
|
+
ref={listboxRef}
|
|
1604
|
+
id={listboxId}
|
|
1605
|
+
role="listbox"
|
|
1606
|
+
aria-labelledby={triggerId}
|
|
1607
|
+
aria-activedescendant={
|
|
1608
|
+
surface.isOpen && highlightedIndex >= 0
|
|
1609
|
+
? getItemId(highlightedIndex)
|
|
1610
|
+
: undefined
|
|
1611
|
+
}
|
|
1612
|
+
tabIndex={surface.activePresentation === 'bottom-sheet' ? 0 : undefined}
|
|
1613
|
+
onKeyDown={
|
|
1614
|
+
surface.activePresentation === 'bottom-sheet'
|
|
1615
|
+
? handleTriggerKeyDown
|
|
1616
|
+
: undefined
|
|
1617
|
+
}
|
|
1618
|
+
{...stylex.props(
|
|
1619
|
+
styles.dropdown,
|
|
1620
|
+
surface.activePresentation === 'popover' &&
|
|
1621
|
+
variant !== 'ghost' &&
|
|
1622
|
+
styles.dropdownInput,
|
|
1623
|
+
surface.activePresentation === 'popover' &&
|
|
1624
|
+
!isPositioned &&
|
|
1625
|
+
styles.dropdownHidden,
|
|
1626
|
+
)}>
|
|
1627
|
+
{renderOptions()}
|
|
1628
|
+
</div>
|
|
1629
|
+
);
|
|
1630
|
+
|
|
1631
|
+
const selectionSurface =
|
|
1632
|
+
surface.activePresentation === 'bottom-sheet' ? (
|
|
1633
|
+
<SelectorBottomSheet
|
|
1634
|
+
isOpen={surface.isSheetOpen}
|
|
1635
|
+
onOpenChange={surface.onSheetOpenChange}
|
|
1636
|
+
finalFocusRef={triggerRef}
|
|
1637
|
+
initialFocusRef={hasSearch ? searchRef : listboxRef}
|
|
1638
|
+
label={label}>
|
|
1639
|
+
{panelContent}
|
|
1640
|
+
</SelectorBottomSheet>
|
|
1641
|
+
) : (
|
|
1642
|
+
popover.render(panelContent, {
|
|
1643
|
+
placement: popoverPlacement,
|
|
1644
|
+
alignment: 'start',
|
|
1645
|
+
// The system's standard menu clearance, except in overlay mode:
|
|
1646
|
+
// there the measured negative margin owns the block geometry and
|
|
1647
|
+
// the menu is meant to sit on the trigger, not clear it.
|
|
1648
|
+
offset: shouldOverlaySelectedItem
|
|
1649
|
+
? undefined
|
|
1650
|
+
: spacingVars['--spacing-1'],
|
|
1651
|
+
xstyle: [styles.popover, layerAnimations[popoverPlacement]],
|
|
1652
|
+
style: popoverOffsetStyle,
|
|
1653
|
+
})
|
|
1654
|
+
);
|
|
1655
|
+
|
|
1557
1656
|
const selectorContent = (
|
|
1558
1657
|
<>
|
|
1559
1658
|
<div
|
|
@@ -1581,6 +1680,8 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1581
1680
|
variant === 'ghost' && styles.triggerGhost,
|
|
1582
1681
|
variant === 'ghost' && interactionOverlayStyles.backgroundImage,
|
|
1583
1682
|
variant === 'ghost' && focusOutlineStyles.focusWithin,
|
|
1683
|
+
surface.isTriggerFocusRingSuppressed &&
|
|
1684
|
+
selectorPresentationStyles.pointerRestoredFocus,
|
|
1584
1685
|
isDisabled && inputWrapperStyles.disabled,
|
|
1585
1686
|
variant === 'ghost' && isDisabled && styles.triggerGhostDisabled,
|
|
1586
1687
|
!selectedItem && styles.triggerPlaceholder,
|
|
@@ -1612,11 +1713,13 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1612
1713
|
// plain button that opens the listbox — not a second combobox.
|
|
1613
1714
|
role={hasSearch ? undefined : 'combobox'}
|
|
1614
1715
|
{...rest}
|
|
1615
|
-
aria-haspopup=
|
|
1616
|
-
|
|
1716
|
+
aria-haspopup={
|
|
1717
|
+
surface.activePresentation === 'bottom-sheet' ? 'dialog' : 'listbox'
|
|
1718
|
+
}
|
|
1719
|
+
aria-expanded={surface.isOpen}
|
|
1617
1720
|
aria-controls={listboxId}
|
|
1618
1721
|
aria-activedescendant={
|
|
1619
|
-
!hasSearch &&
|
|
1722
|
+
!hasSearch && surface.isOpen && highlightedIndex >= 0
|
|
1620
1723
|
? getItemId(highlightedIndex)
|
|
1621
1724
|
: undefined
|
|
1622
1725
|
}
|
|
@@ -1631,6 +1734,10 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1631
1734
|
disabled={isDisabled && !showsDisabledMessage}
|
|
1632
1735
|
aria-disabled={showsDisabledMessage ? 'true' : undefined}
|
|
1633
1736
|
onKeyDown={handleTriggerKeyDown}
|
|
1737
|
+
onFocus={event => {
|
|
1738
|
+
onFocus?.(event);
|
|
1739
|
+
surface.onTriggerFocus(event);
|
|
1740
|
+
}}
|
|
1634
1741
|
tabIndex={isDisabled && !showsDisabledMessage ? -1 : 0}
|
|
1635
1742
|
{...stylex.props(
|
|
1636
1743
|
styles.trigger,
|
|
@@ -1702,7 +1809,7 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1702
1809
|
xstyle={[
|
|
1703
1810
|
styles.triggerIcon,
|
|
1704
1811
|
styles.triggerIconRotation,
|
|
1705
|
-
|
|
1812
|
+
surface.isOpen && styles.triggerIconOpen,
|
|
1706
1813
|
]}
|
|
1707
1814
|
// Stable theme target on the chevron glyph itself, so a theme can
|
|
1708
1815
|
// restyle just this icon (color, size, hover) — and its
|
|
@@ -1710,61 +1817,13 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1710
1817
|
// @layer astryx-theme win over the icon's own base color/size,
|
|
1711
1818
|
// which a button-level target could not reach.
|
|
1712
1819
|
{...themeProps('selector-indicator-icon', {
|
|
1713
|
-
state:
|
|
1820
|
+
state: surface.isOpen ? 'expanded' : 'collapsed',
|
|
1714
1821
|
})}
|
|
1715
1822
|
/>
|
|
1716
1823
|
)}
|
|
1717
1824
|
</div>
|
|
1718
1825
|
|
|
1719
|
-
{
|
|
1720
|
-
hasSearch ? (
|
|
1721
|
-
<div>
|
|
1722
|
-
{renderSearch()}
|
|
1723
|
-
{/*
|
|
1724
|
-
Separates the header from the options and spans the panel: the
|
|
1725
|
-
search row and the listbox each hold their own inline padding,
|
|
1726
|
-
the line does not, so it reads as the panel's own edge.
|
|
1727
|
-
*/}
|
|
1728
|
-
<Divider />
|
|
1729
|
-
<div
|
|
1730
|
-
ref={listboxRef}
|
|
1731
|
-
id={listboxId}
|
|
1732
|
-
role="listbox"
|
|
1733
|
-
aria-labelledby={triggerId}
|
|
1734
|
-
{...stylex.props(
|
|
1735
|
-
styles.dropdown,
|
|
1736
|
-
variant !== 'ghost' && styles.dropdownInput,
|
|
1737
|
-
)}>
|
|
1738
|
-
{renderOptions()}
|
|
1739
|
-
</div>
|
|
1740
|
-
</div>
|
|
1741
|
-
) : (
|
|
1742
|
-
<div
|
|
1743
|
-
ref={listboxRef}
|
|
1744
|
-
id={listboxId}
|
|
1745
|
-
role="listbox"
|
|
1746
|
-
aria-labelledby={triggerId}
|
|
1747
|
-
{...stylex.props(
|
|
1748
|
-
styles.dropdown,
|
|
1749
|
-
variant !== 'ghost' && styles.dropdownInput,
|
|
1750
|
-
!isPositioned && styles.dropdownHidden,
|
|
1751
|
-
)}>
|
|
1752
|
-
{renderOptions()}
|
|
1753
|
-
</div>
|
|
1754
|
-
),
|
|
1755
|
-
{
|
|
1756
|
-
placement: popoverPlacement,
|
|
1757
|
-
alignment: 'start',
|
|
1758
|
-
// The system's standard menu clearance, except in overlay mode:
|
|
1759
|
-
// there the measured negative margin owns the block geometry and
|
|
1760
|
-
// the menu is meant to sit on the trigger, not clear it.
|
|
1761
|
-
offset: shouldOverlaySelectedItem
|
|
1762
|
-
? undefined
|
|
1763
|
-
: spacingVars['--spacing-1'],
|
|
1764
|
-
xstyle: [styles.popover, layerAnimations[popoverPlacement]],
|
|
1765
|
-
style: popoverOffsetStyle,
|
|
1766
|
-
},
|
|
1767
|
-
)}
|
|
1826
|
+
{selectionSurface}
|
|
1768
1827
|
|
|
1769
1828
|
{showStatusTooltip && statusTooltip.renderTooltip(status?.message ?? '')}
|
|
1770
1829
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file SelectorBottomSheet.tsx
|
|
7
|
+
* @input Uses BottomSheet, Section, and Heading
|
|
8
|
+
* @output Shared mobile listbox sheet for Selector and MultiSelector
|
|
9
|
+
* @position Internal presentation primitive for selection controls
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {lazy, Suspense, useEffect, type ReactNode, type RefObject} from 'react';
|
|
13
|
+
import * as stylex from '@stylexjs/stylex';
|
|
14
|
+
import {Heading} from '../Heading';
|
|
15
|
+
import {Section} from '../Section';
|
|
16
|
+
import {spacingVars} from '../theme/tokens.stylex';
|
|
17
|
+
|
|
18
|
+
const LazyBottomSheet = lazy(async () =>
|
|
19
|
+
import('../BottomSheet/BottomSheet').then(module => ({
|
|
20
|
+
default: module.BottomSheet,
|
|
21
|
+
})),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const styles = stylex.create({
|
|
25
|
+
content: {
|
|
26
|
+
width: '100%',
|
|
27
|
+
},
|
|
28
|
+
heading: {
|
|
29
|
+
marginBlockEnd: spacingVars['--spacing-2'],
|
|
30
|
+
marginInline: spacingVars['--spacing-3'],
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
interface SelectorBottomSheetProps {
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
finalFocusRef: RefObject<HTMLElement | null>;
|
|
37
|
+
initialFocusRef: RefObject<HTMLElement | null>;
|
|
38
|
+
isOpen: boolean;
|
|
39
|
+
label: string;
|
|
40
|
+
onOpenChange: (isOpen: boolean) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function SelectorBottomSheetInitialFocus({
|
|
44
|
+
isOpen,
|
|
45
|
+
targetRef,
|
|
46
|
+
}: {
|
|
47
|
+
isOpen: boolean;
|
|
48
|
+
targetRef: RefObject<HTMLElement | null>;
|
|
49
|
+
}) {
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!isOpen) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const frame = requestAnimationFrame(() => {
|
|
55
|
+
targetRef.current?.focus({preventScroll: true});
|
|
56
|
+
});
|
|
57
|
+
return () => cancelAnimationFrame(frame);
|
|
58
|
+
}, [isOpen, targetRef]);
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function SelectorBottomSheet({
|
|
64
|
+
children,
|
|
65
|
+
finalFocusRef,
|
|
66
|
+
initialFocusRef,
|
|
67
|
+
isOpen,
|
|
68
|
+
label,
|
|
69
|
+
onOpenChange,
|
|
70
|
+
}: SelectorBottomSheetProps) {
|
|
71
|
+
return (
|
|
72
|
+
<Suspense fallback={null}>
|
|
73
|
+
<LazyBottomSheet
|
|
74
|
+
isOpen={isOpen}
|
|
75
|
+
onOpenChange={onOpenChange}
|
|
76
|
+
finalFocusRef={finalFocusRef}
|
|
77
|
+
label={label}
|
|
78
|
+
height="hug"
|
|
79
|
+
purpose="info">
|
|
80
|
+
<Section paddingBlockStart={4} paddingBlockEnd={0} paddingInline={1}>
|
|
81
|
+
<div {...stylex.props(styles.content)}>
|
|
82
|
+
<Heading level={3} xstyle={styles.heading}>
|
|
83
|
+
{label}
|
|
84
|
+
</Heading>
|
|
85
|
+
{children}
|
|
86
|
+
<SelectorBottomSheetInitialFocus
|
|
87
|
+
isOpen={isOpen}
|
|
88
|
+
targetRef={initialFocusRef}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
</Section>
|
|
92
|
+
</LazyBottomSheet>
|
|
93
|
+
</Suspense>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
SelectorBottomSheet.displayName = 'SelectorBottomSheet';
|
package/src/Selector/index.ts
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file selectorPresentation.stylex.ts
|
|
5
|
+
* @input Uses field border tokens
|
|
6
|
+
* @output Shared focus-return styles for Selector and MultiSelector
|
|
7
|
+
* @position Internal visual policy for selector presentation changes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as stylex from '@stylexjs/stylex';
|
|
11
|
+
import {colorVars} from '../theme/tokens.stylex';
|
|
12
|
+
|
|
13
|
+
export const selectorPresentationStyles = stylex.create({
|
|
14
|
+
pointerRestoredFocus: {
|
|
15
|
+
borderColor: colorVars['--color-border-emphasized'],
|
|
16
|
+
boxShadow: 'none',
|
|
17
|
+
outlineWidth: '0',
|
|
18
|
+
outlineStyle: 'none',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useSelectorPresentation.ts
|
|
7
|
+
* @input Uses the shared adaptive presentation policy and usePopover
|
|
8
|
+
* @output Coordinates popover and bottom-sheet disclosure state
|
|
9
|
+
* @position Internal presentation controller shared by Selector and MultiSelector
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
useCallback,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
type FocusEvent,
|
|
17
|
+
type RefObject,
|
|
18
|
+
} from 'react';
|
|
19
|
+
import {
|
|
20
|
+
usePopoverInternal,
|
|
21
|
+
type UsePopoverOptions,
|
|
22
|
+
type UsePopoverReturn,
|
|
23
|
+
} from '../Popover/usePopover';
|
|
24
|
+
import {
|
|
25
|
+
useAdaptivePresentation,
|
|
26
|
+
type AdaptivePresentation,
|
|
27
|
+
type ResolvedAdaptivePresentation,
|
|
28
|
+
} from '../hooks/useAdaptivePresentation';
|
|
29
|
+
import {useFocusReturnVisibility} from '../hooks/useFocusReturnVisibility';
|
|
30
|
+
|
|
31
|
+
interface UseSelectorPresentationOptions {
|
|
32
|
+
presentation: AdaptivePresentation;
|
|
33
|
+
onHide: () => void;
|
|
34
|
+
popoverOptions: Omit<UsePopoverOptions, 'onHide'>;
|
|
35
|
+
triggerRef: RefObject<HTMLElement | null>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SelectorPresentationController {
|
|
39
|
+
activePresentation: ResolvedAdaptivePresentation;
|
|
40
|
+
hide: () => void;
|
|
41
|
+
isOpen: boolean;
|
|
42
|
+
isSheetOpen: boolean;
|
|
43
|
+
isTriggerFocusRingSuppressed: boolean;
|
|
44
|
+
onSheetOpenChange: (isOpen: boolean) => void;
|
|
45
|
+
onTriggerFocus: (event: FocusEvent<HTMLElement>) => void;
|
|
46
|
+
popover: UsePopoverReturn & {wasJustDismissed: () => boolean};
|
|
47
|
+
show: () => void;
|
|
48
|
+
wasJustDismissed: () => boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useSelectorPresentation({
|
|
52
|
+
presentation,
|
|
53
|
+
onHide,
|
|
54
|
+
popoverOptions,
|
|
55
|
+
triggerRef,
|
|
56
|
+
}: UseSelectorPresentationOptions): SelectorPresentationController {
|
|
57
|
+
const resolvedPresentation = useAdaptivePresentation(presentation);
|
|
58
|
+
const activePresentationRef =
|
|
59
|
+
useRef<ResolvedAdaptivePresentation>(resolvedPresentation);
|
|
60
|
+
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
61
|
+
const isSheetOpenRef = useRef(false);
|
|
62
|
+
const onHideRef = useRef(onHide);
|
|
63
|
+
const {
|
|
64
|
+
isFocusRingSuppressed,
|
|
65
|
+
onFocusReturnTargetFocus,
|
|
66
|
+
prepareFocusReturn,
|
|
67
|
+
resetFocusReturn,
|
|
68
|
+
} = useFocusReturnVisibility();
|
|
69
|
+
onHideRef.current = onHide;
|
|
70
|
+
|
|
71
|
+
const handlePopoverHide = useCallback(() => {
|
|
72
|
+
prepareFocusReturn();
|
|
73
|
+
onHideRef.current();
|
|
74
|
+
triggerRef.current?.focus();
|
|
75
|
+
}, [prepareFocusReturn, triggerRef]);
|
|
76
|
+
const popover = usePopoverInternal({
|
|
77
|
+
...popoverOptions,
|
|
78
|
+
onHide: handlePopoverHide,
|
|
79
|
+
});
|
|
80
|
+
const {
|
|
81
|
+
hide: hidePopover,
|
|
82
|
+
show: showPopover,
|
|
83
|
+
wasJustDismissed: wasPopoverJustDismissed,
|
|
84
|
+
} = popover;
|
|
85
|
+
|
|
86
|
+
const show = useCallback(() => {
|
|
87
|
+
resetFocusReturn();
|
|
88
|
+
activePresentationRef.current = resolvedPresentation;
|
|
89
|
+
if (resolvedPresentation === 'bottom-sheet') {
|
|
90
|
+
isSheetOpenRef.current = true;
|
|
91
|
+
setIsSheetOpen(true);
|
|
92
|
+
} else {
|
|
93
|
+
showPopover();
|
|
94
|
+
}
|
|
95
|
+
}, [resetFocusReturn, resolvedPresentation, showPopover]);
|
|
96
|
+
|
|
97
|
+
const hide = useCallback(() => {
|
|
98
|
+
if (isSheetOpenRef.current) {
|
|
99
|
+
prepareFocusReturn();
|
|
100
|
+
isSheetOpenRef.current = false;
|
|
101
|
+
setIsSheetOpen(false);
|
|
102
|
+
onHideRef.current();
|
|
103
|
+
} else {
|
|
104
|
+
hidePopover();
|
|
105
|
+
}
|
|
106
|
+
}, [hidePopover, prepareFocusReturn]);
|
|
107
|
+
|
|
108
|
+
const handleTriggerFocus = useCallback(
|
|
109
|
+
(_event: FocusEvent<HTMLElement>) => onFocusReturnTargetFocus(),
|
|
110
|
+
[onFocusReturnTargetFocus],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const handleSheetOpenChange = useCallback(
|
|
114
|
+
(nextIsOpen: boolean) => {
|
|
115
|
+
if (nextIsOpen) {
|
|
116
|
+
show();
|
|
117
|
+
} else {
|
|
118
|
+
hide();
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[hide, show],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const isOpen = popover.isOpen || isSheetOpen;
|
|
125
|
+
const activePresentation = isOpen
|
|
126
|
+
? activePresentationRef.current
|
|
127
|
+
: resolvedPresentation;
|
|
128
|
+
|
|
129
|
+
const wasJustDismissed = useCallback(
|
|
130
|
+
() =>
|
|
131
|
+
activePresentationRef.current === 'popover' && wasPopoverJustDismissed(),
|
|
132
|
+
[wasPopoverJustDismissed],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
activePresentation,
|
|
137
|
+
hide,
|
|
138
|
+
isOpen,
|
|
139
|
+
isSheetOpen,
|
|
140
|
+
isTriggerFocusRingSuppressed: isFocusRingSuppressed,
|
|
141
|
+
onSheetOpenChange: handleSheetOpenChange,
|
|
142
|
+
onTriggerFocus: handleTriggerFocus,
|
|
143
|
+
popover,
|
|
144
|
+
show,
|
|
145
|
+
wasJustDismissed,
|
|
146
|
+
};
|
|
147
|
+
}
|