@spaced-out/ui-design-system 0.6.35-beta.0 → 0.6.35

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 (35) hide show
  1. package/.cspell/custom-words.txt +0 -9
  2. package/CHANGELOG.md +9 -0
  3. package/lib/components/Menu/Menu.d.ts +1 -71
  4. package/lib/components/Menu/Menu.d.ts.map +1 -1
  5. package/lib/components/Menu/Menu.js +221 -1035
  6. package/lib/components/Menu/Menu.module.css +27 -148
  7. package/lib/components/Menu/MenuOptionButton.d.ts +2 -23
  8. package/lib/components/Menu/MenuOptionButton.d.ts.map +1 -1
  9. package/lib/components/Menu/MenuOptionButton.js +38 -87
  10. package/lib/components/Menu/index.d.ts +0 -2
  11. package/lib/components/Menu/index.d.ts.map +1 -1
  12. package/lib/components/Menu/index.js +0 -22
  13. package/lib/hooks/index.d.ts +0 -1
  14. package/lib/hooks/index.d.ts.map +1 -1
  15. package/lib/hooks/index.js +0 -11
  16. package/lib/utils/click-away/click-away.d.ts +0 -3
  17. package/lib/utils/click-away/click-away.d.ts.map +1 -1
  18. package/lib/utils/click-away/click-away.js +0 -49
  19. package/mcp/package.json +1 -1
  20. package/package.json +1 -1
  21. package/lib/components/Menu/menuOptionId.d.ts +0 -12
  22. package/lib/components/Menu/menuOptionId.d.ts.map +0 -1
  23. package/lib/components/Menu/menuOptionId.js +0 -18
  24. package/lib/components/Menu/useMenuKeyboardNavigation.d.ts +0 -93
  25. package/lib/components/Menu/useMenuKeyboardNavigation.d.ts.map +0 -1
  26. package/lib/components/Menu/useMenuKeyboardNavigation.js +0 -471
  27. package/lib/components/Menu/useMenuTrigger.d.ts +0 -183
  28. package/lib/components/Menu/useMenuTrigger.d.ts.map +0 -1
  29. package/lib/components/Menu/useMenuTrigger.js +0 -347
  30. package/lib/hooks/useInteractionModality/index.d.ts +0 -2
  31. package/lib/hooks/useInteractionModality/index.d.ts.map +0 -1
  32. package/lib/hooks/useInteractionModality/index.js +0 -16
  33. package/lib/hooks/useInteractionModality/useInteractionModality.d.ts +0 -15
  34. package/lib/hooks/useInteractionModality/useInteractionModality.d.ts.map +0 -1
  35. package/lib/hooks/useInteractionModality/useInteractionModality.js +0 -103
