1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | import {useCallback, useEffect, useRef, useState} from 'react';
|
14 | import {useLayoutEffect} from './useLayoutEffect';
|
15 | import {useSSRSafeId} from '@react-aria/ssr';
|
16 | import {useValueEffect} from './';
|
17 |
|
18 |
|
19 | let canUseDOM = Boolean(
|
20 | typeof window !== 'undefined' &&
|
21 | window.document &&
|
22 | window.document.createElement
|
23 | );
|
24 |
|
25 | export let idsUpdaterMap: Map<string, { current: string | null }[]> = new Map();
|
26 |
|
27 |
|
28 |
|
29 | let registry;
|
30 | if (typeof FinalizationRegistry !== 'undefined') {
|
31 | registry = new FinalizationRegistry<string>((heldValue) => {
|
32 | idsUpdaterMap.delete(heldValue);
|
33 | });
|
34 | }
|
35 |
|
36 | /**
|
37 | * If a default is not provided, generate an id.
|
38 | * @param defaultId - Default component id.
|
39 | */
|
40 | export function useId(defaultId?: string): string {
|
41 | let [value, setValue] = useState(defaultId);
|
42 | let nextId = useRef(null);
|
43 |
|
44 | let res = useSSRSafeId(value);
|
45 | let cleanupRef = useRef(null);
|
46 |
|
47 | if (registry) {
|
48 | registry.register(cleanupRef, res);
|
49 | }
|
50 |
|
51 | if (canUseDOM) {
|
52 | const cacheIdRef = idsUpdaterMap.get(res);
|
53 | if (cacheIdRef && !cacheIdRef.includes(nextId)) {
|
54 | cacheIdRef.push(nextId);
|
55 | } else {
|
56 | idsUpdaterMap.set(res, [nextId]);
|
57 | }
|
58 | }
|
59 |
|
60 | useLayoutEffect(() => {
|
61 | let r = res;
|
62 | return () => {
|
63 |
|
64 |
|
65 | if (registry) {
|
66 | registry.unregister(cleanupRef);
|
67 | }
|
68 | idsUpdaterMap.delete(r);
|
69 | };
|
70 | }, [res]);
|
71 |
|
72 | // This cannot cause an infinite loop because the ref is always cleaned up.
|
73 | // eslint-disable-next-line
|
74 | useEffect(() => {
|
75 | let newId = nextId.current;
|
76 | if (newId) { setValue(newId); }
|
77 |
|
78 | return () => {
|
79 | if (newId) { nextId.current = null; }
|
80 | };
|
81 | });
|
82 |
|
83 | return res;
|
84 | }
|
85 |
|
86 | /**
|
87 | * Merges two ids.
|
88 | * Different ids will trigger a side-effect and re-render components hooked up with `useId`.
|
89 | */
|
90 | export function mergeIds(idA: string, idB: string): string {
|
91 | if (idA === idB) {
|
92 | return idA;
|
93 | }
|
94 |
|
95 | let setIdsA = idsUpdaterMap.get(idA);
|
96 | if (setIdsA) {
|
97 | setIdsA.forEach(ref => (ref.current = idB));
|
98 | return idB;
|
99 | }
|
100 |
|
101 | let setIdsB = idsUpdaterMap.get(idB);
|
102 | if (setIdsB) {
|
103 | setIdsB.forEach((ref) => (ref.current = idA));
|
104 | return idA;
|
105 | }
|
106 |
|
107 | return idB;
|
108 | }
|
109 |
|
110 | /**
|
111 | * Used to generate an id, and after render, check if that id is rendered so we know
|
112 | * if we can use it in places such as labelledby.
|
113 | * @param depArray - When to recalculate if the id is in the DOM.
|
114 | */
|
115 | export function useSlotId(depArray: ReadonlyArray<any> = []): string {
|
116 | let id = useId();
|
117 | let [resolvedId, setResolvedId] = useValueEffect(id);
|
118 | let updateId = useCallback(() => {
|
119 | setResolvedId(function *() {
|
120 | yield id;
|
121 |
|
122 | yield document.getElementById(id) ? id : undefined;
|
123 | });
|
124 | }, [id, setResolvedId]);
|
125 |
|
126 | useLayoutEffect(updateId, [id, updateId, ...depArray]);
|
127 |
|
128 | return resolvedId;
|
129 | }
|
130 |
|
\ | No newline at end of file |