UNPKG

29.7 kBPlain TextView Raw
1import * as React from "react";
2import {
3 SealedInitialState,
4 useSealedState,
5} from "reakit-utils/useSealedState";
6import { applyState } from "reakit-utils/applyState";
7import { useIsomorphicEffect } from "reakit-utils/useIsomorphicEffect";
8import {
9 unstable_IdState,
10 unstable_IdActions,
11 unstable_IdInitialState,
12 unstable_useIdState,
13 unstable_IdStateReturn,
14} from "../Id/IdState";
15import { reverse } from "./__utils/reverse";
16import { Item, Group, Orientation } from "./__utils/types";
17import { findDOMIndex } from "./__utils/findDOMIndex";
18import { findFirstEnabledItem } from "./__utils/findFirstEnabledItem";
19import { findEnabledItemById } from "./__utils/findEnabledItemById";
20import { verticalizeItems } from "./__utils/verticalizeItems";
21import { groupItems } from "./__utils/groupItems";
22import { flatten } from "./__utils/flatten";
23import { fillGroups } from "./__utils/fillGroups";
24import { getCurrentId } from "./__utils/getCurrentId";
25import { placeItemsAfter } from "./__utils/placeItemsAfter";
26import { getItemsInGroup } from "./__utils/getItemsInGroup";
27import { getOppositeOrientation } from "./__utils/getOppositeOrientation";
28import { addItemAtIndex } from "./__utils/addItemAtIndex";
29import { sortBasedOnDOMPosition } from "./__utils/sortBasedOnDOMPosition";
30import { useSortBasedOnDOMPosition } from "./__utils/useSortBasedOnDOMPosition";
31
32type CompositeReducerAction =
33 | { type: "registerItem"; item: Item }
34 | { type: "unregisterItem"; id: string | null }
35 | { type: "registerGroup"; group: Group }
36 | { type: "unregisterGroup"; id: string | null }
37 | { type: "move"; id?: string | null }
38 | { type: "next"; allTheWay?: boolean; hasNullItem?: boolean }
39 | { type: "previous"; allTheWay?: boolean }
40 | { type: "up"; allTheWay?: boolean }
41 | { type: "down"; allTheWay?: boolean }
42 | { type: "first" }
43 | { type: "last" }
44 | { type: "sort" }
45 | {
46 type: "setVirtual";
47 virtual: React.SetStateAction<CompositeState["unstable_virtual"]>;
48 }
49 | {
50 type: "setRTL";
51 rtl: React.SetStateAction<CompositeState["rtl"]>;
52 }
53 | {
54 type: "setOrientation";
55 orientation?: React.SetStateAction<CompositeState["orientation"]>;
56 }
57 | {
58 type: "setCurrentId";
59 currentId?: React.SetStateAction<CompositeState["currentId"]>;
60 }
61 | {
62 type: "setLoop";
63 loop: React.SetStateAction<CompositeState["loop"]>;
64 }
65 | {
66 type: "setWrap";
67 wrap: React.SetStateAction<CompositeState["wrap"]>;
68 }
69 | {
70 type: "setShift";
71 shift: React.SetStateAction<CompositeState["shift"]>;
72 }
73 | { type: "reset" }
74 | { type: "setItems"; items: CompositeState["items"] }
75 | {
76 type: "setIncludesBaseElement";
77 includesBaseElement: React.SetStateAction<
78 CompositeState["unstable_includesBaseElement"]
79 >;
80 };
81
82type CompositeReducerState = Omit<
83 CompositeState,
84 "unstable_hasActiveWidget" | keyof unstable_IdState
85> & {
86 pastIds: string[];
87 initialVirtual: CompositeState["unstable_virtual"];
88 initialRTL: CompositeState["rtl"];
89 initialOrientation: CompositeState["orientation"];
90 initialCurrentId: CompositeState["currentId"];
91 initialLoop: CompositeState["loop"];
92 initialWrap: CompositeState["wrap"];
93 initialShift: CompositeState["shift"];
94 hasSetCurrentId?: boolean;
95};
96
97function reducer(
98 state: CompositeReducerState,
99 action: CompositeReducerAction
100): CompositeReducerState {
101 const {
102 unstable_virtual: virtual,
103 rtl,
104 orientation,
105 items,
106 groups,
107 currentId,
108 loop,
109 wrap,
110 pastIds,
111 shift,
112 unstable_moves: moves,
113 unstable_includesBaseElement: includesBaseElement,
114 initialVirtual,
115 initialRTL,
116 initialOrientation,
117 initialCurrentId,
118 initialLoop,
119 initialWrap,
120 initialShift,
121 hasSetCurrentId,
122 } = state;
123
124 switch (action.type) {
125 case "registerGroup": {
126 const { group } = action;
127 // If there are no groups yet, just add it as the first one
128 if (groups.length === 0) {
129 return { ...state, groups: [group] };
130 }
131 // Finds the group index based on DOM position
132 const index = findDOMIndex(groups, group);
133 return { ...state, groups: addItemAtIndex(groups, group, index) };
134 }
135
136 case "unregisterGroup": {
137 const { id } = action;
138 const nextGroups = groups.filter((group) => group.id !== id);
139 // The group isn't registered, so do nothing
140 if (nextGroups.length === groups.length) {
141 return state;
142 }
143 return { ...state, groups: nextGroups };
144 }
145
146 case "registerItem": {
147 const { item } = action;
148 // Finds the item group based on the DOM hierarchy
149 const group = groups.find((r) =>
150 r.ref.current?.contains(item.ref.current)
151 );
152 // Group will be null if it's a one-dimensional composite
153 const nextItem = { groupId: group?.id, ...item };
154 const index = findDOMIndex(items, nextItem);
155 const nextState = {
156 ...state,
157 items: addItemAtIndex(items, nextItem, index),
158 };
159 if (!hasSetCurrentId && !moves && initialCurrentId === undefined) {
160 // Sets currentId to the first enabled item. This runs whenever an item
161 // is registered because the first enabled item may be registered
162 // asynchronously.
163 return {
164 ...nextState,
165 currentId: findFirstEnabledItem(nextState.items)?.id,
166 };
167 }
168 return nextState;
169 }
170
171 case "unregisterItem": {
172 const { id } = action;
173 const nextItems = items.filter((item) => item.id !== id);
174 // The item isn't registered, so do nothing
175 if (nextItems.length === items.length) {
176 return state;
177 }
178 // Filters out the item that is being removed from the pastIds list
179 const nextPastIds = pastIds.filter((pastId) => pastId !== id);
180 const nextState = {
181 ...state,
182 pastIds: nextPastIds,
183 items: nextItems,
184 };
185 // If the current item is the item that is being removed, focus pastId
186 if (currentId && currentId === id) {
187 const nextId = includesBaseElement
188 ? null
189 : getCurrentId({
190 ...nextState,
191 currentId: nextPastIds[0],
192 });
193 return { ...nextState, currentId: nextId };
194 }
195 return nextState;
196 }
197
198 case "move": {
199 const { id } = action;
200 // move() does nothing
201 if (id === undefined) {
202 return state;
203 }
204 // Removes the current item and the item that is receiving focus from the
205 // pastIds list
206 const filteredPastIds = pastIds.filter(
207 (pastId) => pastId !== currentId && pastId !== id
208 );
209 // If there's a currentId, add it to the pastIds list so it can be focused
210 // if the new item gets removed or disabled
211 const nextPastIds = currentId
212 ? [currentId, ...filteredPastIds]
213 : filteredPastIds;
214 const nextState = { ...state, pastIds: nextPastIds };
215 // move(null) will focus the composite element itself, not an item
216 if (id === null) {
217 return {
218 ...nextState,
219 unstable_moves: moves + 1,
220 currentId: getCurrentId(nextState, id),
221 };
222 }
223 const item = findEnabledItemById(items, id);
224 return {
225 ...nextState,
226 unstable_moves: item ? moves + 1 : moves,
227 currentId: getCurrentId(nextState, item?.id),
228 };
229 }
230
231 case "next": {
232 // If there's no item focused, we just move the first one
233 if (currentId == null) {
234 return reducer(state, { ...action, type: "first" });
235 }
236 // RTL doesn't make sense on vertical navigation
237 const isHorizontal = orientation !== "vertical";
238 const isRTL = rtl && isHorizontal;
239 const allItems = isRTL ? reverse(items) : items;
240 const currentItem = allItems.find((item) => item.id === currentId);
241 // If there's no item focused, we just move the first one
242 if (!currentItem) {
243 return reducer(state, { ...action, type: "first" });
244 }
245 const isGrid = !!currentItem.groupId;
246 const currentIndex = allItems.indexOf(currentItem);
247 const nextItems = allItems.slice(currentIndex + 1);
248 const nextItemsInGroup = getItemsInGroup(nextItems, currentItem.groupId);
249 // Home, End
250 if (action.allTheWay) {
251 // We reverse so we can get the last enabled item in the group. If it's
252 // RTL, nextItems and nextItemsInGroup are already reversed and don't
253 // have the items before the current one anymore. So we have to get
254 // items in group again with allItems.
255 const nextItem = findFirstEnabledItem(
256 isRTL
257 ? getItemsInGroup(allItems, currentItem.groupId)
258 : reverse(nextItemsInGroup)
259 );
260 return reducer(state, { ...action, type: "move", id: nextItem?.id });
261 }
262 const oppositeOrientation = getOppositeOrientation(
263 // If it's a grid and orientation is not set, it's a next/previous
264 // call, which is inherently horizontal. up/down will call next with
265 // orientation set to vertical by default (see below on up/down cases).
266 isGrid ? orientation || "horizontal" : orientation
267 );
268 const canLoop = loop && loop !== oppositeOrientation;
269 const canWrap = isGrid && wrap && wrap !== oppositeOrientation;
270 const hasNullItem =
271 // `previous` and `up` will set action.hasNullItem, but when calling
272 // next directly, hasNullItem will only be true if it's not a grid and
273 // loop is set to true, which means that pressing right or down keys on
274 // grids will never focus the composite element. On one-dimensional
275 // composites that don't loop, pressing right or down keys also doesn't
276 // focus the composite element.
277 action.hasNullItem || (!isGrid && canLoop && includesBaseElement);
278
279 if (canLoop) {
280 const loopItems =
281 canWrap && !hasNullItem
282 ? allItems
283 : getItemsInGroup(allItems, currentItem.groupId);
284 // Turns [0, 1, current, 3, 4] into [3, 4, 0, 1]
285 const sortedItems = placeItemsAfter(loopItems, currentId, hasNullItem);
286 const nextItem = findFirstEnabledItem(sortedItems, currentId);
287 return reducer(state, { ...action, type: "move", id: nextItem?.id });
288 }
289 if (canWrap) {
290 const nextItem = findFirstEnabledItem(
291 // We can use nextItems, which contains all the next items, including
292 // items from other groups, to wrap between groups. However, if there
293 // is a null item (the composite element), we'll only use the next
294 // items in the group. So moving next from the last item will focus
295 // the composite element (null). On grid composites, horizontal
296 // navigation never focuses the composite element, only vertical.
297 hasNullItem ? nextItemsInGroup : nextItems,
298 currentId
299 );
300 const nextId = hasNullItem ? nextItem?.id || null : nextItem?.id;
301 return reducer(state, { ...action, type: "move", id: nextId });
302 }
303 const nextItem = findFirstEnabledItem(nextItemsInGroup, currentId);
304 if (!nextItem && hasNullItem) {
305 return reducer(state, { ...action, type: "move", id: null });
306 }
307 return reducer(state, { ...action, type: "move", id: nextItem?.id });
308 }
309
310 case "previous": {
311 // If currentId is initially set to null, the composite element will be
312 // focusable while navigating with arrow keys. But, if it's a grid, we
313 // don't want to focus the composite element with horizontal navigation.
314 const isGrid = !!groups.length;
315 const hasNullItem = !isGrid && includesBaseElement;
316 const nextState = reducer(
317 { ...state, items: reverse(items) },
318 { ...action, type: "next", hasNullItem }
319 );
320 return { ...nextState, items };
321 }
322
323 case "down": {
324 const shouldShift = shift && !action.allTheWay;
325 // First, we make sure groups have the same number of items by filling it
326 // with disabled fake items. Then, we reorganize the items list so
327 // [1-1, 1-2, 2-1, 2-2] becomes [1-1, 2-1, 1-2, 2-2].
328 const verticalItems = verticalizeItems(
329 flatten(fillGroups(groupItems(items), currentId, shouldShift))
330 );
331 const canLoop = loop && loop !== "horizontal";
332 // Pressing down arrow key will only focus the composite element if loop
333 // is true or vertical.
334 const hasNullItem = canLoop && includesBaseElement;
335 const nextState = reducer(
336 { ...state, orientation: "vertical", items: verticalItems },
337 { ...action, type: "next", hasNullItem }
338 );
339 return { ...nextState, orientation, items };
340 }
341
342 case "up": {
343 const shouldShift = shift && !action.allTheWay;
344 const verticalItems = verticalizeItems(
345 reverse(flatten(fillGroups(groupItems(items), currentId, shouldShift)))
346 );
347 // If currentId is initially set to null, we'll always focus the
348 // composite element when the up arrow key is pressed in the first row.
349 const hasNullItem = includesBaseElement;
350 const nextState = reducer(
351 { ...state, orientation: "vertical", items: verticalItems },
352 { ...action, type: "next", hasNullItem }
353 );
354 return { ...nextState, orientation, items };
355 }
356
357 case "first": {
358 const firstItem = findFirstEnabledItem(items);
359 return reducer(state, { ...action, type: "move", id: firstItem?.id });
360 }
361
362 case "last": {
363 const nextState = reducer(
364 { ...state, items: reverse(items) },
365 { ...action, type: "first" }
366 );
367 return { ...nextState, items };
368 }
369
370 case "sort": {
371 return {
372 ...state,
373 items: sortBasedOnDOMPosition(items),
374 groups: sortBasedOnDOMPosition(groups),
375 };
376 }
377
378 case "setVirtual":
379 return {
380 ...state,
381 unstable_virtual: applyState(action.virtual, virtual),
382 };
383
384 case "setRTL":
385 return { ...state, rtl: applyState(action.rtl, rtl) };
386
387 case "setOrientation":
388 return {
389 ...state,
390 orientation: applyState(action.orientation, orientation),
391 };
392
393 case "setCurrentId": {
394 const nextCurrentId = getCurrentId({
395 ...state,
396 currentId: applyState(action.currentId, currentId),
397 });
398 return { ...state, currentId: nextCurrentId, hasSetCurrentId: true };
399 }
400
401 case "setLoop":
402 return { ...state, loop: applyState(action.loop, loop) };
403
404 case "setWrap":
405 return { ...state, wrap: applyState(action.wrap, wrap) };
406
407 case "setShift":
408 return { ...state, shift: applyState(action.shift, shift) };
409
410 case "setIncludesBaseElement": {
411 return {
412 ...state,
413 unstable_includesBaseElement: applyState(
414 action.includesBaseElement,
415 includesBaseElement
416 ),
417 };
418 }
419
420 case "reset":
421 return {
422 ...state,
423 unstable_virtual: initialVirtual,
424 rtl: initialRTL,
425 orientation: initialOrientation,
426 currentId: getCurrentId({ ...state, currentId: initialCurrentId }),
427 loop: initialLoop,
428 wrap: initialWrap,
429 shift: initialShift,
430 unstable_moves: 0,
431 pastIds: [],
432 };
433
434 case "setItems": {
435 return { ...state, items: action.items };
436 }
437 default:
438 throw new Error();
439 }
440}
441
442function useAction<T extends (...args: any[]) => any>(fn: T) {
443 return React.useCallback(fn, []);
444}
445
446function useIsUnmountedRef() {
447 const isUnmountedRef = React.useRef(false);
448 useIsomorphicEffect(() => {
449 return () => {
450 isUnmountedRef.current = true;
451 };
452 }, []);
453 return isUnmountedRef;
454}
455
456export function useCompositeState(
457 initialState: SealedInitialState<CompositeInitialState> = {}
458): CompositeStateReturn {
459 const {
460 unstable_virtual: virtual = false,
461 rtl = false,
462 orientation,
463 currentId,
464 loop = false,
465 wrap = false,
466 shift = false,
467 unstable_includesBaseElement,
468 ...sealed
469 } = useSealedState(initialState);
470 const idState = unstable_useIdState(sealed);
471 const [
472 {
473 pastIds,
474 initialVirtual,
475 initialRTL,
476 initialOrientation,
477 initialCurrentId,
478 initialLoop,
479 initialWrap,
480 initialShift,
481 hasSetCurrentId,
482 ...state
483 },
484 dispatch,
485 ] = React.useReducer(reducer, {
486 unstable_virtual: virtual,
487 rtl,
488 orientation,
489 items: [],
490 groups: [],
491 currentId,
492 loop,
493 wrap,
494 shift,
495 unstable_moves: 0,
496 pastIds: [],
497 unstable_includesBaseElement:
498 unstable_includesBaseElement ?? currentId === null,
499 initialVirtual: virtual,
500 initialRTL: rtl,
501 initialOrientation: orientation,
502 initialCurrentId: currentId,
503 initialLoop: loop,
504 initialWrap: wrap,
505 initialShift: shift,
506 });
507 const [hasActiveWidget, setHasActiveWidget] = React.useState(false);
508 // register/unregister may be called when this component is unmounted. We
509 // store the unmounted state here so we don't update the state if it's true.
510 // This only happens in a very specific situation.
511 // See https://github.com/reakit/reakit/issues/650
512 const isUnmountedRef = useIsUnmountedRef();
513
514 const setItems = React.useCallback(
515 (items: Item[]) => dispatch({ type: "setItems", items }),
516 []
517 );
518 useSortBasedOnDOMPosition(state.items, setItems);
519
520 return {
521 ...idState,
522 ...state,
523 unstable_hasActiveWidget: hasActiveWidget,
524 unstable_setHasActiveWidget: setHasActiveWidget,
525 registerItem: useAction((item) => {
526 if (isUnmountedRef.current) return;
527 dispatch({ type: "registerItem", item });
528 }),
529 unregisterItem: useAction((id) => {
530 if (isUnmountedRef.current) return;
531 dispatch({ type: "unregisterItem", id });
532 }),
533 registerGroup: useAction((group) => {
534 if (isUnmountedRef.current) return;
535 dispatch({ type: "registerGroup", group });
536 }),
537 unregisterGroup: useAction((id) => {
538 if (isUnmountedRef.current) return;
539 dispatch({ type: "unregisterGroup", id });
540 }),
541 move: useAction((id) => dispatch({ type: "move", id })),
542 next: useAction((allTheWay) => dispatch({ type: "next", allTheWay })),
543 previous: useAction((allTheWay) =>
544 dispatch({ type: "previous", allTheWay })
545 ),
546 up: useAction((allTheWay) => dispatch({ type: "up", allTheWay })),
547 down: useAction((allTheWay) => dispatch({ type: "down", allTheWay })),
548 first: useAction(() => dispatch({ type: "first" })),
549 last: useAction(() => dispatch({ type: "last" })),
550 sort: useAction(() => dispatch({ type: "sort" })),
551 unstable_setVirtual: useAction((value) =>
552 dispatch({ type: "setVirtual", virtual: value })
553 ),
554 setRTL: useAction((value) => dispatch({ type: "setRTL", rtl: value })),
555 setOrientation: useAction((value) =>
556 dispatch({ type: "setOrientation", orientation: value })
557 ),
558 setCurrentId: useAction((value) =>
559 dispatch({ type: "setCurrentId", currentId: value })
560 ),
561 setLoop: useAction((value) => dispatch({ type: "setLoop", loop: value })),
562 setWrap: useAction((value) => dispatch({ type: "setWrap", wrap: value })),
563 setShift: useAction((value) =>
564 dispatch({ type: "setShift", shift: value })
565 ),
566 unstable_setIncludesBaseElement: useAction((value) =>
567 dispatch({ type: "setIncludesBaseElement", includesBaseElement: value })
568 ),
569 reset: useAction(() => dispatch({ type: "reset" })),
570 };
571}
572
573export type CompositeState = unstable_IdState & {
574 /**
575 * If enabled, the composite element will act as an
576 * [aria-activedescendant](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant)
577 * container instead of
578 * [roving tabindex](https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex).
579 * DOM focus will remain on the composite while its items receive virtual focus.
580 * @default false
581 */
582 unstable_virtual: boolean;
583 /**
584 * Determines how `next` and `previous` functions will behave. If `rtl` is
585 * set to `true`, they will be inverted. This only affects the composite
586 * widget behavior. You still need to set `dir="rtl"` on HTML/CSS.
587 * @default false
588 */
589 rtl: boolean;
590 /**
591 * Defines the orientation of the composite widget. If the composite has a
592 * single row or column (one-dimensional), the `orientation` value determines
593 * which arrow keys can be used to move focus:
594 * - `undefined`: all arrow keys work.
595 * - `horizontal`: only left and right arrow keys work.
596 * - `vertical`: only up and down arrow keys work.
597 *
598 * It doesn't have any effect on two-dimensional composites.
599 * @default undefined
600 */
601 orientation?: Orientation;
602 /**
603 * Lists all the composite items with their `id`, DOM `ref`, `disabled` state
604 * and `groupId` if any. This state is automatically updated when
605 * `registerItem` and `unregisterItem` are called.
606 * @example
607 * const composite = useCompositeState();
608 * composite.items.forEach((item) => {
609 * const { id, ref, disabled, groupId } = item;
610 * ...
611 * });
612 */
613 items: Item[];
614 /**
615 * Lists all the composite groups with their `id` and DOM `ref`. This state
616 * is automatically updated when `registerGroup` and `unregisterGroup` are
617 * called.
618 * @example
619 * const composite = useCompositeState();
620 * composite.groups.forEach((group) => {
621 * const { id, ref } = group;
622 * ...
623 * });
624 */
625 groups: Group[];
626 /**
627 * The current focused item `id`.
628 * - `undefined` will automatically focus the first enabled composite item.
629 * - `null` will focus the base composite element and users will be able to
630 * navigate out of it using arrow keys.
631 * - If `currentId` is initially set to `null`, the base composite element
632 * itself will have focus and users will be able to navigate to it using
633 * arrow keys.
634 * @default undefined
635 * @example
636 * // First enabled item has initial focus
637 * useCompositeState();
638 * // Base composite element has initial focus
639 * useCompositeState({ currentId: null });
640 * // Specific composite item element has initial focus
641 * useCompositeState({ currentId: "item-id" });
642 */
643 currentId?: string | null;
644 /**
645 * On one-dimensional composites:
646 * - `true` loops from the last item to the first item and vice-versa.
647 * - `horizontal` loops only if `orientation` is `horizontal` or not set.
648 * - `vertical` loops only if `orientation` is `vertical` or not set.
649 * - If `currentId` is initially set to `null`, the composite element will
650 * be focused in between the last and first items.
651 *
652 * On two-dimensional composites:
653 * - `true` loops from the last row/column item to the first item in the
654 * same row/column and vice-versa. If it's the last item in the last row, it
655 * moves to the first item in the first row and vice-versa.
656 * - `horizontal` loops only from the last row item to the first item in
657 * the same row.
658 * - `vertical` loops only from the last column item to the first item in
659 * the column row.
660 * - If `currentId` is initially set to `null`, vertical loop will have no
661 * effect as moving down from the last row or up from the first row will
662 * focus the composite element.
663 * - If `wrap` matches the value of `loop`, it'll wrap between the last
664 * item in the last row or column and the first item in the first row or
665 * column and vice-versa.
666 * @default false
667 */
668 loop: boolean | Orientation;
669 /**
670 * **Has effect only on two-dimensional composites**. If enabled, moving to
671 * the next item from the last one in a row or column will focus the first
672 * item in the next row or column and vice-versa.
673 * - `true` wraps between rows and columns.
674 * - `horizontal` wraps only between rows.
675 * - `vertical` wraps only between columns.
676 * - If `loop` matches the value of `wrap`, it'll wrap between the last
677 * item in the last row or column and the first item in the first row or
678 * column and vice-versa.
679 * @default false
680 */
681 wrap: boolean | Orientation;
682 /**
683 * **Has effect only on two-dimensional composites**. If enabled, moving up
684 * or down when there's no next item or the next item is disabled will shift
685 * to the item right before it.
686 * @default false
687 */
688 shift: boolean;
689 /**
690 * Stores the number of moves that have been performed by calling `move`,
691 * `next`, `previous`, `up`, `down`, `first` or `last`.
692 * @default 0
693 */
694 unstable_moves: number;
695 /**
696 * @default false
697 * @private
698 */
699 unstable_hasActiveWidget: boolean;
700 /**
701 * @default false
702 * @private
703 */
704 unstable_includesBaseElement: boolean;
705};
706
707export type CompositeActions = unstable_IdActions & {
708 /**
709 * Registers a composite item.
710 * @example
711 * const ref = React.useRef();
712 * const composite = useCompositeState();
713 * React.useEffect(() => {
714 * composite.registerItem({ ref, id: "id" });
715 * return () => composite.unregisterItem("id");
716 * }, []);
717 */
718 registerItem: (item: Item) => void;
719 /**
720 * Unregisters a composite item.
721 * @example
722 * const ref = React.useRef();
723 * const composite = useCompositeState();
724 * React.useEffect(() => {
725 * composite.registerItem({ ref, id: "id" });
726 * return () => composite.unregisterItem("id");
727 * }, []);
728 */
729 unregisterItem: (id: string) => void;
730 /**
731 * Registers a composite group.
732 * @example
733 * const ref = React.useRef();
734 * const composite = useCompositeState();
735 * React.useEffect(() => {
736 * composite.registerGroup({ ref, id: "id" });
737 * return () => composite.unregisterGroup("id");
738 * }, []);
739 */
740 registerGroup: (group: Group) => void;
741 /**
742 * Unregisters a composite group.
743 * @example
744 * const ref = React.useRef();
745 * const composite = useCompositeState();
746 * React.useEffect(() => {
747 * composite.registerGroup({ ref, id: "id" });
748 * return () => composite.unregisterGroup("id");
749 * }, []);
750 */
751 unregisterGroup: (id: string) => void;
752 /**
753 * Moves focus to a given item ID.
754 * @example
755 * const composite = useCompositeState();
756 * composite.move("item-2"); // focus item 2
757 */
758 move: (id: string | null) => void;
759 /**
760 * Moves focus to the next item.
761 */
762 next: (unstable_allTheWay?: boolean) => void;
763 /**
764 * Moves focus to the previous item.
765 */
766 previous: (unstable_allTheWay?: boolean) => void;
767 /**
768 * Moves focus to the item above.
769 */
770 up: (unstable_allTheWay?: boolean) => void;
771 /**
772 * Moves focus to the item below.
773 */
774 down: (unstable_allTheWay?: boolean) => void;
775 /**
776 * Moves focus to the first item.
777 */
778 first: () => void;
779 /**
780 * Moves focus to the last item.
781 */
782 last: () => void;
783 /**
784 * Sorts the `composite.items` based on the items position in the DOM. This
785 * is especially useful after modifying the composite items order in the DOM.
786 * Most of the time, though, you don't need to manually call this function as
787 * the re-ordering happens automatically.
788 */
789 sort: () => void;
790 /**
791 * Sets `virtual`.
792 */
793 unstable_setVirtual: React.Dispatch<
794 React.SetStateAction<CompositeState["unstable_virtual"]>
795 >;
796 /**
797 * Sets `rtl`.
798 * @example
799 * const composite = useCompositeState({ rtl: true });
800 * composite.setRTL(false);
801 */
802 setRTL: React.Dispatch<React.SetStateAction<CompositeState["rtl"]>>;
803 /**
804 * Sets `orientation`.
805 */
806 setOrientation: React.Dispatch<
807 React.SetStateAction<CompositeState["orientation"]>
808 >;
809 /**
810 * Sets `currentId`. This is different from `composite.move` as this only
811 * updates the `currentId` state without moving focus. When the composite
812 * widget gets focused by the user, the item referred by the `currentId`
813 * state will get focus.
814 * @example
815 * const composite = useCompositeState({ currentId: "item-1" });
816 * // Updates `composite.currentId` to `item-2`
817 * composite.setCurrentId("item-2");
818 */
819 setCurrentId: React.Dispatch<
820 React.SetStateAction<CompositeState["currentId"]>
821 >;
822 /**
823 * Sets `loop`.
824 */
825 setLoop: React.Dispatch<React.SetStateAction<CompositeState["loop"]>>;
826 /**
827 * Sets `wrap`.
828 */
829 setWrap: React.Dispatch<React.SetStateAction<CompositeState["wrap"]>>;
830 /**
831 * Sets `shift`.
832 */
833 setShift: React.Dispatch<React.SetStateAction<CompositeState["shift"]>>;
834 /**
835 * Resets to initial state.
836 * @example
837 * // On initial render, currentId will be item-1 and loop will be true
838 * const composite = useCompositeState({
839 * currentId: "item-1",
840 * loop: true,
841 * });
842 * // On next render, currentId will be item-2 and loop will be false
843 * composite.setCurrentId("item-2");
844 * composite.setLoop(false);
845 * // On next render, currentId will be item-1 and loop will be true
846 * composite.reset();
847 */
848 reset: () => void;
849 /**
850 * Sets `includesBaseElement`.
851 * @private
852 */
853 unstable_setIncludesBaseElement: React.Dispatch<
854 React.SetStateAction<CompositeState["unstable_includesBaseElement"]>
855 >;
856 /**
857 * Sets `hasActiveWidget`.
858 * @private
859 */
860 unstable_setHasActiveWidget: React.Dispatch<
861 React.SetStateAction<CompositeState["unstable_hasActiveWidget"]>
862 >;
863};
864
865export type CompositeInitialState = unstable_IdInitialState &
866 Partial<
867 Pick<
868 CompositeState,
869 | "unstable_virtual"
870 | "rtl"
871 | "orientation"
872 | "currentId"
873 | "loop"
874 | "wrap"
875 | "shift"
876 | "unstable_includesBaseElement"
877 >
878 >;
879
880export type CompositeStateReturn = unstable_IdStateReturn &
881 CompositeState &
882 CompositeActions;