UNPKG

3.86 kBJavaScriptView Raw
1/* eslint-disable react-hooks/exhaustive-deps */
2import querySelectorAll from 'dom-helpers/querySelectorAll';
3import React, { useCallback, useContext, useLayoutEffect, useMemo, useState, useRef } from 'react';
4export const FocusListContext = /*#__PURE__*/React.createContext(null);
5const defaultOpts = {
6 behavior: 'stop'
7};
8export function useListOption(dataItem) {
9 const ctx = useContext(FocusListContext);
10 const prevElement = useRef(null); // this is a bit convoluted because we want to use a ref object, a callback ref
11 // causes an extra render which is fine except that it means the list hook for
12 // anchor items fires before elements are processed
13
14 const ref = useRef(null);
15 useLayoutEffect(() => () => {
16 ctx == null ? void 0 : ctx.map.delete(ref.current);
17 }, []);
18 useLayoutEffect(() => {
19 if (prevElement.current !== ref.current) {
20 ctx == null ? void 0 : ctx.map.delete(prevElement.current);
21 }
22
23 prevElement.current = ref.current;
24
25 if (ref.current && (ctx == null ? void 0 : ctx.map.get(ref.current)) !== dataItem) {
26 ctx == null ? void 0 : ctx.map.set(ref.current, dataItem);
27 }
28 });
29 const focused = dataItem === (ctx == null ? void 0 : ctx.focusedItem);
30 return [ref, focused, focused ? ctx == null ? void 0 : ctx.activeId : undefined];
31}
32export const useFocusList = ({
33 scope: listRef,
34 anchorItem,
35 focusFirstItem: _focusFirstItem = false,
36 scopeSelector: _scopeSelector = '',
37 activeId
38}) => {
39 const map = useMemo(() => new WeakMap(), []);
40 const [focusedItem, setFocusedItem] = useState();
41 const itemSelector = `${_scopeSelector} [data-rw-focusable]`.trim();
42
43 const get = () => {
44 const items = querySelectorAll(listRef.current, itemSelector);
45 return [items, items.find(e => e.dataset.rwFocused === '')];
46 };
47
48 const list = useMemo(() => {
49 return {
50 size() {
51 const [items] = get();
52 return items.length;
53 },
54
55 get,
56 toDataItem: el => map.get(el),
57
58 first() {
59 const [[first]] = get();
60 return first;
61 },
62
63 focus(el) {
64 if (!el || map.has(el)) setFocusedItem(el ? map.get(el) : undefined);
65 },
66
67 last() {
68 const [items] = get();
69 return items[items.length - 1];
70 },
71
72 next({
73 behavior
74 } = defaultOpts) {
75 const [items, focusedItem] = get();
76 let nextIdx = items.indexOf(focusedItem) + 1;
77
78 if (nextIdx >= items.length) {
79 if (behavior === 'loop') return items[0];
80 if (behavior === 'clear') return undefined;
81 return focusedItem;
82 }
83
84 return items[nextIdx];
85 },
86
87 prev({
88 behavior
89 } = defaultOpts) {
90 const [items, focusedItem] = get();
91 let nextIdx = Math.max(0, items.indexOf(focusedItem)) - 1;
92
93 if (nextIdx < 0) {
94 if (behavior === 'loop') return items[items.length - 1];
95 if (behavior === 'clear') return undefined;
96 return focusedItem;
97 }
98
99 return items[nextIdx];
100 }
101
102 };
103 }, []);
104 useLayoutEffect(() => {
105 if (!anchorItem) {
106 list.focus(null);
107 return;
108 }
109
110 const element = get()[0].find(el => list.toDataItem(el) === anchorItem);
111 list.focus(element);
112 }, [anchorItem]);
113 useLayoutEffect(() => {
114 if (!listRef.current) return;
115 const [, focusedElement] = get();
116 const hasItem = focusedElement != null;
117
118 if (!hasItem && _focusFirstItem || hasItem && !listRef.current.contains(focusedElement)) {
119 if (_focusFirstItem) list.focus(list.first());else list.focus(null);
120 }
121 });
122 const context = useMemo(() => ({
123 map,
124 focusedItem,
125 activeId
126 }), [focusedItem, activeId]);
127 list.context = context;
128 list.getFocused = useCallback(() => focusedItem, [focusedItem]);
129
130 list.hasFocused = () => focusedItem !== undefined;
131
132 return list;
133};
\No newline at end of file