UNPKG

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