1 |
|
2 | import querySelectorAll from 'dom-helpers/querySelectorAll';
|
3 | import React, { useCallback, useContext, useLayoutEffect, useMemo, useState, useRef } from 'react';
|
4 | export const FocusListContext = React.createContext(null);
|
5 | const defaultOpts = {
|
6 | behavior: 'stop'
|
7 | };
|
8 | export function useListOption(dataItem) {
|
9 | const ctx = useContext(FocusListContext);
|
10 | const prevElement = useRef(null);
|
11 |
|
12 |
|
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 | }
|
32 | export 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 |