@@ -1,183 +0,0 @@
1
- import * as React from 'react';
2
- import type { MenuOption, MenuProps } from '../../components/Menu/Menu';
3
- /**
4
- * Wires a trigger to the Menu it opens.
5
- *
6
- * Three things have to live on the trigger rather than in Menu, because they
7
- * exist while the menu does NOT: `aria-haspopup`, `aria-expanded="false"`, and
8
- * the ArrowDown/ArrowUp that opens the popup in the first place. That is the
9
- * floor — no Menu prop can supply them — so these hooks hand them over as props
10
- * to spread instead, and own everything else: the ids, the tie between the
11
- * trigger's name and the listbox, and the ref Menu focuses into on open.
12
- *
13
- * `useMenuTrigger` is for triggers whose popup options own focus — a button, a
14
- * tab overflow chevron, an inline label — so arrows move real DOM focus.
15
- * `useComboboxMenuTrigger` is for triggers that are text inputs, where focus
16
- * stays put and the highlight is virtual (`aria-activedescendant`).
17
- *
18
- * `getTriggerProps` is a getter rather than a static object because open state
19
- * belongs to the consumer's `ClickAway`, which exposes it through a render
20
- * callback — where a hook cannot be called.
21
- *
22
- * const menuTrigger = useMenuTrigger({id: props.id, ariaLabel: menu?.ariaLabel});
23
- * <ClickAway>
24
- * {({isOpen, onOpen, clickAway}) => (
25
- * <>
26
- * <Button {...props} {...menuTrigger.getTriggerProps({isOpen, onOpen})} />
27
- * {isOpen && <Menu {...menu} {...menuTrigger.menuProps} onTabOut={clickAway} />}
28
- * </>
29
- * )}
30
- * </ClickAway>
31
- */
32
- export interface UseMenuTriggerArgs {
33
- /**
34
- * The trigger's own `id`, when the consumer supplied one. It is kept — the
35
- * hook only mints an id when there is none, since the listbox is named after
36
- * the trigger through it.
37
- */
38
- id?: string;
39
- /** The listbox id, when the consumer set `menu.menuId`. Minted otherwise. */
40
- menuId?: string;
41
- /** Literal name for the listbox. Wins over naming it after the trigger. */
42
- ariaLabel?: string;
43
- /** Id of the element naming the listbox. Defaults to the trigger itself. */
44
- ariaLabelledBy?: string;
45
- }
46
- /**
47
- * Generic in the trigger element so the returned handler matches whatever it is
48
- * spread onto — inferred from the trigger's own `onKeyDown` where it has one.
49
- */
50
- export interface GetTriggerPropsArgs<T extends HTMLElement = HTMLElement> {
51
- /** The consumer's popup state — what ClickAway reports. */
52
- isOpen: boolean;
53
- onOpen: () => void;
54
- /** The trigger's own key handler, if it has one. Runs first. */
55
- onKeyDown?: React.KeyboardEventHandler<T>;
56
- }
57
- export interface MenuTriggerProps<T extends HTMLElement = HTMLElement> {
58
- id: string;
59
- 'aria-haspopup': 'listbox';
60
- 'aria-expanded': boolean;
61
- 'aria-controls': string | undefined;
62
- onKeyDown: React.KeyboardEventHandler<T>;
63
- }
64
- export interface MenuTriggerMenuProps {
65
- menuId: string;
66
- ariaLabelledBy?: string;
67
- ariaLabel?: string;
68
- initialFocusRef: React.RefObject<HTMLElement | null>;
69
- }
70
- export declare const useMenuTrigger: ({ id, menuId: consumerMenuId, ariaLabel, ariaLabelledBy, }?: UseMenuTriggerArgs) => {
71
- getTriggerProps: <T extends HTMLElement = HTMLElement>(args: GetTriggerPropsArgs<T>) => MenuTriggerProps<T>;
72
- menuProps: MenuTriggerMenuProps;
73
- };
74
- /**
75
- * Which of ARIA's two combobox patterns the trigger implements.
76
- *
77
- * - `select` — a read-only field standing in for a `<select>`. An option is
78
- * highlighted as soon as the list opens, Space selects it (the field has no
79
- * typing role), and Enter or Space opens the list in the first place.
80
- * - `autocomplete` — an editable field that filters the list as you type.
81
- * Nothing is highlighted until the user arrows to it (APG calls this manual
82
- * selection: auto-highlighting would let Enter select something they never
83
- * chose), Space types a space, and only the arrow keys open the list.
84
- */
85
- export type ComboboxPattern = 'select' | 'autocomplete';
86
- export interface UseComboboxMenuTriggerArgs {
87
- /** Which pattern this trigger implements. Defaults to `select`. */
88
- pattern?: ComboboxPattern;
89
- /**
90
- * The menu this trigger opens. Read for the option source (unless `options`
91
- * is given), the selection, `virtualization`, `menuDisabled`, `resolveLabel`,
92
- * `allowSearch` and the consumer's own `menuId` / `ariaLabel` /
93
- * `ariaLabelledBy`, all of which win over what the hook would derive.
94
- */
95
- menu?: MenuProps;
96
- /**
97
- * Option source, when it is not `menu` — a consumer that owns filtering and
98
- * hands the already-filtered options to Menu supplies them here instead. Every
99
- * derivation (navigation list, highlight seed, selection lookup) uses it.
100
- */
101
- options?: Array<MenuOption>;
102
- /**
103
- * Refuse to open, e.g. until enough has been typed. Consulted only for the
104
- * keys that would otherwise open the list.
105
- */
106
- canOpen?: () => boolean;
107
- /** Ref on the rendered Menu, for reaching its header/footer controls. */
108
- menuRef: React.RefObject<HTMLElement | null>;
109
- /**
110
- * The consumer's popup state — what ClickAway reports. Drives which keys the
111
- * trigger handles (open-state navigation vs. closed-state opening) and the
112
- * highlight's lifecycle. If the listbox can be WITHHELD while the popup is
113
- * open (still loading, no matches), keep passing the open state here and hand
114
- * `isExpanded` to `getTriggerProps` for the ARIA — routing keys off the
115
- * rendered state instead would make an arrow key call `onOpen` on an
116
- * already-open popup, and `onOpen` toggles.
117
- */
118
- isOpen: boolean;
119
- /** Apply a selection. Called with the option the keyboard acted on. */
120
- onSelectOption: (option: MenuOption) => void;
121
- /** Close the popup — used when Tab leaves a menu with no chrome to land on. */
122
- onClose: () => void;
123
- /** Put focus back on the trigger input, keeping the menu open. */
124
- focusTrigger: () => void;
125
- /** What already labels the trigger, used to name the listbox after it. */
126
- triggerAriaLabelledBy?: string;
127
- triggerAriaLabel?: string;
128
- /**
129
- * Literal name for the listbox when nothing else supplies one. Defaults to
130
- * `'Options'`; a suggestion list wants something truer to what it holds.
131
- */
132
- defaultAriaLabel?: string;
133
- }
134
- export interface GetComboboxTriggerPropsArgs<T extends HTMLElement = HTMLElement> extends GetTriggerPropsArgs<T> {
135
- /**
136
- * Whether the listbox is actually rendered right now, when that can differ
137
- * from `isOpen` (a popup withheld while loading or with no matches). Drives
138
- * `aria-expanded`, `aria-controls` and `aria-activedescendant`, so the trigger
139
- * never claims an expanded listbox that is not in the DOM. Defaults to
140
- * `isOpen`.
141
- */
142
- isExpanded?: boolean;
143
- }
144
- export interface ComboboxTriggerProps<T extends HTMLElement = HTMLElement> {
145
- role: 'combobox';
146
- 'aria-haspopup': 'listbox';
147
- 'aria-expanded': boolean;
148
- 'aria-controls': string | undefined;
149
- 'aria-autocomplete': 'none' | 'list';
150
- 'aria-activedescendant': string | undefined;
151
- onKeyDown: React.KeyboardEventHandler<T>;
152
- }
153
- export interface ComboboxMenuProps {
154
- menuId: string;
155
- ariaLabelledBy?: string;
156
- ariaLabel?: string;
157
- externalKeyboardNav: boolean;
158
- activeOptionKey?: string | null;
159
- onActiveOptionKeyChange?: (key: string | null) => void;
160
- onReturnFocusToInput?: () => void;
161
- initialFocusRef?: React.RefObject<HTMLElement | null>;
162
- }
163
- /**
164
- * Wires a COMBOBOX trigger — a text input — to the Menu it opens. Focus stays
165
- * on the input and the highlight is virtual, reported through
166
- * `aria-activedescendant`, because the input has to keep receiving the keys it
167
- * filters with.
168
- *
169
- * Driving the highlight from outside Menu means owning the active option as
170
- * state, and owning it means deriving the same option list, initial highlight
171
- * and navigation list that Menu derives internally. Both combobox consumers had
172
- * grown their own copies of all three; they come from Menu itself now.
173
- *
174
- * A menu that carries its own filter field (`menu.allowSearch`) inverts the
175
- * model: navigation lives inside Menu, so the trigger stops driving it and
176
- * hands over an `initialFocusRef` instead. Both shapes come out of this hook.
177
- */
178
- export declare const useComboboxMenuTrigger: ({ pattern, menu, options, canOpen, menuRef, isOpen, onSelectOption, onClose, focusTrigger, triggerAriaLabelledBy, triggerAriaLabel, defaultAriaLabel, }: UseComboboxMenuTriggerArgs) => {
179
- getTriggerProps: <T extends HTMLElement = HTMLElement>(args: GetComboboxTriggerPropsArgs<T>) => ComboboxTriggerProps<T>;
180
- menuProps: ComboboxMenuProps;
181
- activeOptionKey: string | null;
182
- };
183
- //# sourceMappingURL=useMenuTrigger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useMenuTrigger.d.ts","sourceRoot":"","sources":["../../../src/components/Menu/useMenuTrigger.ts"],"names":[],"mappings":"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"}
@@ -1,347 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.useMenuTrigger = exports.useComboboxMenuTrigger = void 0;
7
- var React = _interopRequireWildcard(require("react"));
8
- var _Menu = require("./Menu");
9
- var _menuOptionId = require("./menuOptionId");
10
- var _useMenuKeyboardNavigation = require("./useMenuKeyboardNavigation");
11
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
12
- /**
13
- * Drop undefined-valued keys. The props these hooks return are spread AFTER the
14
- * consumer's own `menu` props, and an own property holding `undefined` would
15
- * erase a value the consumer set — `{...menu, ...{ariaLabel: undefined}}` has no
16
- * label at all.
17
- */
18
- const compact = obj => Object.fromEntries(Object.entries(obj).filter(_ref => {
19
- let [, value] = _ref;
20
- return value !== undefined;
21
- }));
22
-
23
- /**
24
- * Wires a trigger to the Menu it opens.
25
- *
26
- * Three things have to live on the trigger rather than in Menu, because they
27
- * exist while the menu does NOT: `aria-haspopup`, `aria-expanded="false"`, and
28
- * the ArrowDown/ArrowUp that opens the popup in the first place. That is the
29
- * floor — no Menu prop can supply them — so these hooks hand them over as props
30
- * to spread instead, and own everything else: the ids, the tie between the
31
- * trigger's name and the listbox, and the ref Menu focuses into on open.
32
- *
33
- * `useMenuTrigger` is for triggers whose popup options own focus — a button, a
34
- * tab overflow chevron, an inline label — so arrows move real DOM focus.
35
- * `useComboboxMenuTrigger` is for triggers that are text inputs, where focus
36
- * stays put and the highlight is virtual (`aria-activedescendant`).
37
- *
38
- * `getTriggerProps` is a getter rather than a static object because open state
39
- * belongs to the consumer's `ClickAway`, which exposes it through a render
40
- * callback — where a hook cannot be called.
41
- *
42
- * const menuTrigger = useMenuTrigger({id: props.id, ariaLabel: menu?.ariaLabel});
43
- * <ClickAway>
44
- * {({isOpen, onOpen, clickAway}) => (
45
- * <>
46
- * <Button {...props} {...menuTrigger.getTriggerProps({isOpen, onOpen})} />
47
- * {isOpen && <Menu {...menu} {...menuTrigger.menuProps} onTabOut={clickAway} />}
48
- * </>
49
- * )}
50
- * </ClickAway>
51
- */
52
-
53
- /**
54
- * Generic in the trigger element so the returned handler matches whatever it is
55
- * spread onto — inferred from the trigger's own `onKeyDown` where it has one.
56
- */
57
-
58
- const useMenuTrigger = function () {
59
- let {
60
- id,
61
- menuId: consumerMenuId,
62
- ariaLabel,
63
- ariaLabelledBy
64
- } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
65
- const generatedMenuId = React.useId();
66
- const generatedTriggerId = React.useId();
67
- const menuId = consumerMenuId ?? generatedMenuId;
68
- const triggerId = id ?? generatedTriggerId;
69
- const initialFocusRef = React.useRef(null);
70
- const getTriggerProps = _ref2 => {
71
- let {
72
- isOpen,
73
- onOpen,
74
- onKeyDown
75
- } = _ref2;
76
- return {
77
- id: triggerId,
78
- 'aria-haspopup': 'listbox',
79
- 'aria-expanded': isOpen,
80
- // Only while the listbox is actually rendered: an IDREF that resolves to
81
- // nothing is an invalid value, not an empty one.
82
- 'aria-controls': isOpen ? menuId : undefined,
83
- onKeyDown: event => {
84
- onKeyDown?.(event);
85
- // Opening on an arrow key is the trigger's job precisely because the menu
86
- // does not exist yet to handle it. Leave the key alone if the trigger's
87
- // own handler already claimed it.
88
- if (event.defaultPrevented || isOpen) {
89
- return;
90
- }
91
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
92
- event.preventDefault();
93
- onOpen();
94
- }
95
- }
96
- };
97
- };
98
- return {
99
- getTriggerProps,
100
- menuProps: compact({
101
- menuId,
102
- // An explicit label suppresses the idref, so the two never compete.
103
- ariaLabelledBy: ariaLabel ? undefined : ariaLabelledBy ?? triggerId,
104
- ariaLabel,
105
- initialFocusRef
106
- })
107
- };
108
- };
109
-
110
- /**
111
- * Which of ARIA's two combobox patterns the trigger implements.
112
- *
113
- * - `select` — a read-only field standing in for a `<select>`. An option is
114
- * highlighted as soon as the list opens, Space selects it (the field has no
115
- * typing role), and Enter or Space opens the list in the first place.
116
- * - `autocomplete` — an editable field that filters the list as you type.
117
- * Nothing is highlighted until the user arrows to it (APG calls this manual
118
- * selection: auto-highlighting would let Enter select something they never
119
- * chose), Space types a space, and only the arrow keys open the list.
120
- */
121
- exports.useMenuTrigger = useMenuTrigger;
122
- /**
123
- * Wires a COMBOBOX trigger — a text input — to the Menu it opens. Focus stays
124
- * on the input and the highlight is virtual, reported through
125
- * `aria-activedescendant`, because the input has to keep receiving the keys it
126
- * filters with.
127
- *
128
- * Driving the highlight from outside Menu means owning the active option as
129
- * state, and owning it means deriving the same option list, initial highlight
130
- * and navigation list that Menu derives internally. Both combobox consumers had
131
- * grown their own copies of all three; they come from Menu itself now.
132
- *
133
- * A menu that carries its own filter field (`menu.allowSearch`) inverts the
134
- * model: navigation lives inside Menu, so the trigger stops driving it and
135
- * hands over an `initialFocusRef` instead. Both shapes come out of this hook.
136
- */
137
- const useComboboxMenuTrigger = _ref3 => {
138
- let {
139
- pattern = 'select',
140
- menu,
141
- options,
142
- canOpen,
143
- menuRef,
144
- isOpen,
145
- onSelectOption,
146
- onClose,
147
- focusTrigger,
148
- triggerAriaLabelledBy,
149
- triggerAriaLabel,
150
- defaultAriaLabel = 'Options'
151
- } = _ref3;
152
- const generatedMenuId = React.useId();
153
- // The consumer's own id wins: it is what their `aria-controls` elsewhere, or
154
- // their tests, already point at.
155
- const menuId = menu?.menuId ?? generatedMenuId;
156
- const initialFocusRef = React.useRef(null);
157
- const [activeOptionKey, setActiveOptionKey] = React.useState(null);
158
- const isAutocomplete = pattern === 'autocomplete';
159
-
160
- // A searchable menu drives navigation internally from its own filter field; a
161
- // select-only menu is driven by this trigger instead, with focus never
162
- // entering the listbox. An autocomplete trigger IS the filter field, so it
163
- // always drives navigation itself.
164
- const usesExternalNav = isAutocomplete || !menu?.allowSearch;
165
-
166
- /** The options the keyboard navigates, whatever supplied them. */
167
- const optionSource = React.useMemo(() => options ?? (menu ? (0, _Menu.getFlatOptions)(menu) : []), [options, menu]);
168
-
169
- /*
170
- * `listbox` is an accessible-name-required role, so the popup must be named.
171
- * Best name first:
172
- * 1. what the consumer said (`menu.ariaLabelledBy` / `menu.ariaLabel`),
173
- * 2. whatever already labels the trigger — a field label is the most useful
174
- * name there is,
175
- * 3. a literal fallback. Deliberately NOT the placeholder: echoing
176
- * "Select..." names the list after an instruction, not its contents.
177
- * An explicit label suppresses the idref, since a listbox drops `aria-label`
178
- * whenever `aria-labelledby` is present.
179
- */
180
- const menuAriaLabelledBy = menu?.ariaLabelledBy ?? (menu?.ariaLabel ? undefined : triggerAriaLabelledBy);
181
- const menuAriaLabel = menu?.ariaLabel ?? (menuAriaLabelledBy ? undefined : triggerAriaLabel ?? defaultAriaLabel);
182
- const selectByKey = key => {
183
- const option = optionSource.find(opt => opt.key === key);
184
- if (option) {
185
- onSelectOption(option);
186
- }
187
- };
188
-
189
- /*
190
- * A virtualized menu keeps only a window of rows in the DOM, so the keyboard
191
- * hook would otherwise clamp End / PageDown / wrap-around / type-ahead to that
192
- * window. Hand it the real option list in that case — whichever source it came
193
- * from, and flat or grouped alike, since Menu handles both — and Menu
194
- * scrolls the active row into view. Plain menus stay on the DOM-driven path,
195
- * which keeps grouped layouts and `resolveLabel` type-ahead working off the
196
- * rendered text.
197
- */
198
- const resolveLabel = menu?.resolveLabel;
199
- const navigationOptions = React.useMemo(() => {
200
- // A wholly disabled menu has no navigable options at all.
201
- if (!menu?.virtualization?.enable || menu.menuDisabled) {
202
- return undefined;
203
- }
204
- return optionSource.filter(option => !option.disabled).map(option => {
205
- // Type-ahead must match the text the user can actually read.
206
- const resolved = resolveLabel?.(option);
207
- return {
208
- key: option.key,
209
- label: typeof resolved === 'string' ? resolved : option.label ?? ''
210
- };
211
- });
212
- }, [menu?.virtualization?.enable, menu?.menuDisabled, resolveLabel, optionSource]);
213
- const {
214
- onKeyDown: onComboboxKeyDown
215
- } = (0, _useMenuKeyboardNavigation.useMenuKeyboardNavigation)({
216
- containerRef: menuRef,
217
- enabled: isOpen && usesExternalNav && !menu?.menuDisabled,
218
- // An editable trigger keeps printable keys for filtering; a read-only one
219
- // spends them on type-ahead.
220
- mode: isAutocomplete ? 'search' : 'activedescendant',
221
- activeOptionKey,
222
- onSelectKey: selectByKey,
223
- setActiveOptionKey,
224
- options: navigationOptions
225
- });
226
-
227
- /*
228
- * Seed the highlight when the menu opens, drop it when it closes, and keep it
229
- * valid while the option set changes underneath it (async or cascading
230
- * options). Menu skips its own re-validation under `externalKeyboardNav`, so
231
- * the trigger owns it — without this, `aria-activedescendant` could point at
232
- * the id of an option that no longer exists. A layout effect, so the id is in
233
- * place before the browser paints the open menu.
234
- */
235
- const selectedKeys = menu?.selectedKeys;
236
- const selectedOption = menu?.selectedOption;
237
- React.useLayoutEffect(() => {
238
- // A closed list has no active option, whichever pattern this is.
239
- if (!isOpen) {
240
- setActiveOptionKey(null);
241
- return;
242
- }
243
- const enabledKeys = optionSource.filter(option => !option.disabled).map(option => option.key);
244
- const surviving = prev => prev && enabledKeys.includes(prev) ? prev : null;
245
- if (isAutocomplete) {
246
- // Manual selection: never highlight anything on the user's behalf, only
247
- // drop a highlight that the filtering has removed under them.
248
- setActiveOptionKey(surviving);
249
- return;
250
- }
251
- if (!usesExternalNav) {
252
- setActiveOptionKey(null);
253
- return;
254
- }
255
- setActiveOptionKey(prev => surviving(prev) ?? (0, _Menu.getInitialActiveOptionKey)({
256
- options: optionSource,
257
- selectedKeys,
258
- selectedOption
259
- }));
260
- }, [isAutocomplete, isOpen, usesExternalNav, optionSource, selectedKeys, selectedOption]);
261
- const getTriggerProps = _ref4 => {
262
- let {
263
- isOpen: triggerIsOpen,
264
- isExpanded,
265
- onOpen,
266
- onKeyDown
267
- } = _ref4;
268
- const expanded = isExpanded ?? triggerIsOpen;
269
- return {
270
- // The one combobox for both shapes: the menu's own filter field is a
271
- // `searchbox`, so nothing competes for the listbox. Keeping the role
272
- // unconditional also keeps `aria-expanded` valid — it is not an allowed
273
- // attribute on a plain textbox.
274
- role: 'combobox',
275
- 'aria-haspopup': 'listbox',
276
- 'aria-expanded': expanded,
277
- // Only reference the listbox while it is actually rendered. The hook always
278
- // hands `menuId` to Menu, so the element exists whenever `expanded` says so,
279
- // whichever option source was supplied.
280
- 'aria-controls': expanded ? menuId : undefined,
281
- // 'list' whenever typing narrows the list: the autocomplete pattern, or a
282
- // menu carrying its own filter field.
283
- 'aria-autocomplete': isAutocomplete || !usesExternalNav ? 'list' : 'none',
284
- 'aria-activedescendant': expanded && usesExternalNav && activeOptionKey ? (0, _menuOptionId.menuOptionId)(menuId, activeOptionKey) : undefined,
285
- onKeyDown: event => {
286
- onKeyDown?.(event);
287
- if (event.defaultPrevented) {
288
- return;
289
- }
290
- if (!triggerIsOpen) {
291
- // Closed: the arrow keys open the list, and a read-only trigger takes
292
- // Enter and Space for it too — a typed space belongs to an editable
293
- // one. This is the part no Menu prop can supply: the menu does not
294
- // exist yet.
295
- const opensList = event.key === 'ArrowDown' || event.key === 'ArrowUp' || !isAutocomplete && (event.key === 'Enter' || event.key === ' ');
296
- if (opensList && (canOpen?.() ?? true)) {
297
- event.preventDefault();
298
- onOpen();
299
- }
300
- return;
301
- }
302
- // Open + searchable: focus is in the menu's own filter field, which owns
303
- // navigation — nothing to do here.
304
- if (!usesExternalNav) {
305
- return;
306
- }
307
- // Tab moves into the menu's footer, Shift+Tab into its header, when that
308
- // region has focusable controls. With none there, the listbox closes
309
- // (APG: the popup closes when the combobox loses focus) and focus moves
310
- // on.
311
- if (event.key === 'Tab') {
312
- if ((0, _useMenuKeyboardNavigation.moveFocusToMenuChrome)(menuRef.current, event.shiftKey)) {
313
- event.preventDefault();
314
- } else {
315
- onClose();
316
- }
317
- return;
318
- }
319
- // Space selects the active option — only where it has no typing role.
320
- if (!isAutocomplete && event.key === ' ') {
321
- event.preventDefault();
322
- if (activeOptionKey) {
323
- selectByKey(activeOptionKey);
324
- }
325
- return;
326
- }
327
- // Arrows / Home / End / Enter / type-ahead.
328
- onComboboxKeyDown(event);
329
- }
330
- };
331
- };
332
- return {
333
- getTriggerProps,
334
- menuProps: compact({
335
- menuId,
336
- ariaLabelledBy: menuAriaLabelledBy,
337
- ariaLabel: menuAriaLabel,
338
- externalKeyboardNav: usesExternalNav,
339
- activeOptionKey: usesExternalNav ? activeOptionKey : undefined,
340
- onActiveOptionKeyChange: usesExternalNav ? setActiveOptionKey : undefined,
341
- onReturnFocusToInput: usesExternalNav ? focusTrigger : undefined,
342
- initialFocusRef: usesExternalNav ? undefined : initialFocusRef
343
- }),
344
- activeOptionKey
345
- };
346
- };
347
- exports.useComboboxMenuTrigger = useComboboxMenuTrigger;
@@ -1,2 +0,0 @@
1
- export * from '../../hooks/useInteractionModality/useInteractionModality';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/useInteractionModality/index.ts"],"names":[],"mappings":"AAAA,cAAc,yDAAyD,CAAC"}
@@ -1,16 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- var _useInteractionModality = require("./useInteractionModality");
7
- Object.keys(_useInteractionModality).forEach(function (key) {
8
- if (key === "default" || key === "__esModule") return;
9
- if (key in exports && exports[key] === _useInteractionModality[key]) return;
10
- Object.defineProperty(exports, key, {
11
- enumerable: true,
12
- get: function () {
13
- return _useInteractionModality[key];
14
- }
15
- });
16
- });
@@ -1,15 +0,0 @@
1
- export type InteractionModality = 'keyboard' | 'pointer';
2
- /**
3
- * How the user last interacted with the page, read imperatively.
4
- *
5
- * Use this when the answer is needed during render or in an effect rather than
6
- * as reactive state — for example a popup deciding on mount whether it was opened
7
- * by keyboard, where re-rendering on later modality changes would be wrong.
8
- */
9
- export declare const getInteractionModality: () => InteractionModality;
10
- /**
11
- * How the user last interacted with the page, as reactive state. Re-renders the
12
- * caller when the user switches between keyboard and pointer.
13
- */
14
- export declare const useInteractionModality: () => InteractionModality;
15
- //# sourceMappingURL=useInteractionModality.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useInteractionModality.d.ts","sourceRoot":"","sources":["../../../src/hooks/useInteractionModality/useInteractionModality.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,SAAS,CAAC;AA6FzD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,QAAO,mBAGzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,QAAO,mBAMzC,CAAC"}