Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import { useState } from 'react';
2import { useLayoutEffect } from './utils/react';
3import { Ref } from './types';
4
5/** Creates a priority stack of elements so that we can determine the "deepest" one to be the active hook */
6export const makePriorityHook = () => {
7 const listeners: Set<Function> = new Set();
8 const priorityStack: HTMLElement[] = [];
9
10 const sortByHierarchy = (a: HTMLElement, b: HTMLElement) => {
11 const x = a.compareDocumentPosition(b);
12 return (
13 (x & 16 /* a contains b */ && 1) ||
14 (x & 8 /* b contains a */ && -1) ||
15 (x & 4 /* a follows b */ && 1) ||
16 (x & 2 /* b follows a */ && -1) ||
17 0
18 );
19 };
20
21 /** Indicates whether a given element on a stack of active priority hooks is the deepest element. */
22 return function usePriority<T extends HTMLElement>(
23 ref: Ref<T>,
24 disabled?: boolean
25 ): boolean {
26 function computeHasPriority(): boolean {
27 if (!ref.current) return false;
28 const tempStack = priorityStack.concat(ref.current).sort(sortByHierarchy);
29 return tempStack[0] === ref.current;
30 }
31
32 const isDisabled = !!disabled;
33 const [hasPriority, setHasPriority] = useState(computeHasPriority);
34
35 useLayoutEffect(() => {
36 if (!ref.current || isDisabled) return;
37
38 const { current } = ref;
39
40 function onChange() {
41 setHasPriority(() => priorityStack[0] === ref.current);
42 }
43
44 priorityStack.push(current);
45 priorityStack.sort(sortByHierarchy);
46 listeners.add(onChange);
47 listeners.forEach(fn => fn());
48
49 return () => {
50 const index = priorityStack.indexOf(current);
51 priorityStack.splice(index, 1);
52 listeners.delete(onChange);
53 listeners.forEach(fn => fn());
54 };
55 }, [ref, isDisabled]);
56
57 return hasPriority;
58 };
59};