@lumx/react 4.3.2-alpha.13 → 4.3.2-alpha.14

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.
@@ -0,0 +1,1805 @@
1
+ import React__default, { useContext, useMemo, createContext, useEffect, useRef } from 'react';
2
+ import { jsx, Fragment } from 'react/jsx-runtime';
3
+ import isEmpty from 'lodash/isEmpty';
4
+ import { join, visuallyHidden } from '@lumx/core/js/utils/classNames';
5
+ import { createPortal } from 'react-dom';
6
+ import noop from 'lodash/noop';
7
+ import uniqueId from 'lodash/uniqueId';
8
+ import findLast from 'lodash/findLast';
9
+ import find from 'lodash/find';
10
+ import findLastIndex from 'lodash/findLastIndex';
11
+ import isNil from 'lodash/isNil';
12
+ import groupBy from 'lodash/groupBy';
13
+
14
+ const DisabledStateContext = /*#__PURE__*/React__default.createContext({
15
+ state: null
16
+ });
17
+ /**
18
+ * Disabled state provider.
19
+ * All nested LumX Design System components inherit this disabled state.
20
+ */
21
+ function DisabledStateProvider({
22
+ children,
23
+ ...value
24
+ }) {
25
+ return /*#__PURE__*/jsx(DisabledStateContext.Provider, {
26
+ value: value,
27
+ children: children
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Get DisabledState context value
33
+ */
34
+ function useDisabledStateContext() {
35
+ return useContext(DisabledStateContext);
36
+ }
37
+
38
+ /** Context to store the refs of the combobox elements */
39
+ const ComboboxRefsContext = /*#__PURE__*/createContext({
40
+ triggerRef: {
41
+ current: null
42
+ },
43
+ anchorRef: {
44
+ current: null
45
+ }
46
+ });
47
+ /** Provider to store the required refs for the Combobox */
48
+ const ComboboxRefsProvider = ({
49
+ triggerRef,
50
+ anchorRef,
51
+ children
52
+ }) => {
53
+ const value = useMemo(() => ({
54
+ triggerRef,
55
+ anchorRef
56
+ }), [triggerRef, anchorRef]);
57
+ return /*#__PURE__*/jsx(ComboboxRefsContext.Provider, {
58
+ value: value,
59
+ children: children
60
+ });
61
+ };
62
+
63
+ /** Retrieves the combobox elements references from context */
64
+ const useComboboxRefs = () => {
65
+ const refs = useContext(ComboboxRefsContext);
66
+ if (!refs) {
67
+ throw new Error('The useComboboxRefs hook must be called within a ComboboxRefsProvider');
68
+ }
69
+ return refs;
70
+ };
71
+
72
+ const EVENT_TYPES = ['mousedown', 'touchstart'];
73
+ function isClickAway(targets, refs) {
74
+ // The targets elements are not contained in any of the listed element references.
75
+ return !refs.some(ref => targets.some(target => ref?.current?.contains(target)));
76
+ }
77
+ /**
78
+ * Listen to clicks away from the given elements and callback the passed in function.
79
+ *
80
+ * Warning: If you need to detect click away on nested React portals, please use the `ClickAwayProvider` component.
81
+ */
82
+ function useClickAway({
83
+ callback,
84
+ childrenRefs
85
+ }) {
86
+ useEffect(() => {
87
+ const {
88
+ current: currentRefs
89
+ } = childrenRefs;
90
+ if (!callback || !currentRefs || isEmpty(currentRefs)) {
91
+ return undefined;
92
+ }
93
+ const listener = evt => {
94
+ const targets = [evt.composedPath?.()[0], evt.target];
95
+ if (isClickAway(targets, currentRefs)) {
96
+ callback(evt);
97
+ }
98
+ };
99
+ EVENT_TYPES.forEach(evtType => document.addEventListener(evtType, listener));
100
+ return () => {
101
+ EVENT_TYPES.forEach(evtType => document.removeEventListener(evtType, listener));
102
+ };
103
+ }, [callback, childrenRefs]);
104
+ }
105
+
106
+ const ClickAwayAncestorContext = /*#__PURE__*/createContext(null);
107
+ /**
108
+ * Component combining the `useClickAway` hook with a React context to hook into the React component tree and make sure
109
+ * we take into account both the DOM tree and the React tree to detect click away.
110
+ *
111
+ * @return the react component.
112
+ */
113
+ const ClickAwayProvider = ({
114
+ children,
115
+ callback,
116
+ childrenRefs,
117
+ parentRef
118
+ }) => {
119
+ const parentContext = useContext(ClickAwayAncestorContext);
120
+ const currentContext = useMemo(() => {
121
+ const context = {
122
+ childrenRefs: [],
123
+ /**
124
+ * Add element refs to the current context and propagate to the parent context.
125
+ */
126
+ addRefs(...newChildrenRefs) {
127
+ // Add element refs that should be considered as inside the click away context.
128
+ context.childrenRefs.push(...newChildrenRefs);
129
+ if (parentContext) {
130
+ // Also add then to the parent context
131
+ parentContext.addRefs(...newChildrenRefs);
132
+ if (parentRef) {
133
+ // The parent element is also considered as inside the parent click away context but not inside the current context
134
+ parentContext.addRefs(parentRef);
135
+ }
136
+ }
137
+ }
138
+ };
139
+ return context;
140
+ }, [parentContext, parentRef]);
141
+ useEffect(() => {
142
+ const {
143
+ current: currentRefs
144
+ } = childrenRefs;
145
+ if (!currentRefs) {
146
+ return;
147
+ }
148
+ currentContext.addRefs(...currentRefs);
149
+ }, [currentContext, childrenRefs]);
150
+ useClickAway({
151
+ callback,
152
+ childrenRefs: useRef(currentContext.childrenRefs)
153
+ });
154
+ return /*#__PURE__*/jsx(ClickAwayAncestorContext.Provider, {
155
+ value: currentContext,
156
+ children: children
157
+ });
158
+ };
159
+ ClickAwayProvider.displayName = 'ClickAwayProvider';
160
+
161
+ /**
162
+ * Live region to read a message to screen readers.
163
+ * Messages can be "polite" and be read when possible or
164
+ * "assertive" and interrupt any message currently be read. (To be used sparingly)
165
+ *
166
+ * More information here: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
167
+ *
168
+ * @family A11Y
169
+ * @param A11YLiveMessageProps
170
+ * @returns A11YLiveMessage
171
+ */
172
+ const A11YLiveMessage = ({
173
+ type = 'polite',
174
+ atomic,
175
+ role,
176
+ hidden,
177
+ relevant,
178
+ children,
179
+ className,
180
+ ...forwardedProps
181
+ }) => {
182
+ return /*#__PURE__*/jsx("div", {
183
+ ...forwardedProps,
184
+ className: join(hidden ? visuallyHidden() : undefined, className),
185
+ role: role,
186
+ "aria-live": type,
187
+ "aria-atomic": atomic,
188
+ "aria-relevant": relevant,
189
+ children: children
190
+ });
191
+ };
192
+
193
+ /**
194
+ * Portal initializing function.
195
+ * If it does not provide a container, the Portal children will render in classic React tree and not in a portal.
196
+ */
197
+
198
+ const PortalContext = /*#__PURE__*/React__default.createContext(() => ({
199
+ container: document.body
200
+ }));
201
+ /**
202
+ * Customize where <Portal> wrapped elements render (tooltip, popover, dialog, etc.)
203
+ */
204
+ const PortalProvider = PortalContext.Provider;
205
+
206
+ /**
207
+ * Render children in a portal outside the current DOM position
208
+ * (defaults to `document.body` but can be customized with the PortalContextProvider)
209
+ */
210
+ const Portal = ({
211
+ children,
212
+ enabled = true
213
+ }) => {
214
+ const init = React__default.useContext(PortalContext);
215
+ const context = React__default.useMemo(() => {
216
+ return enabled ? init() : null;
217
+ },
218
+ // Only update on 'enabled'
219
+ // eslint-disable-next-line react-hooks/exhaustive-deps
220
+ [enabled]);
221
+ React__default.useLayoutEffect(() => {
222
+ return context?.teardown;
223
+ }, [context?.teardown, enabled]);
224
+ if (!context?.container) {
225
+ return /*#__PURE__*/jsx(Fragment, {
226
+ children: children
227
+ });
228
+ }
229
+ return /*#__PURE__*/createPortal(children, context.container);
230
+ };
231
+
232
+ /**
233
+ * Create a map from the given tab stop to sort them by rowKey;
234
+ */
235
+ function createGridMap(tabStops) {
236
+ /** Group all tabStop by rows to easily access them by their row keys */
237
+ const tabStopsByRowKey = groupBy(tabStops, 'rowKey');
238
+ /**
239
+ * An array with each row key in the order set in the tabStops array.
240
+ * Each rowKey will only appear once.
241
+ */
242
+ const rowKeys = tabStops.reduce((acc, {
243
+ rowKey
244
+ }) => {
245
+ if (!isNil(rowKey) && !acc.includes(rowKey)) {
246
+ return [...acc, rowKey];
247
+ }
248
+ return acc;
249
+ }, []);
250
+ return {
251
+ tabStopsByRowKey,
252
+ rowKeys
253
+ };
254
+ }
255
+
256
+ /** Check if the given tab stop is enabled */
257
+ const tabStopIsEnabled = tabStop => !tabStop.disabled;
258
+
259
+ const LOOP_AROUND_TYPES = {
260
+ /**
261
+ * Will continue navigation to the next row or column and loop back to the start
262
+ * when the last tab stop is reached
263
+ */
264
+ nextLoop: 'next-loop',
265
+ /**
266
+ * Will continue navigation to the next row or column until
267
+ * the last tab stop is reached
268
+ */
269
+ nextEnd: 'next-end',
270
+ /**
271
+ * Will loop within the current row or column
272
+ */
273
+ inside: 'inside'
274
+ };
275
+ const CELL_SEARCH_DIRECTION = {
276
+ /** Look ahead of the given position */
277
+ asc: 'asc',
278
+ /** Look before the given position */
279
+ desc: 'desc'
280
+ };
281
+
282
+ /**
283
+ * Build a loopAround configuration to ensure both row and col behavior are set.
284
+ *
285
+ * Setting a boolean will set the following behaviors:
286
+ *
287
+ * * true => { row: 'next-loop', col: 'next-loop' }
288
+ * * false => { row: 'next-end', col: 'next-end' }
289
+ */
290
+ function buildLoopAroundObject(loopAround) {
291
+ if (typeof loopAround === 'boolean' || loopAround === undefined) {
292
+ const newLoopAround = loopAround ? {
293
+ row: LOOP_AROUND_TYPES.nextLoop,
294
+ col: LOOP_AROUND_TYPES.nextLoop
295
+ } : {
296
+ row: LOOP_AROUND_TYPES.nextEnd,
297
+ col: LOOP_AROUND_TYPES.nextEnd
298
+ };
299
+ return newLoopAround;
300
+ }
301
+ return loopAround;
302
+ }
303
+
304
+ /**
305
+ * Check that the given coordinate is a simple number
306
+ */
307
+ const isNumberCoords = coords => typeof coords === 'number';
308
+
309
+ /**
310
+ * Check that the given coordinate is a direction
311
+ */
312
+ function isDirectionCoords(coords) {
313
+ return Boolean(typeof coords !== 'number' && typeof coords?.from === 'number');
314
+ }
315
+
316
+ /**
317
+ * Search the given column of a grid map for a cell.
318
+ */
319
+ function findCellInCol(gridMap, col, rowCoords, cellSelector = tabStopIsEnabled) {
320
+ /** The rowIndex might not be strictly successive, so we need to use the actual row index keys. */
321
+ const {
322
+ rowKeys,
323
+ tabStopsByRowKey
324
+ } = gridMap;
325
+ const lastIndex = rowKeys.length - 1;
326
+ /**
327
+ * If the rowCoords.from is set at -1, it means we should search from the start/end.
328
+ */
329
+ let searchIndex = rowCoords.from;
330
+ if (searchIndex === -1) {
331
+ searchIndex = rowCoords.direction === CELL_SEARCH_DIRECTION.desc ? lastIndex : 0;
332
+ }
333
+ const searchCellFunc = rowCoords.direction === CELL_SEARCH_DIRECTION.desc ? findLast : find;
334
+ const rowKeyWithEnabledCell = searchCellFunc(rowKeys, (rowKey, index) => {
335
+ const row = tabStopsByRowKey[rowKey];
336
+ const cell = row[col];
337
+ const hasCell = Boolean(cell);
338
+ const cellRowIndex = index;
339
+
340
+ /** Check that the target row index is in the right direction of the search */
341
+ const correctRowIndex = rowCoords.direction === CELL_SEARCH_DIRECTION.desc ? cellRowIndex <= searchIndex : cellRowIndex >= searchIndex;
342
+ if (cell && correctRowIndex) {
343
+ return cellSelector ? hasCell && cellSelector(cell) : hasCell;
344
+ }
345
+ return false;
346
+ });
347
+ const row = rowKeyWithEnabledCell !== undefined ? tabStopsByRowKey[rowKeyWithEnabledCell] : undefined;
348
+ return row?.[col];
349
+ }
350
+
351
+ /**
352
+ * Search the given column of a grid map for a cell.
353
+ */
354
+ function findCellInRow(gridMap, row, colCoords, cellSelector = tabStopIsEnabled) {
355
+ const {
356
+ direction,
357
+ from
358
+ } = colCoords || {};
359
+ const {
360
+ rowKeys,
361
+ tabStopsByRowKey
362
+ } = gridMap;
363
+ const rowKey = rowKeys[row];
364
+ const currentRow = tabStopsByRowKey[rowKey];
365
+ if (!currentRow) {
366
+ return undefined;
367
+ }
368
+ const searchCellFunc = direction === CELL_SEARCH_DIRECTION.desc ? findLast : find;
369
+ const cell = searchCellFunc(currentRow, cellSelector, from);
370
+ return cell;
371
+ }
372
+
373
+ /**
374
+ * Parse each column of the given gridMap to find the first cell matching the selector.
375
+ * The direction and starting point of the search can be set using the coordinates attribute.
376
+ */
377
+ function parseColsForCell(/** The gridMap to search */
378
+ gridMap, /** The coordinate to search */
379
+ {
380
+ direction = CELL_SEARCH_DIRECTION.asc,
381
+ from
382
+ }, cellSelector = tabStopIsEnabled) {
383
+ if (from === undefined) {
384
+ return undefined;
385
+ }
386
+ const {
387
+ rowKeys,
388
+ tabStopsByRowKey
389
+ } = gridMap;
390
+
391
+ /** As we cannot know for certain when to stop, we need to know which column is the last column */
392
+ const maxColIndex = rowKeys.reduce((maxLength, rowIndex) => {
393
+ const rowLength = tabStopsByRowKey[rowIndex].length;
394
+ return rowLength > maxLength ? rowLength - 1 : maxLength;
395
+ }, 0);
396
+
397
+ /** If "from" is set as -1, start from the end. */
398
+ const fromIndex = from === -1 ? maxColIndex : from || 0;
399
+ for (let index = fromIndex; direction === CELL_SEARCH_DIRECTION.desc ? index > -1 : index <= maxColIndex; direction === CELL_SEARCH_DIRECTION.desc ? index -= 1 : index += 1) {
400
+ const rowWithEnabledCed = findCellInCol(gridMap, index, {
401
+ direction,
402
+ from: direction === CELL_SEARCH_DIRECTION.desc ? -1 : 0
403
+ }, cellSelector);
404
+ if (rowWithEnabledCed) {
405
+ return rowWithEnabledCed;
406
+ }
407
+ }
408
+ return undefined;
409
+ }
410
+
411
+ /**
412
+ * Search for a cell in a gridMap
413
+ *
414
+ * This allows you to
415
+ * * Select a cell at a specific coordinate
416
+ * * Search for a cell from a specific column in any direction
417
+ * * Search for a cell from a specific row in any direction
418
+ *
419
+ * If no cell is found, returns undefined
420
+ */
421
+ function getCell(/** The gridMap object to search in. */
422
+ gridMap, /** The coordinates of the cell to select */
423
+ coords,
424
+ /**
425
+ * A selector function to select the cell.
426
+ * Selects enabled cells by default.
427
+ */
428
+ cellSelector = tabStopIsEnabled) {
429
+ const {
430
+ row,
431
+ col
432
+ } = coords || {};
433
+ const {
434
+ rowKeys,
435
+ tabStopsByRowKey
436
+ } = gridMap || {};
437
+
438
+ /** Defined row and col */
439
+ if (isNumberCoords(row) && isNumberCoords(col)) {
440
+ const rowKey = rowKeys[row];
441
+ return tabStopsByRowKey[rowKey][col];
442
+ }
443
+
444
+ /** Defined row but variable col */
445
+ if (isDirectionCoords(col) && isNumberCoords(row)) {
446
+ return findCellInRow(gridMap, row, col, cellSelector);
447
+ }
448
+ if (isDirectionCoords(row)) {
449
+ if (isDirectionCoords(col)) {
450
+ return parseColsForCell(gridMap, col, cellSelector);
451
+ }
452
+ return findCellInCol(gridMap, col, row, cellSelector);
453
+ }
454
+ return undefined;
455
+ }
456
+
457
+ function getCellCoordinates(gridMap, tabStop) {
458
+ const currentRowKey = tabStop.rowKey;
459
+ if (isNil(currentRowKey)) {
460
+ return undefined;
461
+ }
462
+ const {
463
+ rowKeys,
464
+ tabStopsByRowKey
465
+ } = gridMap;
466
+ const rowIndex = rowKeys.findIndex(rowKey => rowKey === currentRowKey);
467
+ const row = tabStopsByRowKey[currentRowKey];
468
+ const columnOffset = row.findIndex(ts => ts.id === tabStop.id);
469
+ return {
470
+ rowIndex,
471
+ row,
472
+ columnOffset
473
+ };
474
+ }
475
+
476
+ /** Check whether the list should vertically loop with the given configuration */
477
+ function shouldLoopListVertically(direction, loopAround) {
478
+ return direction === 'vertical' && loopAround?.col !== 'next-end' || direction === 'both' && loopAround?.col !== 'next-end';
479
+ }
480
+
481
+ /** Check whether the list should horizontally loop with the given configuration */
482
+ function shouldLoopListHorizontally(direction, loopAround) {
483
+ return direction === 'horizontal' && loopAround?.row !== 'next-end' || direction === 'both' && loopAround?.row !== 'next-end';
484
+ }
485
+
486
+ /**
487
+ * Get the correct pointer type from the given event.
488
+ * This is used when a tab stop is selected, to check if is has been selected using a keyboard or a pointer
489
+ * (pen / mouse / touch)
490
+ */
491
+ function getPointerTypeFromEvent(event) {
492
+ return event && 'pointerType' in event && Boolean(event.pointerType) ? 'pointer' : 'keyboard';
493
+ }
494
+
495
+ // Event keys used for keyboard navigation.
496
+ const VERTICAL_NAV_KEYS = ['ArrowUp', 'ArrowDown', 'Home', 'End'];
497
+ const HORIZONTAL_NAV_KEYS = ['ArrowLeft', 'ArrowRight', 'Home', 'End'];
498
+ const KEY_NAV_KEYS = [...HORIZONTAL_NAV_KEYS, ...VERTICAL_NAV_KEYS];
499
+ const NAV_KEYS = {
500
+ both: KEY_NAV_KEYS,
501
+ vertical: VERTICAL_NAV_KEYS,
502
+ horizontal: HORIZONTAL_NAV_KEYS
503
+ };
504
+
505
+ // Event keys union type
506
+
507
+ // Handle all navigation moves resulting in a new state.
508
+ const MOVES = {
509
+ // Move to the next item.
510
+ // The grid is flatten so the item after the last of a row will be the
511
+ // first item of the next row.
512
+ NEXT(state, _, index) {
513
+ for (let i = index + 1; i < state.tabStops.length; ++i) {
514
+ const tabStop = state.tabStops[i];
515
+ if (!tabStop.disabled) {
516
+ return {
517
+ ...state,
518
+ allowFocusing: true,
519
+ selectedId: tabStop.id
520
+ };
521
+ }
522
+ }
523
+ return state;
524
+ },
525
+ // Move to the previous item.
526
+ // The grid is flatten so the item before the first of a row will be the
527
+ // last item of the previous row.
528
+ PREVIOUS(state, _, index) {
529
+ for (let i = index - 1; i >= 0; --i) {
530
+ const tabStop = state.tabStops[i];
531
+ if (!tabStop.disabled) {
532
+ return {
533
+ ...state,
534
+ allowFocusing: true,
535
+ selectedId: tabStop.id
536
+ };
537
+ }
538
+ }
539
+ return state;
540
+ },
541
+ // Moving to the next row
542
+ // We move to the next row, and we stay in the same column.
543
+ // If we are in the last row, then we move to the first not disabled item of the next column.
544
+ NEXT_ROW(state, currentTabStop) {
545
+ const currentRowKey = currentTabStop.rowKey;
546
+ if (isNil(currentRowKey)) {
547
+ return state;
548
+ }
549
+ const gridMap = state.gridMap || createGridMap(state.tabStops);
550
+ const cellCoordinates = getCellCoordinates(gridMap, currentTabStop);
551
+ if (!cellCoordinates) {
552
+ return state;
553
+ }
554
+ const {
555
+ rowIndex,
556
+ columnOffset
557
+ } = cellCoordinates;
558
+ const nextRow = rowIndex + 1;
559
+
560
+ /** First try to get the next cell in the current column */
561
+ let tabStop = getCell(gridMap, {
562
+ row: {
563
+ from: nextRow,
564
+ direction: 'asc'
565
+ },
566
+ col: columnOffset
567
+ });
568
+
569
+ // If none were found, search for the next cell depending on the loop around parameter
570
+ if (!tabStop) {
571
+ switch (state.loopAround.col) {
572
+ /**
573
+ * If columns are configured to be looped inside,
574
+ * get the first enabled cell of the current column
575
+ */
576
+ case LOOP_AROUND_TYPES.inside:
577
+ tabStop = getCell(gridMap, {
578
+ col: columnOffset,
579
+ row: {
580
+ from: 0,
581
+ direction: 'asc'
582
+ }
583
+ });
584
+ break;
585
+ /**
586
+ * If columns are configured to be go to the next,
587
+ * search for the next enabled cell from the next column
588
+ */
589
+ case LOOP_AROUND_TYPES.nextEnd:
590
+ case LOOP_AROUND_TYPES.nextLoop:
591
+ default:
592
+ tabStop = getCell(gridMap, {
593
+ row: {
594
+ from: 0,
595
+ direction: 'asc'
596
+ },
597
+ col: {
598
+ from: columnOffset + 1,
599
+ direction: 'asc'
600
+ }
601
+ });
602
+ break;
603
+ }
604
+ }
605
+
606
+ /**
607
+ * If still none is found and the columns are configured to loop
608
+ * search starting from the start
609
+ */
610
+ if (!tabStop && state.loopAround.col === LOOP_AROUND_TYPES.nextLoop) {
611
+ tabStop = getCell(gridMap, {
612
+ row: {
613
+ from: 0,
614
+ direction: 'asc'
615
+ },
616
+ col: {
617
+ from: 0,
618
+ direction: 'asc'
619
+ }
620
+ });
621
+ }
622
+ if (tabStop) {
623
+ return {
624
+ ...state,
625
+ allowFocusing: true,
626
+ selectedId: tabStop.id,
627
+ gridMap
628
+ };
629
+ }
630
+ return {
631
+ ...state,
632
+ allowFocusing: true,
633
+ gridMap
634
+ };
635
+ },
636
+ // Moving to the previous row
637
+ // We move to the previous row, and we stay in the same column.
638
+ // If we are in the first row, then we move to the last not disabled item of the previous column.
639
+ PREVIOUS_ROW(state, currentTabStop) {
640
+ const currentRowKey = currentTabStop.rowKey;
641
+ if (isNil(currentRowKey)) {
642
+ return state;
643
+ }
644
+ const gridMap = state.gridMap || createGridMap(state.tabStops);
645
+ const cellCoordinates = getCellCoordinates(gridMap, currentTabStop);
646
+ if (!cellCoordinates) {
647
+ return state;
648
+ }
649
+ const {
650
+ rowIndex,
651
+ columnOffset
652
+ } = cellCoordinates;
653
+ const previousRow = rowIndex - 1;
654
+ let tabStop;
655
+ /** Search for the previous enabled cell in the current column */
656
+ if (previousRow >= 0) {
657
+ tabStop = getCell(gridMap, {
658
+ row: {
659
+ from: previousRow,
660
+ direction: 'desc'
661
+ },
662
+ col: columnOffset
663
+ });
664
+ }
665
+
666
+ // If none were found, search for the previous cell depending on the loop around parameter
667
+ if (!tabStop) {
668
+ switch (state.loopAround.col) {
669
+ /**
670
+ * If columns are configured to be looped inside,
671
+ * get the last enabled cell of the current column
672
+ */
673
+ case LOOP_AROUND_TYPES.inside:
674
+ tabStop = getCell(gridMap, {
675
+ col: columnOffset,
676
+ row: {
677
+ from: -1,
678
+ direction: 'desc'
679
+ }
680
+ });
681
+ break;
682
+ /**
683
+ * If columns are configured to be go to the previous,
684
+ * search for the last enabled cell from the previous column
685
+ */
686
+ case LOOP_AROUND_TYPES.nextEnd:
687
+ case LOOP_AROUND_TYPES.nextLoop:
688
+ default:
689
+ if (columnOffset - 1 >= 0) {
690
+ tabStop = getCell(gridMap, {
691
+ row: {
692
+ from: -1,
693
+ direction: 'desc'
694
+ },
695
+ col: {
696
+ from: columnOffset - 1,
697
+ direction: 'desc'
698
+ }
699
+ });
700
+ break;
701
+ }
702
+ }
703
+ }
704
+ /**
705
+ * If still none is found and the columns are configured to loop
706
+ * search starting from the end
707
+ */
708
+ if (!tabStop && state.loopAround.col === LOOP_AROUND_TYPES.nextLoop) {
709
+ tabStop = getCell(gridMap, {
710
+ row: {
711
+ from: -1,
712
+ direction: 'desc'
713
+ },
714
+ col: {
715
+ from: -1,
716
+ direction: 'desc'
717
+ }
718
+ });
719
+ }
720
+ if (tabStop) {
721
+ return {
722
+ ...state,
723
+ allowFocusing: true,
724
+ selectedId: tabStop.id,
725
+ gridMap
726
+ };
727
+ }
728
+ return {
729
+ ...state,
730
+ allowFocusing: true,
731
+ gridMap
732
+ };
733
+ },
734
+ // Moving to the very first not disabled item of the list
735
+ VERY_FIRST(state) {
736
+ // The very first not disabled item' index.
737
+ const tabStop = state.tabStops.find(tabStopIsEnabled);
738
+ if (tabStop) {
739
+ return {
740
+ ...state,
741
+ allowFocusing: true,
742
+ selectedId: tabStop.id
743
+ };
744
+ }
745
+ return state;
746
+ },
747
+ // Moving to the very last not disabled item of the list
748
+ VERY_LAST(state) {
749
+ // The very last not disabled item' index.
750
+ const tabStop = findLast(state.tabStops, tabStopIsEnabled);
751
+ if (tabStop) {
752
+ return {
753
+ ...state,
754
+ allowFocusing: true,
755
+ selectedId: tabStop.id
756
+ };
757
+ }
758
+ return state;
759
+ },
760
+ NEXT_COLUMN(state, currentTabStop, index) {
761
+ const currentRowKey = currentTabStop.rowKey;
762
+ if (isNil(currentRowKey)) {
763
+ return state;
764
+ }
765
+ const gridMap = state.gridMap || createGridMap(state.tabStops);
766
+ const cellCoordinates = getCellCoordinates(gridMap, currentTabStop);
767
+ if (!cellCoordinates) {
768
+ return state;
769
+ }
770
+ const {
771
+ rowIndex,
772
+ columnOffset
773
+ } = cellCoordinates;
774
+ // Parse the current row and look for the next enabled cell
775
+ let tabStop = getCell(gridMap, {
776
+ row: rowIndex,
777
+ col: {
778
+ from: columnOffset + 1,
779
+ direction: 'asc'
780
+ }
781
+ });
782
+
783
+ // If none were found, search for the next cell depending on the loop around parameter
784
+ if (!tabStop) {
785
+ switch (state.loopAround.row) {
786
+ /**
787
+ * If rows are configured to be looped inside,
788
+ * get the first enabled cell of the current rows
789
+ */
790
+ case LOOP_AROUND_TYPES.inside:
791
+ tabStop = getCell(gridMap, {
792
+ row: rowIndex,
793
+ col: {
794
+ from: 0,
795
+ direction: 'asc'
796
+ }
797
+ });
798
+ break;
799
+ /**
800
+ * If rows are configured to be go to the next,
801
+ * search for the next enabled cell from the next row
802
+ */
803
+ case LOOP_AROUND_TYPES.nextEnd:
804
+ case LOOP_AROUND_TYPES.nextLoop:
805
+ default:
806
+ tabStop = find(state.tabStops, tabStopIsEnabled, index + 1);
807
+ break;
808
+ }
809
+ }
810
+ /**
811
+ * If still none is found and the row are configured to loop
812
+ * search starting from the start
813
+ */
814
+ if (!tabStop && state.loopAround.row === LOOP_AROUND_TYPES.nextLoop) {
815
+ tabStop = find(state.tabStops, tabStopIsEnabled);
816
+ }
817
+ return {
818
+ ...state,
819
+ allowFocusing: true,
820
+ selectedId: tabStop?.id || state.selectedId,
821
+ gridMap
822
+ };
823
+ },
824
+ PREVIOUS_COLUMN(state, currentTabStop, index) {
825
+ const currentRowKey = currentTabStop.rowKey;
826
+ if (isNil(currentRowKey)) {
827
+ return state;
828
+ }
829
+ const gridMap = state.gridMap || createGridMap(state.tabStops);
830
+ const cellCoordinates = getCellCoordinates(gridMap, currentTabStop);
831
+ if (!cellCoordinates) {
832
+ return state;
833
+ }
834
+ const {
835
+ rowIndex,
836
+ columnOffset
837
+ } = cellCoordinates;
838
+ const previousColumn = columnOffset - 1;
839
+ let tabStop;
840
+ if (previousColumn >= 0) {
841
+ // Parse the current row and look for the next enable cell
842
+ tabStop = getCell(gridMap, {
843
+ row: rowIndex,
844
+ col: {
845
+ from: previousColumn,
846
+ direction: 'desc'
847
+ }
848
+ });
849
+ }
850
+ if (!tabStop) {
851
+ switch (state.loopAround.row) {
852
+ /**
853
+ * If rows are configured to be looped inside,
854
+ * get the last enabled cell of the current row
855
+ */
856
+ case LOOP_AROUND_TYPES.inside:
857
+ tabStop = getCell(gridMap, {
858
+ row: rowIndex,
859
+ col: {
860
+ from: -1,
861
+ direction: 'desc'
862
+ }
863
+ });
864
+ break;
865
+ /**
866
+ * If rows are configured to be go to the next,
867
+ * search for the previous enabled cell from the previous row
868
+ */
869
+ case LOOP_AROUND_TYPES.nextEnd:
870
+ case LOOP_AROUND_TYPES.nextLoop:
871
+ default:
872
+ if (index - 1 >= 0) {
873
+ tabStop = findLast(state.tabStops, tabStopIsEnabled, index - 1);
874
+ }
875
+ break;
876
+ }
877
+ }
878
+ /**
879
+ * If still none is found and the rows are configured to loop
880
+ * search starting from the end
881
+ */
882
+ if (!tabStop && state.loopAround.row === LOOP_AROUND_TYPES.nextLoop) {
883
+ tabStop = findLast(state.tabStops, tabStopIsEnabled);
884
+ }
885
+ return {
886
+ ...state,
887
+ allowFocusing: true,
888
+ selectedId: tabStop?.id || state.selectedId,
889
+ gridMap
890
+ };
891
+ },
892
+ FIRST_IN_COLUMN(state, currentTabStop) {
893
+ const currentRowKey = currentTabStop.rowKey;
894
+ if (isNil(currentRowKey)) {
895
+ return state;
896
+ }
897
+ const gridMap = state.gridMap || createGridMap(state.tabStops);
898
+ const cellCoordinates = getCellCoordinates(gridMap, currentTabStop);
899
+ if (!cellCoordinates) {
900
+ return state;
901
+ }
902
+ const {
903
+ columnOffset
904
+ } = cellCoordinates;
905
+ const tabStop = getCell(gridMap, {
906
+ col: columnOffset,
907
+ row: {
908
+ from: 0,
909
+ direction: 'asc'
910
+ }
911
+ });
912
+ return {
913
+ ...state,
914
+ allowFocusing: true,
915
+ selectedId: tabStop?.id || state.selectedId,
916
+ gridMap
917
+ };
918
+ },
919
+ LAST_IN_COLUMN(state, currentTabStop) {
920
+ const currentRowKey = currentTabStop.rowKey;
921
+ if (isNil(currentRowKey)) {
922
+ return state;
923
+ }
924
+ const gridMap = state.gridMap || createGridMap(state.tabStops);
925
+ const cellCoordinates = getCellCoordinates(gridMap, currentTabStop);
926
+ if (!cellCoordinates) {
927
+ return state;
928
+ }
929
+ const {
930
+ columnOffset
931
+ } = cellCoordinates;
932
+ const tabStop = getCell(gridMap, {
933
+ col: columnOffset,
934
+ row: {
935
+ from: -1,
936
+ direction: 'desc'
937
+ }
938
+ });
939
+ return {
940
+ ...state,
941
+ allowFocusing: true,
942
+ selectedId: tabStop?.id || state.selectedId,
943
+ gridMap
944
+ };
945
+ },
946
+ // Moving to the first item in row
947
+ FIRST_IN_ROW(state, currentTabStop) {
948
+ const currentRowKey = currentTabStop.rowKey;
949
+ if (isNil(currentRowKey)) {
950
+ return state;
951
+ }
952
+ const gridMap = state.gridMap || createGridMap(state.tabStops);
953
+ const cellCoordinates = getCellCoordinates(gridMap, currentTabStop);
954
+ if (!cellCoordinates) {
955
+ return state;
956
+ }
957
+ const {
958
+ rowIndex
959
+ } = cellCoordinates;
960
+ const tabStop = getCell(gridMap, {
961
+ row: rowIndex,
962
+ col: {
963
+ from: 0,
964
+ direction: 'asc'
965
+ }
966
+ });
967
+ return {
968
+ ...state,
969
+ allowFocusing: true,
970
+ selectedId: tabStop?.id || state.selectedId,
971
+ gridMap
972
+ };
973
+ },
974
+ // Moving to the last item in row
975
+ LAST_IN_ROW(state, currentTabStop) {
976
+ const currentRowKey = currentTabStop.rowKey;
977
+ if (isNil(currentRowKey)) {
978
+ return state;
979
+ }
980
+ const gridMap = state.gridMap || createGridMap(state.tabStops);
981
+ const cellCoordinates = getCellCoordinates(gridMap, currentTabStop);
982
+ if (!cellCoordinates) {
983
+ return state;
984
+ }
985
+ const {
986
+ rowIndex
987
+ } = cellCoordinates;
988
+ const tabStop = getCell(gridMap, {
989
+ row: rowIndex,
990
+ col: {
991
+ from: -1,
992
+ direction: 'desc'
993
+ }
994
+ });
995
+ return {
996
+ ...state,
997
+ allowFocusing: true,
998
+ selectedId: tabStop?.id || state.selectedId,
999
+ gridMap
1000
+ };
1001
+ }
1002
+ };
1003
+
1004
+ /** Handle `KEY_NAV` action to update */
1005
+ const KEY_NAV = (state, action) => {
1006
+ const {
1007
+ id = state.selectedId || state.tabStops[0]?.id,
1008
+ key,
1009
+ ctrlKey
1010
+ } = action.payload;
1011
+ const index = state.tabStops.findIndex(tabStop => tabStop.id === id);
1012
+ if (index === -1) {
1013
+ // tab stop not registered
1014
+ return state;
1015
+ }
1016
+ const currentTabStop = state.tabStops[index];
1017
+ if (currentTabStop.disabled) {
1018
+ return state;
1019
+ }
1020
+ const isGrid = currentTabStop.rowKey !== null;
1021
+ const isFirst = index === state.tabStops.findIndex(tabStopIsEnabled);
1022
+ const isLast = index === findLastIndex(state.tabStops, tabStopIsEnabled);
1023
+ // Translates the user key down event info into a navigation instruction.
1024
+ let navigation = null;
1025
+ // eslint-disable-next-line default-case
1026
+ switch (key) {
1027
+ case 'ArrowLeft':
1028
+ if (isGrid) {
1029
+ navigation = 'PREVIOUS_COLUMN';
1030
+ } else if (state.direction === 'horizontal' || state.direction === 'both') {
1031
+ navigation = shouldLoopListHorizontally(state.direction, state.loopAround) && isFirst ? 'VERY_LAST' : 'PREVIOUS';
1032
+ }
1033
+ break;
1034
+ case 'ArrowRight':
1035
+ if (isGrid) {
1036
+ navigation = 'NEXT_COLUMN';
1037
+ } else if (state.direction === 'horizontal' || state.direction === 'both') {
1038
+ navigation = shouldLoopListHorizontally(state.direction, state.loopAround) && isLast ? 'VERY_FIRST' : 'NEXT';
1039
+ }
1040
+ break;
1041
+ case 'ArrowUp':
1042
+ if (isGrid) {
1043
+ navigation = 'PREVIOUS_ROW';
1044
+ } else if (state.direction === 'vertical' || state.direction === 'both') {
1045
+ navigation = shouldLoopListVertically(state.direction, state.loopAround) && isFirst ? 'VERY_LAST' : 'PREVIOUS';
1046
+ }
1047
+ break;
1048
+ case 'ArrowDown':
1049
+ if (isGrid) {
1050
+ navigation = 'NEXT_ROW';
1051
+ } else if (state.direction === 'vertical' || state.direction === 'both') {
1052
+ navigation = shouldLoopListVertically(state.direction, state.loopAround) && isLast ? 'VERY_FIRST' : 'NEXT';
1053
+ }
1054
+ break;
1055
+ case 'Home':
1056
+ if (isGrid && !ctrlKey) {
1057
+ navigation = state.gridJumpToShortcutDirection === 'vertical' ? 'FIRST_IN_COLUMN' : 'FIRST_IN_ROW';
1058
+ } else {
1059
+ navigation = 'VERY_FIRST';
1060
+ }
1061
+ break;
1062
+ case 'End':
1063
+ if (isGrid && !ctrlKey) {
1064
+ navigation = state.gridJumpToShortcutDirection === 'vertical' ? 'LAST_IN_COLUMN' : 'LAST_IN_ROW';
1065
+ } else {
1066
+ navigation = 'VERY_LAST';
1067
+ }
1068
+ break;
1069
+ }
1070
+ if (!navigation) {
1071
+ return state;
1072
+ }
1073
+ const newState = MOVES[navigation](state, currentTabStop, index);
1074
+ return {
1075
+ ...newState,
1076
+ isUsingKeyboard: true
1077
+ };
1078
+ };
1079
+
1080
+ /** Determine the updated value for selectedId: */
1081
+ const getUpdatedSelectedId = (tabStops, currentSelectedId, defaultSelectedId = null) => {
1082
+ // Get tab stop by id
1083
+ const tabStop = currentSelectedId && tabStops.find(ts => ts.id === currentSelectedId && !ts.disabled);
1084
+ if (!tabStop) {
1085
+ // Fallback to default selected id if available, or first enabled tab stop if not
1086
+ return tabStops.find(ts => ts.id === defaultSelectedId)?.id || tabStops.find(tabStopIsEnabled)?.id || null;
1087
+ }
1088
+ return tabStop?.id || defaultSelectedId;
1089
+ };
1090
+
1091
+ /** Handle `REGISTER_TAB_STOP` action registering a new tab stop. */
1092
+ const REGISTER_TAB_STOP = (state, action) => {
1093
+ const newTabStop = action.payload;
1094
+ const newTabStopElement = newTabStop.domElementRef.current;
1095
+ if (!newTabStopElement) {
1096
+ return state;
1097
+ }
1098
+
1099
+ // Find index of tab stop that
1100
+ const indexToInsertAt = findLastIndex(state.tabStops, tabStop => {
1101
+ if (tabStop.id === newTabStop.id) {
1102
+ // tab stop already registered
1103
+ return false;
1104
+ }
1105
+ const domTabStop = tabStop.domElementRef.current;
1106
+
1107
+ // New tab stop is following the current tab stop
1108
+ return domTabStop?.compareDocumentPosition(newTabStopElement) === Node.DOCUMENT_POSITION_FOLLOWING;
1109
+ });
1110
+ const insertIndex = indexToInsertAt + 1;
1111
+
1112
+ // Insert new tab stop at position `indexToInsertAt`.
1113
+ const newTabStops = [...state.tabStops];
1114
+ newTabStops.splice(insertIndex, 0, newTabStop);
1115
+
1116
+ // Handle autofocus if needed
1117
+ let {
1118
+ selectedId,
1119
+ allowFocusing
1120
+ } = state;
1121
+ if (state.autofocus === 'first' && insertIndex === 0 || state.autofocus === 'last' && insertIndex === newTabStops.length - 1 || newTabStop.autofocus) {
1122
+ allowFocusing = true;
1123
+ selectedId = newTabStop.id;
1124
+ }
1125
+ const newSelectedId = newTabStop.id === state.defaultSelectedId && !newTabStop.disabled ? newTabStop.id : getUpdatedSelectedId(newTabStops, selectedId, state.defaultSelectedId);
1126
+ return {
1127
+ ...state,
1128
+ /**
1129
+ * If the tab currently being registered is enabled and set as default selected,
1130
+ * set as selected.
1131
+ *
1132
+ */
1133
+ selectedId: newSelectedId,
1134
+ tabStops: newTabStops,
1135
+ gridMap: null,
1136
+ allowFocusing
1137
+ };
1138
+ };
1139
+
1140
+ /** Handle `UNREGISTER_TAB_STOP` action un-registering a new tab stop. */
1141
+ const UNREGISTER_TAB_STOP = (state, action) => {
1142
+ const {
1143
+ id
1144
+ } = action.payload;
1145
+ const newTabStops = state.tabStops.filter(tabStop => tabStop.id !== id);
1146
+ if (newTabStops.length === state.tabStops.length) {
1147
+ // tab stop already unregistered
1148
+ return state;
1149
+ }
1150
+
1151
+ /** Get the previous enabled tab stop */
1152
+ const previousTabStopIndex = state.tabStops.findIndex(tabStop => tabStop.id === state.selectedId && tabStopIsEnabled(tabStop));
1153
+ const newLocal = previousTabStopIndex - 1 > -1;
1154
+ const previousTabStop = newLocal ? findLast(newTabStops, tabStopIsEnabled, previousTabStopIndex - 1) : undefined;
1155
+ return {
1156
+ ...state,
1157
+ /** Set the focus on either the previous tab stop if found or the one set as default */
1158
+ selectedId: getUpdatedSelectedId(newTabStops, state.selectedId, previousTabStop?.id || state.defaultSelectedId),
1159
+ tabStops: newTabStops,
1160
+ gridMap: null
1161
+ };
1162
+ };
1163
+
1164
+ /** Handle `UPDATE_TAB_STOP` action updating properties of a tab stop. */
1165
+ const UPDATE_TAB_STOP = (state, action) => {
1166
+ const {
1167
+ id,
1168
+ rowKey,
1169
+ disabled
1170
+ } = action.payload;
1171
+ const index = state.tabStops.findIndex(tabStop => tabStop.id === id);
1172
+ if (index === -1) {
1173
+ // tab stop not registered
1174
+ return state;
1175
+ }
1176
+ const tabStop = state.tabStops[index];
1177
+ if (tabStop.disabled === disabled && tabStop.rowKey === rowKey) {
1178
+ // Nothing to do so short-circuit.
1179
+ return state;
1180
+ }
1181
+ const newTabStop = {
1182
+ ...tabStop,
1183
+ rowKey,
1184
+ disabled
1185
+ };
1186
+ const newTabStops = [...state.tabStops];
1187
+ newTabStops.splice(index, 1, newTabStop);
1188
+ return {
1189
+ ...state,
1190
+ selectedId: getUpdatedSelectedId(newTabStops, state.selectedId, state.defaultSelectedId),
1191
+ tabStops: newTabStops,
1192
+ gridMap: null
1193
+ };
1194
+ };
1195
+
1196
+ /** Handle `SELECT_TAB_STOP` action selecting a tab stop. */
1197
+ const SELECT_TAB_STOP = (state, action) => {
1198
+ const {
1199
+ id,
1200
+ type
1201
+ } = action.payload;
1202
+ const tabStop = state.tabStops.find(ts => ts.id === id);
1203
+ if (!tabStop || tabStop.disabled) {
1204
+ return state;
1205
+ }
1206
+ return {
1207
+ ...state,
1208
+ allowFocusing: true,
1209
+ selectedId: tabStop.id,
1210
+ isUsingKeyboard: type === 'keyboard'
1211
+ };
1212
+ };
1213
+ const SET_ALLOW_FOCUSING = (state, action) => {
1214
+ return {
1215
+ ...state,
1216
+ selectedId: getUpdatedSelectedId(state.tabStops, null, state.defaultSelectedId),
1217
+ allowFocusing: action.payload.allow,
1218
+ isUsingKeyboard: Boolean(action.payload.isKeyboardNavigation)
1219
+ };
1220
+ };
1221
+
1222
+ /** Handle `RESET_SELECTED_TAB_STOP` action reseting the selected tab stop. */
1223
+ const RESET_SELECTED_TAB_STOP = state => {
1224
+ return {
1225
+ ...state,
1226
+ allowFocusing: false,
1227
+ selectedId: getUpdatedSelectedId(state.tabStops, null, state.defaultSelectedId),
1228
+ defaultSelectedId: getUpdatedSelectedId(state.tabStops, null, state.defaultSelectedId),
1229
+ isUsingKeyboard: false
1230
+ };
1231
+ };
1232
+
1233
+ const INITIAL_STATE = {
1234
+ selectedId: null,
1235
+ allowFocusing: false,
1236
+ tabStops: [],
1237
+ direction: 'horizontal',
1238
+ loopAround: buildLoopAroundObject(false),
1239
+ gridMap: null,
1240
+ defaultSelectedId: null,
1241
+ autofocus: undefined,
1242
+ isUsingKeyboard: false
1243
+ };
1244
+ const OPTIONS_UPDATED = (state, action) => {
1245
+ const {
1246
+ autofocus
1247
+ } = action.payload;
1248
+ let {
1249
+ selectedId,
1250
+ allowFocusing
1251
+ } = state;
1252
+
1253
+ // Update selectedId when updating the `autofocus` option.
1254
+ if (!state.autofocus && autofocus) {
1255
+ if (autofocus === 'first') {
1256
+ selectedId = state.tabStops.find(tabStopIsEnabled)?.id || null;
1257
+ } else if (autofocus === 'last') {
1258
+ selectedId = findLast(state.tabStops, tabStopIsEnabled)?.id || null;
1259
+ }
1260
+ allowFocusing = true;
1261
+ }
1262
+ return {
1263
+ ...state,
1264
+ ...action.payload,
1265
+ selectedId,
1266
+ allowFocusing: action.payload.allowFocusing || allowFocusing,
1267
+ loopAround: buildLoopAroundObject(action.payload.loopAround)
1268
+ };
1269
+ };
1270
+
1271
+ /** Reducers for each action type: */
1272
+ const REDUCERS$1 = {
1273
+ REGISTER_TAB_STOP,
1274
+ UNREGISTER_TAB_STOP,
1275
+ UPDATE_TAB_STOP,
1276
+ SELECT_TAB_STOP,
1277
+ OPTIONS_UPDATED,
1278
+ KEY_NAV,
1279
+ SET_ALLOW_FOCUSING,
1280
+ RESET_SELECTED_TAB_STOP
1281
+ };
1282
+
1283
+ /** Main reducer */
1284
+ const reducer$1 = (state, action) => {
1285
+ return REDUCERS$1[action.type]?.(state, action) || state;
1286
+ };
1287
+
1288
+ const MovingFocusContext = /*#__PURE__*/React__default.createContext({
1289
+ state: INITIAL_STATE,
1290
+ dispatch: noop
1291
+ });
1292
+
1293
+ /**
1294
+ * Creates a roving tabindex context.
1295
+ */
1296
+ const MovingFocusProvider = ({
1297
+ children,
1298
+ options
1299
+ }) => {
1300
+ const [state, dispatch] = React__default.useReducer(reducer$1, INITIAL_STATE, st => ({
1301
+ ...st,
1302
+ ...options,
1303
+ direction: options?.direction || st.direction,
1304
+ loopAround: buildLoopAroundObject(options?.loopAround),
1305
+ selectedId: options?.defaultSelectedId || INITIAL_STATE.selectedId
1306
+ }));
1307
+ const isMounted = React__default.useRef(false);
1308
+
1309
+ // Update the options whenever they change:
1310
+ React__default.useEffect(() => {
1311
+ // Skip update on mount (already up to date)
1312
+ if (!isMounted.current) {
1313
+ isMounted.current = true;
1314
+ return;
1315
+ }
1316
+ dispatch({
1317
+ type: 'OPTIONS_UPDATED',
1318
+ payload: {
1319
+ direction: options?.direction || INITIAL_STATE.direction,
1320
+ loopAround: buildLoopAroundObject(options?.loopAround || INITIAL_STATE.loopAround),
1321
+ defaultSelectedId: options?.defaultSelectedId || INITIAL_STATE.defaultSelectedId,
1322
+ autofocus: options?.autofocus || INITIAL_STATE.autofocus,
1323
+ allowFocusing: options?.allowFocusing || INITIAL_STATE.allowFocusing,
1324
+ listKey: options?.listKey || INITIAL_STATE.listKey,
1325
+ firstFocusDirection: options?.firstFocusDirection || INITIAL_STATE.firstFocusDirection,
1326
+ gridJumpToShortcutDirection: options?.gridJumpToShortcutDirection || INITIAL_STATE.gridJumpToShortcutDirection
1327
+ }
1328
+ });
1329
+ }, [isMounted, options?.allowFocusing, options?.autofocus, options?.defaultSelectedId, options?.direction, options?.loopAround, options?.listKey, options?.firstFocusDirection, options?.gridJumpToShortcutDirection]);
1330
+
1331
+ // Create a cached object to use as the context value:
1332
+ const context = React__default.useMemo(() => ({
1333
+ state,
1334
+ dispatch
1335
+ }), [state]);
1336
+ return /*#__PURE__*/jsx(MovingFocusContext.Provider, {
1337
+ value: context,
1338
+ children: children
1339
+ });
1340
+ };
1341
+
1342
+ /**
1343
+ * Hook options
1344
+ */
1345
+
1346
+ /**
1347
+ * Hook to use in tab stop element of a virtual focus (ex: options of a listbox in a combobox).
1348
+ *
1349
+ * @returns true if the current tab stop has virtual focus
1350
+ */
1351
+ const useVirtualFocus = (id, domElementRef, disabled = false, rowKey = null, autofocus = false) => {
1352
+ const isMounted = React__default.useRef(false);
1353
+ const {
1354
+ state,
1355
+ dispatch
1356
+ } = React__default.useContext(MovingFocusContext);
1357
+
1358
+ // Register the tab stop on mount and unregister it on unmount:
1359
+ React__default.useEffect(() => {
1360
+ const {
1361
+ current: domElement
1362
+ } = domElementRef;
1363
+ if (!domElement) {
1364
+ return undefined;
1365
+ }
1366
+ // Select tab stop on click
1367
+ const onClick = event => {
1368
+ dispatch({
1369
+ type: 'SELECT_TAB_STOP',
1370
+ payload: {
1371
+ id,
1372
+ type: getPointerTypeFromEvent(event)
1373
+ }
1374
+ });
1375
+ };
1376
+ domElement.addEventListener('click', onClick);
1377
+
1378
+ // Register tab stop in context
1379
+ dispatch({
1380
+ type: 'REGISTER_TAB_STOP',
1381
+ payload: {
1382
+ id,
1383
+ domElementRef,
1384
+ rowKey,
1385
+ disabled,
1386
+ autofocus
1387
+ }
1388
+ });
1389
+ return () => {
1390
+ domElement.removeEventListener('click', onClick);
1391
+ dispatch({
1392
+ type: 'UNREGISTER_TAB_STOP',
1393
+ payload: {
1394
+ id
1395
+ }
1396
+ });
1397
+ };
1398
+ },
1399
+ /**
1400
+ * Pass the list key as dependency to make tab stops
1401
+ * re-register when it changes.
1402
+ */
1403
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1404
+ [state.listKey]);
1405
+
1406
+ /*
1407
+ * Update the tab stop data if `rowKey` or `disabled` change.
1408
+ * The isMounted flag is used to prevent this effect running on mount, which is benign but redundant (as the
1409
+ * REGISTER_TAB_STOP action would have just been dispatched).
1410
+ */
1411
+ React__default.useEffect(() => {
1412
+ if (isMounted.current) {
1413
+ dispatch({
1414
+ type: 'UPDATE_TAB_STOP',
1415
+ payload: {
1416
+ id,
1417
+ rowKey,
1418
+ disabled
1419
+ }
1420
+ });
1421
+ } else {
1422
+ isMounted.current = true;
1423
+ }
1424
+ },
1425
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1426
+ [disabled, rowKey]);
1427
+ const isActive = id === state.selectedId;
1428
+
1429
+ // Scroll element into view when highlighted
1430
+ useEffect(() => {
1431
+ const {
1432
+ current
1433
+ } = domElementRef;
1434
+ if (isActive && current && current.scrollIntoView) {
1435
+ /**
1436
+ * In some cases, the selected item is contained in a popover
1437
+ * that won't be immediately set in the correct position.
1438
+ * Setting a small timeout before scroll the item into view
1439
+ * leaves it time to settle at the correct position.
1440
+ */
1441
+ const timeout = setTimeout(() => {
1442
+ current.scrollIntoView({
1443
+ block: 'nearest'
1444
+ });
1445
+ }, 10);
1446
+ return () => {
1447
+ clearTimeout(timeout);
1448
+ };
1449
+ }
1450
+ return undefined;
1451
+ }, [domElementRef, isActive]);
1452
+ const focused = isActive && state.allowFocusing;
1453
+
1454
+ // Determine if the current tab stop is the currently active one:
1455
+ return focused;
1456
+ };
1457
+
1458
+ /**
1459
+ * Hook to use in a virtual focus parent (ex: `aria-activedescendant` on the input of a combobox).
1460
+ * * @returns the id of the currently active tab stop (virtual focus)
1461
+ */
1462
+ const useVirtualFocusParent = ref => {
1463
+ const {
1464
+ state,
1465
+ dispatch
1466
+ } = React__default.useContext(MovingFocusContext);
1467
+ React__default.useEffect(() => {
1468
+ const {
1469
+ current: element
1470
+ } = ref;
1471
+ if (!element) {
1472
+ return undefined;
1473
+ }
1474
+ function handleKeyDown(evt) {
1475
+ const eventKey = evt.key;
1476
+ if (
1477
+ // Don't move if the current direction doesn't allow key
1478
+ !NAV_KEYS[state.direction].includes(eventKey) ||
1479
+ // Don't move if alt key is pressed
1480
+ evt.altKey ||
1481
+ // Don't move the focus if it hasn't been set yet and `firstFocusDirection` doesn't allow key
1482
+ !state.allowFocusing && state.firstFocusDirection && !NAV_KEYS[state.firstFocusDirection].includes(eventKey)) {
1483
+ return;
1484
+ }
1485
+ // If focus isn't allowed yet, simply enable it to stay on first item
1486
+ if (!state.allowFocusing && eventKey === 'ArrowDown') {
1487
+ dispatch({
1488
+ type: 'SET_ALLOW_FOCUSING',
1489
+ payload: {
1490
+ allow: true,
1491
+ isKeyboardNavigation: true
1492
+ }
1493
+ });
1494
+ } else {
1495
+ dispatch({
1496
+ type: 'KEY_NAV',
1497
+ payload: {
1498
+ key: eventKey,
1499
+ ctrlKey: evt.ctrlKey
1500
+ }
1501
+ });
1502
+ }
1503
+ evt.preventDefault();
1504
+ }
1505
+ element.addEventListener('keydown', handleKeyDown);
1506
+ return () => {
1507
+ element.removeEventListener('keydown', handleKeyDown);
1508
+ };
1509
+ }, [dispatch, ref, state.allowFocusing, state.direction, state.firstFocusDirection]);
1510
+ const focused = state.allowFocusing && state.selectedId || undefined;
1511
+ return focused;
1512
+ };
1513
+
1514
+ /** Generate the combobox option id from the combobox id and the given id */
1515
+ const generateOptionId = (comboboxId, optionId) => `${comboboxId}-option-${optionId}`;
1516
+
1517
+ /** Verifies that the combobox registered option is an action */
1518
+ const isComboboxAction = option => Boolean(option?.isAction);
1519
+
1520
+ /** Verifies that the combobox registered option is the option's value */
1521
+ const isComboboxValue = option => {
1522
+ return !isComboboxAction(option);
1523
+ };
1524
+
1525
+ const comboboxId = `combobox-${uniqueId()}`;
1526
+ const initialState = {
1527
+ comboboxId,
1528
+ listboxId: `${comboboxId}-popover`,
1529
+ status: 'idle',
1530
+ isOpen: false,
1531
+ inputValue: '',
1532
+ showAll: true,
1533
+ options: {},
1534
+ type: 'listbox',
1535
+ optionsLength: 0
1536
+ };
1537
+
1538
+ /** Actions when the combobox opens. */
1539
+ const OPEN_COMBOBOX = (state, action) => {
1540
+ const {
1541
+ manual
1542
+ } = action.payload || {};
1543
+ // If the combobox was manually opened, show all suggestions
1544
+ return {
1545
+ ...state,
1546
+ showAll: Boolean(manual),
1547
+ isOpen: true
1548
+ };
1549
+ };
1550
+
1551
+ /** Actions when the combobox closes */
1552
+ const CLOSE_COMBOBOX = state => {
1553
+ return {
1554
+ ...state,
1555
+ showAll: true,
1556
+ isOpen: false
1557
+ };
1558
+ };
1559
+
1560
+ /** Actions on input update. */
1561
+ const SET_INPUT_VALUE = (state, action) => {
1562
+ return {
1563
+ ...state,
1564
+ inputValue: action.payload,
1565
+ // When the user is changing the value, show only values that are related to the input value.
1566
+ showAll: false,
1567
+ isOpen: true
1568
+ };
1569
+ };
1570
+
1571
+ /** Register an option to the state */
1572
+ const ADD_OPTION = (state, action) => {
1573
+ const {
1574
+ id,
1575
+ option
1576
+ } = action.payload;
1577
+ const {
1578
+ options
1579
+ } = state;
1580
+ if (options[id]) {
1581
+ // Option already exists, return state unchanged
1582
+ return state;
1583
+ }
1584
+ const newOptions = {
1585
+ ...options,
1586
+ [id]: option
1587
+ };
1588
+ let newType = state.type;
1589
+ if (isComboboxAction(option)) {
1590
+ newType = 'grid';
1591
+ }
1592
+ let newOptionsLength = state.optionsLength;
1593
+ if (isComboboxValue(option)) {
1594
+ newOptionsLength += 1;
1595
+ }
1596
+ return {
1597
+ ...state,
1598
+ options: newOptions,
1599
+ type: newType,
1600
+ optionsLength: newOptionsLength
1601
+ };
1602
+ };
1603
+
1604
+ /** Remove an option from the state */
1605
+ const REMOVE_OPTION = (state, action) => {
1606
+ const {
1607
+ id
1608
+ } = action.payload;
1609
+ const {
1610
+ options
1611
+ } = state;
1612
+ const option = options[id];
1613
+ if (!options[id]) {
1614
+ // Option doesn't exist, return state unchanged
1615
+ return state;
1616
+ }
1617
+ const newOptions = {
1618
+ ...options
1619
+ };
1620
+ delete newOptions[id];
1621
+ let newOptionsLength = state.optionsLength;
1622
+ if (isComboboxValue(option)) {
1623
+ newOptionsLength -= 1;
1624
+ }
1625
+ return {
1626
+ ...state,
1627
+ options: newOptions,
1628
+ optionsLength: newOptionsLength
1629
+ };
1630
+ };
1631
+
1632
+ /** Reducers for each action type: */
1633
+ const REDUCERS = {
1634
+ OPEN_COMBOBOX,
1635
+ CLOSE_COMBOBOX,
1636
+ SET_INPUT_VALUE,
1637
+ ADD_OPTION,
1638
+ REMOVE_OPTION
1639
+ };
1640
+
1641
+ /** Main reducer */
1642
+ const reducer = (state, action) => {
1643
+ return REDUCERS[action.type]?.(state, action) || state;
1644
+ };
1645
+
1646
+ /** Dispatch for the combobox component */
1647
+
1648
+ /** Context for the Combobox component */
1649
+ const ComboboxContext = /*#__PURE__*/React__default.createContext({
1650
+ ...initialState,
1651
+ openOnFocus: false,
1652
+ openOnClick: false,
1653
+ selectionType: 'single',
1654
+ optionsLength: 0,
1655
+ onSelect: noop,
1656
+ onInputChange: noop,
1657
+ onOpen: noop,
1658
+ dispatch: noop,
1659
+ translations: {
1660
+ clearLabel: '',
1661
+ tryReloadLabel: '',
1662
+ showSuggestionsLabel: '',
1663
+ noResultsForInputLabel: input => input || '',
1664
+ loadingLabel: '',
1665
+ serviceUnavailableLabel: '',
1666
+ nbOptionsLabel: options => `${options}`
1667
+ }
1668
+ });
1669
+
1670
+ /** Context for a combobox section to store its unique id */
1671
+ const SectionContext = /*#__PURE__*/React__default.createContext({
1672
+ sectionId: ''
1673
+ });
1674
+
1675
+ /** Retrieve the current combobox state and actions */
1676
+ const useCombobox = () => {
1677
+ const comboboxContext = React__default.useContext(ComboboxContext);
1678
+ const {
1679
+ dispatch: movingFocusDispatch
1680
+ } = React__default.useContext(MovingFocusContext);
1681
+ const {
1682
+ onSelect,
1683
+ onInputChange,
1684
+ onOpen,
1685
+ dispatch,
1686
+ inputValue,
1687
+ ...contextValues
1688
+ } = comboboxContext;
1689
+ const {
1690
+ triggerRef
1691
+ } = useComboboxRefs();
1692
+
1693
+ /** Action triggered when the listBox is closed without selecting any option */
1694
+ const handleClose = React__default.useCallback(() => {
1695
+ dispatch({
1696
+ type: 'CLOSE_COMBOBOX'
1697
+ });
1698
+ // Reset visual focus
1699
+ movingFocusDispatch({
1700
+ type: 'RESET_SELECTED_TAB_STOP'
1701
+ });
1702
+ }, [dispatch, movingFocusDispatch]);
1703
+
1704
+ // Handle callbacks on options mounted
1705
+ const [optionsMountedCallbacks, setOptionsMountedCallback] = React__default.useState();
1706
+ React__default.useEffect(() => {
1707
+ if (comboboxContext.optionsLength > 0 && optionsMountedCallbacks?.length) {
1708
+ const optionsArray = Object.values(comboboxContext.options);
1709
+ // Execute callbacks
1710
+ for (const callback of optionsMountedCallbacks) {
1711
+ callback(optionsArray);
1712
+ }
1713
+ setOptionsMountedCallback(undefined);
1714
+ }
1715
+ }, [comboboxContext.options, comboboxContext.optionsLength, optionsMountedCallbacks]);
1716
+
1717
+ /** Callback for when an option is selected */
1718
+ const handleSelected = React__default.useCallback((option, source) => {
1719
+ if (option?.isDisabled) {
1720
+ return;
1721
+ }
1722
+ if (isComboboxValue(option)) {
1723
+ /**
1724
+ * We only close the list if the selection type is single.
1725
+ * If it is multiple, we want to allow the user to continue
1726
+ * selecting multiple options.
1727
+ */
1728
+ if (comboboxContext.selectionType !== 'multiple') {
1729
+ handleClose();
1730
+ }
1731
+ /** Call parent onSelect callback */
1732
+ if (onSelect) {
1733
+ onSelect(option);
1734
+ }
1735
+ }
1736
+
1737
+ /** If the option itself has a custom action, also call it */
1738
+ if (option?.onSelect) {
1739
+ option.onSelect(option, source);
1740
+ }
1741
+
1742
+ /** Reset focus on input */
1743
+ if (triggerRef?.current) {
1744
+ triggerRef.current?.focus();
1745
+ }
1746
+ }, [comboboxContext.selectionType, handleClose, onSelect, triggerRef]);
1747
+
1748
+ /** Callback for when the input must be updated */
1749
+ const handleInputChange = React__default.useCallback((value, ...args) => {
1750
+ // Update the local state
1751
+ dispatch({
1752
+ type: 'SET_INPUT_VALUE',
1753
+ payload: value
1754
+ });
1755
+ // If a callback if given, call it with the value
1756
+ if (onInputChange) {
1757
+ onInputChange(value, ...args);
1758
+ }
1759
+ // Reset visual focus
1760
+ movingFocusDispatch({
1761
+ type: 'RESET_SELECTED_TAB_STOP'
1762
+ });
1763
+ }, [dispatch, movingFocusDispatch, onInputChange]);
1764
+
1765
+ /**
1766
+ * Open the popover
1767
+ *
1768
+ * @returns a promise with the updated context once all options are mounted
1769
+ */
1770
+ const handleOpen = React__default.useCallback(params => {
1771
+ /** update the local state */
1772
+ dispatch({
1773
+ type: 'OPEN_COMBOBOX',
1774
+ payload: params
1775
+ });
1776
+ /** If a parent callback was given, trigger it with state information */
1777
+ if (onOpen) {
1778
+ onOpen({
1779
+ currentValue: inputValue,
1780
+ manual: Boolean(params?.manual)
1781
+ });
1782
+ }
1783
+
1784
+ // Promise resolving options on mount
1785
+ return new Promise(resolve => {
1786
+ // Append to the list of callback on options mounted
1787
+ setOptionsMountedCallback((callbacks = []) => {
1788
+ callbacks.push(resolve);
1789
+ return callbacks;
1790
+ });
1791
+ });
1792
+ }, [dispatch, inputValue, onOpen]);
1793
+ return React__default.useMemo(() => ({
1794
+ handleClose,
1795
+ handleOpen,
1796
+ handleInputChange,
1797
+ handleSelected,
1798
+ dispatch,
1799
+ inputValue,
1800
+ ...contextValues
1801
+ }), [contextValues, dispatch, handleClose, handleInputChange, handleOpen, handleSelected, inputValue]);
1802
+ };
1803
+
1804
+ export { A11YLiveMessage as A, ComboboxContext as C, DisabledStateProvider as D, MovingFocusContext as M, NAV_KEYS as N, Portal as P, SectionContext as S, useVirtualFocusParent as a, useComboboxRefs as b, useCombobox as c, useVirtualFocus as d, MovingFocusProvider as e, initialState as f, generateOptionId as g, ComboboxRefsProvider as h, isComboboxValue as i, ClickAwayProvider as j, getPointerTypeFromEvent as k, PortalProvider as l, reducer as r, useDisabledStateContext as u };
1805
+ //# sourceMappingURL=BCgo9dYV.js.map