@ringcentral/spring-base 1.11.0 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/chunks/twMerge.js CHANGED
@@ -7,12 +7,16 @@ import { extendTailwindMerge } from 'tailwind-merge';
7
7
  const ownerWindow = unstable_ownerWindow;
8
8
 
9
9
  const ARIA_HIDDEN = 'aria-hidden';
10
- const REF_COUNT_MAP = '__SUI_ARIA_HIDE_OUTSIDE_REF_COUNT_MAP__';
11
- const getRefCountMap = (window) => {
12
- if (!window[REF_COUNT_MAP]) {
13
- window[REF_COUNT_MAP] = new WeakMap();
10
+ const TOP_LAYER_ATTR = 'data-sui-top-layer';
11
+ const alwaysVisibleSelector = `[data-live-announcer="true"], [${TOP_LAYER_ATTR}]`;
12
+ const ARIA_HIDDEN_REF_COUNT_MAP = '__SUI_ARIA_HIDE_OUTSIDE_REF_COUNT_MAP__';
13
+ const INERT_REF_COUNT_MAP = '__SUI_ARIA_HIDE_OUTSIDE_INERT_REF_COUNT_MAP__';
14
+ const getRefCountMap = (window, mechanism) => {
15
+ const key = mechanism === 'inert' ? INERT_REF_COUNT_MAP : ARIA_HIDDEN_REF_COUNT_MAP;
16
+ if (!window[key]) {
17
+ window[key] = new WeakMap();
14
18
  }
15
- return window[REF_COUNT_MAP];
19
+ return window[key];
16
20
  };
17
21
  const OBSERVER_STACK = '__SUI_ARIA_HIDE_OUTSIDE_OBSERVER_STACK__';
18
22
  const getObserverStack = (window) => {
@@ -21,109 +25,316 @@ const getObserverStack = (window) => {
21
25
  }
22
26
  return window[OBSERVER_STACK];
23
27
  };
24
- const isHTMLOrSVGElement = (node) => {
25
- return 'dataset' in node;
26
- };
27
28
  const isElement = (node) => {
28
- return node.nodeType === Node.ELEMENT_NODE;
29
+ return (typeof node === 'object' &&
30
+ node != null &&
31
+ 'nodeType' in node &&
32
+ node.nodeType === 1);
33
+ };
34
+ const isAlwaysVisibleNode = (node) => {
35
+ return (node.getAttribute('data-live-announcer') === 'true' ||
36
+ node.hasAttribute(TOP_LAYER_ATTR));
29
37
  };
30
- const ariaHideOutside = (targets, root = document.body) => {
31
- const currentWindow = ownerWindow(root);
38
+ const createAriaHideOutsideController = (targets, options = {}) => {
39
+ const isLegacyRoot = isElement(options);
40
+ const configuredRoot = isLegacyRoot
41
+ ? options
42
+ : options.root;
43
+ const currentWindow = ownerWindow(configuredRoot ?? targets[0]);
32
44
  if (!currentWindow) {
33
- return () => { };
45
+ return {
46
+ addTarget: () => { },
47
+ removeTarget: () => { },
48
+ dispose: () => { },
49
+ };
34
50
  }
51
+ const root = configuredRoot ?? currentWindow.document.body;
52
+ const shouldUseInert = !isLegacyRoot &&
53
+ options.shouldUseInert &&
54
+ 'inert' in currentWindow.HTMLElement.prototype;
35
55
  const visibleNodes = new Set(targets);
56
+ const targetNodes = new Set(targets);
57
+ const automaticVisibleNodes = new Set();
36
58
  const hiddenNodes = new Set();
37
- const refCountMap = getRefCountMap(currentWindow);
38
- const observerStack = getObserverStack(currentWindow);
39
- const walker = currentWindow.document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
40
- acceptNode(node) {
41
- if (isHTMLOrSVGElement(node) &&
42
- node.dataset['liveAnnouncer'] === 'true') {
43
- visibleNodes.add(node);
59
+ const targetPaths = new Map();
60
+ const getTargetPath = (target) => {
61
+ const path = [];
62
+ let node = target;
63
+ while (node && root.contains(node)) {
64
+ path.push(node);
65
+ if (node === root) {
66
+ break;
44
67
  }
45
- if (visibleNodes.has(node) ||
46
- (node.parentElement &&
47
- hiddenNodes.has(node.parentElement) &&
48
- node.parentElement.getAttribute('role') !== 'row')) {
49
- return NodeFilter.FILTER_REJECT;
50
- }
51
- if (targets.some((target) => node.contains(target))) {
52
- return NodeFilter.FILTER_SKIP;
53
- }
54
- return NodeFilter.FILTER_ACCEPT;
55
- },
56
- });
68
+ node = node.parentElement;
69
+ }
70
+ return path;
71
+ };
72
+ for (const target of targets) {
73
+ targetPaths.set(target, getTargetPath(target));
74
+ }
75
+ const observerStack = getObserverStack(currentWindow);
76
+ const getHideMechanism = (node) => shouldUseInert && node instanceof currentWindow.HTMLElement
77
+ ? 'inert'
78
+ : 'aria-hidden';
79
+ const getHidden = (node, mechanism) => mechanism === 'inert'
80
+ ? node.inert
81
+ : node.getAttribute(ARIA_HIDDEN) === 'true';
82
+ const setHidden = (node, mechanism, hidden) => {
83
+ if (mechanism === 'inert') {
84
+ node.inert = hidden;
85
+ }
86
+ else if (hidden) {
87
+ node.setAttribute(ARIA_HIDDEN, 'true');
88
+ }
89
+ else {
90
+ node.removeAttribute(ARIA_HIDDEN);
91
+ }
92
+ };
93
+ const restoreHiddenNode = (node) => {
94
+ if (!hiddenNodes.delete(node)) {
95
+ return;
96
+ }
97
+ const mechanism = getHideMechanism(node);
98
+ const refCountMap = getRefCountMap(currentWindow, mechanism);
99
+ const count = refCountMap.get(node);
100
+ if (typeof count !== 'number') {
101
+ return;
102
+ }
103
+ if (count === 1) {
104
+ setHidden(node, mechanism, false);
105
+ refCountMap.delete(node);
106
+ }
107
+ else {
108
+ refCountMap.set(node, count - 1);
109
+ }
110
+ };
57
111
  const hide = (node) => {
112
+ const mechanism = getHideMechanism(node);
113
+ const refCountMap = getRefCountMap(currentWindow, mechanism);
58
114
  const refCount = refCountMap.get(node) ?? 0;
59
- if (node.getAttribute(ARIA_HIDDEN) === 'true' && refCount === 0) {
115
+ if (getHidden(node, mechanism) && refCount === 0) {
60
116
  return;
61
117
  }
62
118
  if (refCount === 0) {
63
- node.setAttribute(ARIA_HIDDEN, 'true');
119
+ setHidden(node, mechanism, true);
64
120
  }
65
121
  hiddenNodes.add(node);
66
122
  refCountMap.set(node, refCount + 1);
67
123
  };
124
+ const addAlwaysVisibleNode = (node) => {
125
+ automaticVisibleNodes.add(node);
126
+ visibleNodes.add(node);
127
+ };
128
+ const addAlwaysVisibleNodes = (node) => {
129
+ if (isAlwaysVisibleNode(node)) {
130
+ addAlwaysVisibleNode(node);
131
+ }
132
+ for (const alwaysVisibleNode of node.querySelectorAll(alwaysVisibleSelector)) {
133
+ addAlwaysVisibleNode(alwaysVisibleNode);
134
+ }
135
+ };
136
+ const removeDisconnectedAlwaysVisibleNodes = (node) => {
137
+ for (const alwaysVisibleNode of automaticVisibleNodes) {
138
+ if (!node.contains(alwaysVisibleNode) ||
139
+ root.contains(alwaysVisibleNode)) {
140
+ continue;
141
+ }
142
+ automaticVisibleNodes.delete(alwaysVisibleNode);
143
+ if (!targetNodes.has(alwaysVisibleNode)) {
144
+ visibleNodes.delete(alwaysVisibleNode);
145
+ }
146
+ }
147
+ };
148
+ const containsNode = (nodes, node) => {
149
+ for (const candidate of nodes) {
150
+ if (candidate.contains(node)) {
151
+ return true;
152
+ }
153
+ }
154
+ return false;
155
+ };
156
+ const containsVisibleNode = (node, nodes) => {
157
+ for (const visibleNode of nodes) {
158
+ if (node.contains(visibleNode)) {
159
+ return true;
160
+ }
161
+ }
162
+ return false;
163
+ };
164
+ const isCoveredByHiddenNode = (node) => {
165
+ let ancestor = node.parentElement;
166
+ while (ancestor && root.contains(ancestor)) {
167
+ if (hiddenNodes.has(ancestor)) {
168
+ return !(ancestor === node.parentElement &&
169
+ ancestor.getAttribute('role') === 'row');
170
+ }
171
+ ancestor = ancestor.parentElement;
172
+ }
173
+ return false;
174
+ };
175
+ const shouldAdoptHiddenNode = (node) => {
176
+ return (root.contains(node) &&
177
+ !hiddenNodes.has(node) &&
178
+ !containsNode(visibleNodes, node) &&
179
+ !containsVisibleNode(node, visibleNodes) &&
180
+ !isCoveredByHiddenNode(node));
181
+ };
182
+ const acceptNode = (node) => {
183
+ if (visibleNodes.has(node) ||
184
+ (node.parentElement &&
185
+ hiddenNodes.has(node.parentElement) &&
186
+ node.parentElement.getAttribute('role') !== 'row')) {
187
+ return currentWindow.NodeFilter.FILTER_REJECT;
188
+ }
189
+ if (containsVisibleNode(node, visibleNodes)) {
190
+ return currentWindow.NodeFilter.FILTER_SKIP;
191
+ }
192
+ return currentWindow.NodeFilter.FILTER_ACCEPT;
193
+ };
194
+ const walk = (walkRoot, includeRoot) => {
195
+ addAlwaysVisibleNodes(walkRoot);
196
+ const rootFilter = acceptNode(walkRoot);
197
+ if (includeRoot && rootFilter === currentWindow.NodeFilter.FILTER_ACCEPT) {
198
+ hide(walkRoot);
199
+ }
200
+ if (rootFilter === currentWindow.NodeFilter.FILTER_REJECT) {
201
+ return;
202
+ }
203
+ const walker = currentWindow.document.createTreeWalker(walkRoot, currentWindow.NodeFilter.SHOW_ELEMENT, { acceptNode });
204
+ let node = walker.nextNode();
205
+ while (node != null) {
206
+ hide(node);
207
+ node = walker.nextNode();
208
+ }
209
+ };
210
+ const addTarget = (target) => {
211
+ if (!root.contains(target)) {
212
+ return;
213
+ }
214
+ targetNodes.add(target);
215
+ visibleNodes.add(target);
216
+ const path = getTargetPath(target);
217
+ for (const node of path) {
218
+ restoreHiddenNode(node);
219
+ }
220
+ targetPaths.set(target, path);
221
+ };
222
+ const removeTarget = (target) => {
223
+ if (!targetNodes.delete(target)) {
224
+ return;
225
+ }
226
+ if (!automaticVisibleNodes.has(target)) {
227
+ visibleNodes.delete(target);
228
+ }
229
+ const targetPath = targetPaths.get(target);
230
+ targetPaths.delete(target);
231
+ const connectedAncestor = targetPath?.find((node) => root.contains(node));
232
+ if (!connectedAncestor) {
233
+ return;
234
+ }
235
+ let walkRoot = connectedAncestor;
236
+ while (walkRoot.parentElement &&
237
+ walkRoot.parentElement !== root &&
238
+ !containsVisibleNode(walkRoot.parentElement, visibleNodes)) {
239
+ walkRoot = walkRoot.parentElement;
240
+ }
241
+ walk(walkRoot, true);
242
+ };
68
243
  if (observerStack.length) {
69
244
  observerStack[observerStack.length - 1].disconnect();
70
245
  }
71
- let node = walker.nextNode();
72
- while (node != null) {
73
- hide(node);
74
- node = walker.nextNode();
75
- }
76
- const observer = new currentWindow.MutationObserver((changes) => {
246
+ walk(root, false);
247
+ const processChanges = (changes) => {
77
248
  for (const change of changes) {
78
- if (change.type !== 'childList' || change.addedNodes.length === 0) {
249
+ if (change.type !== 'childList') {
250
+ continue;
251
+ }
252
+ for (const node of change.removedNodes) {
253
+ if (isElement(node)) {
254
+ removeDisconnectedAlwaysVisibleNodes(node);
255
+ }
256
+ }
257
+ if (!change.target.isConnected || change.addedNodes.length === 0) {
79
258
  continue;
80
259
  }
81
- if (![...visibleNodes, ...hiddenNodes].some((node) => node.contains(change.target))) {
260
+ if (!containsNode(visibleNodes, change.target) &&
261
+ !containsNode(hiddenNodes, change.target)) {
82
262
  for (const node of change.addedNodes) {
83
- if (isHTMLOrSVGElement(node) &&
84
- node.dataset['liveAnnouncer'] === 'true') {
85
- visibleNodes.add(node);
86
- }
87
- else if (isElement(node)) {
88
- hide(node);
263
+ if (isElement(node) && root.contains(node)) {
264
+ walk(node, true);
89
265
  }
90
266
  }
91
267
  }
92
268
  }
93
- });
269
+ };
270
+ const observer = new currentWindow.MutationObserver(processChanges);
94
271
  observer.observe(root, { childList: true, subtree: true });
95
272
  const observerWrapper = {
96
273
  observe() {
97
274
  observer.observe(root, { childList: true, subtree: true });
98
275
  },
276
+ resume(releasedHiddenNodes, releasedAlwaysVisibleNodes) {
277
+ for (const node of releasedAlwaysVisibleNodes) {
278
+ if (root.contains(node)) {
279
+ addAlwaysVisibleNode(node);
280
+ }
281
+ }
282
+ for (const node of releasedHiddenNodes) {
283
+ if (shouldAdoptHiddenNode(node)) {
284
+ hide(node);
285
+ }
286
+ }
287
+ observerWrapper.observe();
288
+ },
99
289
  disconnect() {
100
290
  observer.disconnect();
101
291
  },
102
292
  };
103
293
  observerStack.push(observerWrapper);
104
- return () => {
105
- observer.disconnect();
106
- for (const node of hiddenNodes) {
107
- const count = refCountMap.get(node);
108
- if (count === 1) {
109
- node.removeAttribute(ARIA_HIDDEN);
110
- refCountMap.delete(node);
111
- }
112
- else if (typeof count === 'number') {
113
- refCountMap.set(node, count - 1);
114
- }
294
+ let isCleanedUp = false;
295
+ const dispose = () => {
296
+ if (isCleanedUp) {
297
+ return;
115
298
  }
299
+ isCleanedUp = true;
300
+ const pendingChanges = observer.takeRecords();
301
+ observer.disconnect();
302
+ processChanges(pendingChanges);
116
303
  if (observerWrapper === observerStack[observerStack.length - 1]) {
117
304
  observerStack.pop();
118
- if (observerStack.length) {
119
- observerStack[observerStack.length - 1].observe();
305
+ const previousObserver = observerStack[observerStack.length - 1];
306
+ if (previousObserver?.resume) {
307
+ previousObserver.resume(hiddenNodes, automaticVisibleNodes);
308
+ }
309
+ else {
310
+ previousObserver?.observe();
120
311
  }
121
312
  }
122
313
  else {
123
314
  observerStack.splice(observerStack.indexOf(observerWrapper), 1);
124
315
  }
316
+ for (const node of [...hiddenNodes]) {
317
+ restoreHiddenNode(node);
318
+ }
319
+ targetPaths.clear();
320
+ targetNodes.clear();
321
+ automaticVisibleNodes.clear();
322
+ };
323
+ return {
324
+ addTarget: (target) => {
325
+ if (!isCleanedUp) {
326
+ addTarget(target);
327
+ }
328
+ },
329
+ removeTarget: (target) => {
330
+ if (!isCleanedUp) {
331
+ removeTarget(target);
332
+ }
333
+ },
334
+ dispose,
125
335
  };
126
336
  };
337
+ const ariaHideOutside = (targets, options = {}) => createAriaHideOutsideController(targets, options).dispose;
127
338
 
128
339
  const combineProps = (baseProps, newProps, coverPropKeys) => innerCombineProps({
129
340
  baseProps,
@@ -273,4 +484,4 @@ const twMerge = extendTailwindMerge({
273
484
  },
274
485
  });
275
486
 
276
- export { ariaHideOutside as a, combineClasses as b, combineProps as c, getRefElement as g, isRef as i, logInDev as l, ownerWindow as o, twMerge as t };
487
+ export { ariaHideOutside as a, combineClasses as b, combineProps as c, createAriaHideOutsideController as d, getRefElement as g, isRef as i, logInDev as l, ownerWindow as o, twMerge as t };
package/hooks.js CHANGED
@@ -5,7 +5,7 @@ import { useState, useRef, useEffect, useCallback, useLayoutEffect, useMemo } fr
5
5
  import { useWindowContext, useResponsiveContext, getMatchDetail } from './contexts.js';
6
6
  import uniqueId from 'lodash/uniqueId';
7
7
  import { useButton as useButton$1 } from '@mui/base/useButton';
8
- import { c as combineProps, a as ariaHideOutside, g as getRefElement, l as logInDev, o as ownerWindow } from './chunks/twMerge.js';
8
+ import { c as combineProps, d as createAriaHideOutsideController, g as getRefElement, l as logInDev, o as ownerWindow } from './chunks/twMerge.js';
9
9
  import './config.js';
10
10
  import 'lodash/omit';
11
11
  import 'lodash/pick';
@@ -241,18 +241,42 @@ function useButton(parameters) {
241
241
  }
242
242
 
243
243
  const useAriaHideOutside = (props = {}) => {
244
- const { enable = true } = props;
245
- const { externalWindow, document } = useWindowContext();
244
+ const { enable = true, shouldUseInert = false } = props;
245
+ const { document } = useWindowContext();
246
246
  const ariaHideOutsideRef = useRef(null);
247
- useEffect(() => {
247
+ const branchNodesRef = useRef(new Set());
248
+ const controllerRef = useRef();
249
+ const setAriaHideOutsideTarget = useCallback((node, isActive) => {
250
+ if (isActive) {
251
+ branchNodesRef.current.add(node);
252
+ controllerRef.current?.addTarget(node);
253
+ }
254
+ else {
255
+ branchNodesRef.current.delete(node);
256
+ controllerRef.current?.removeTarget(node);
257
+ }
258
+ }, []);
259
+ useLayoutEffect(() => {
248
260
  if (enable && ariaHideOutsideRef.current) {
249
- return ariaHideOutside([ariaHideOutsideRef.current], document?.body);
261
+ const controller = createAriaHideOutsideController([ariaHideOutsideRef.current, ...branchNodesRef.current], {
262
+ root: document?.body,
263
+ shouldUseInert,
264
+ });
265
+ controllerRef.current = controller;
266
+ return () => {
267
+ if (controllerRef.current === controller) {
268
+ controllerRef.current = undefined;
269
+ }
270
+ controller.dispose();
271
+ };
250
272
  }
273
+ controllerRef.current = undefined;
251
274
  return undefined;
252
- }, [enable, externalWindow, document?.body]);
275
+ }, [enable, shouldUseInert, document?.body]);
253
276
  return {
254
277
  ariaHideOutsideRef: ariaHideOutsideRef,
255
278
  ariaHideEnabled: enable,
279
+ setAriaHideOutsideTarget,
256
280
  };
257
281
  };
258
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ringcentral/spring-base",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "author": "RingCentral",
5
5
  "description": "Core utilities, hooks, and foundational functionality for Spring UI components, providing essential building blocks for React applications",
6
6
  "main": "./index.js",
@@ -1,8 +1,10 @@
1
1
  /// <reference types="react" />
2
2
  export type UseAriaHideOutsideProps = {
3
3
  enable?: boolean;
4
+ shouldUseInert?: boolean;
4
5
  };
5
6
  export declare const useAriaHideOutside: (props?: UseAriaHideOutsideProps) => {
6
7
  ariaHideOutsideRef: import("react").RefObject<HTMLElement>;
7
8
  ariaHideEnabled: boolean;
9
+ setAriaHideOutsideTarget: (node: HTMLElement, isActive: boolean) => void;
8
10
  };
@@ -10,7 +10,7 @@ export type UseKeyboardMoveFocusParams<T> = {
10
10
  endSearchDebounceTime?: number;
11
11
  } & Pick<MoveIndexInTwoDimensionalOptions, 'infinite'>;
12
12
  export declare const useKeyboardMoveFocus: <T>({ infinite, options, focusedIndexRef, onFocusedIndexChange, getOptionDisabled, getOptionSearchText, columns, endSearchDebounceTime, }: UseKeyboardMoveFocusParams<T>) => {
13
- getPrevFocusableOption: (index?: number, isInfinite?: any) => number;
14
- getNextFocusableOption: (index?: number, isInfinite?: any) => number;
13
+ getPrevFocusableOption: (index?: number, isInfinite?: boolean) => number;
14
+ getNextFocusableOption: (index?: number, isInfinite?: boolean) => number;
15
15
  onKeyFocusedIndexHandle: (event: React.KeyboardEvent<any>) => void;
16
16
  };
@@ -1 +1,11 @@
1
- export declare const ariaHideOutside: (targets: Element[], root?: HTMLElement) => () => void;
1
+ export type AriaHideOutsideController = {
2
+ addTarget: (target: Element) => void;
3
+ removeTarget: (target: Element) => void;
4
+ dispose: () => void;
5
+ };
6
+ export type AriaHideOutsideOptions = {
7
+ root?: Element;
8
+ shouldUseInert?: boolean;
9
+ };
10
+ export declare const createAriaHideOutsideController: (targets: Element[], options?: AriaHideOutsideOptions | Element) => AriaHideOutsideController;
11
+ export declare const ariaHideOutside: (targets: Element[], options?: AriaHideOutsideOptions | Element) => () => void;
@@ -1,5 +1,6 @@
1
1
  export * from './a11yKeyboardCode';
2
- export * from './ariaHideOutside';
2
+ export { ariaHideOutside } from './ariaHideOutside';
3
+ export type { AriaHideOutsideOptions } from './ariaHideOutside';
3
4
  export * from './capitalize';
4
5
  export * from './checkDefaultPrevented';
5
6
  export * from './checkElementName';