UNPKG

3.81 kBPlain TextView Raw
1/*
2 * Copyright 2020 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13import {useCallback, useEffect, useRef, useState} from 'react';
14import {useLayoutEffect} from './useLayoutEffect';
15import {useSSRSafeId} from '@react-aria/ssr';
16import {useValueEffect} from './';
17
18// copied from SSRProvider.tsx to reduce exports, if needed again, consider sharing
19let canUseDOM = Boolean(
20 typeof window !== 'undefined' &&
21 window.document &&
22 window.document.createElement
23);
24
25export let idsUpdaterMap: Map<string, { current: string | null }[]> = new Map();
26// This allows us to clean up the idsUpdaterMap when the id is no longer used.
27// Map is a strong reference, so unused ids wouldn't be cleaned up otherwise.
28// This can happen in suspended components where mount/unmount is not called.
29let registry;
30if (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 */
40export 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 // In Suspense, the cleanup function may be not called
64 // when it is though, also remove it from the finalization registry.
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 */
90export 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 */
115export 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