UNPKG

2.91 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
25let idsUpdaterMap: Map<string, (v: string) => void> = new Map();
26
27/**
28 * If a default is not provided, generate an id.
29 * @param defaultId - Default component id.
30 */
31export function useId(defaultId?: string): string {
32 let [value, setValue] = useState(defaultId);
33 let nextId = useRef(null);
34
35 let res = useSSRSafeId(value);
36
37 let updateValue = useCallback((val) => {
38 nextId.current = val;
39 }, []);
40
41 if (canUseDOM) {
42 idsUpdaterMap.set(res, updateValue);
43 }
44
45 useLayoutEffect(() => {
46 let r = res;
47 return () => {
48 idsUpdaterMap.delete(r);
49 };
50 }, [res]);
51
52 // This cannot cause an infinite loop because the ref is updated first.
53 // eslint-disable-next-line
54 useEffect(() => {
55 let newId = nextId.current;
56 if (newId) {
57 nextId.current = null;
58 setValue(newId);
59 }
60 });
61
62 return res;
63}
64
65/**
66 * Merges two ids.
67 * Different ids will trigger a side-effect and re-render components hooked up with `useId`.
68 */
69export function mergeIds(idA: string, idB: string): string {
70 if (idA === idB) {
71 return idA;
72 }
73
74 let setIdA = idsUpdaterMap.get(idA);
75 if (setIdA) {
76 setIdA(idB);
77 return idB;
78 }
79
80 let setIdB = idsUpdaterMap.get(idB);
81 if (setIdB) {
82 setIdB(idA);
83 return idA;
84 }
85
86 return idB;
87}
88
89/**
90 * Used to generate an id, and after render, check if that id is rendered so we know
91 * if we can use it in places such as labelledby.
92 * @param depArray - When to recalculate if the id is in the DOM.
93 */
94export function useSlotId(depArray: ReadonlyArray<any> = []): string {
95 let id = useId();
96 let [resolvedId, setResolvedId] = useValueEffect(id);
97 let updateId = useCallback(() => {
98 setResolvedId(function *() {
99 yield id;
100
101 yield document.getElementById(id) ? id : undefined;
102 });
103 }, [id, setResolvedId]);
104
105 useLayoutEffect(updateId, [id, updateId, ...depArray]);
106
107 return resolvedId;
108}