@ringcentral/spring-base 1.11.2 → 1.12.0

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.
@@ -0,0 +1,234 @@
1
+ const LIVE_ANNOUNCER_ATTR = 'data-live-announcer';
2
+ const visuallyHiddenStyleProperties = {
3
+ border: '0',
4
+ clip: 'rect(0 0 0 0)',
5
+ clipPath: 'inset(50%)',
6
+ height: '1px',
7
+ margin: '-1px',
8
+ overflow: 'hidden',
9
+ padding: '0',
10
+ position: 'absolute',
11
+ width: '1px',
12
+ whiteSpace: 'nowrap',
13
+ };
14
+ const applyVisuallyHiddenStyles = (el) => {
15
+ Object.assign(el.style, visuallyHiddenStyleProperties);
16
+ };
17
+
18
+ class RepeatStringText {
19
+ last = {};
20
+ usedZwsp = {
21
+ assertive: false,
22
+ polite: false,
23
+ };
24
+ next(politeness, message) {
25
+ const useZwsp = this.last[politeness] === message ? !this.usedZwsp[politeness] : false;
26
+ this.last[politeness] = message;
27
+ this.usedZwsp[politeness] = useZwsp;
28
+ return useZwsp ? `\u200B${message}` : message;
29
+ }
30
+ reset(politeness) {
31
+ const channels = politeness
32
+ ? [politeness]
33
+ : ['assertive', 'polite'];
34
+ for (const channel of channels) {
35
+ this.last[channel] = undefined;
36
+ this.usedZwsp[channel] = false;
37
+ }
38
+ }
39
+ }
40
+ class LiveAnnouncer {
41
+ node = null;
42
+ assertiveLog = null;
43
+ politeLog = null;
44
+ doc;
45
+ repeatText = new RepeatStringText();
46
+ ready = false;
47
+ pending = [];
48
+ firstDelayTimerId = null;
49
+ constructor(doc) {
50
+ this.doc = doc;
51
+ if (typeof doc === 'undefined' || !doc.body) {
52
+ return;
53
+ }
54
+ this.node = doc.createElement('div');
55
+ this.node.setAttribute(LIVE_ANNOUNCER_ATTR, 'true');
56
+ applyVisuallyHiddenStyles(this.node);
57
+ this.assertiveLog = this.createLog('assertive');
58
+ this.node.appendChild(this.assertiveLog);
59
+ this.politeLog = this.createLog('polite');
60
+ this.node.appendChild(this.politeLog);
61
+ doc.body.prepend(this.node);
62
+ }
63
+ isAttached() {
64
+ return Boolean(this.node?.isConnected);
65
+ }
66
+ createLog(ariaLive) {
67
+ const node = this.doc.createElement('div');
68
+ node.setAttribute('role', 'log');
69
+ node.setAttribute('aria-live', ariaLive);
70
+ node.setAttribute('aria-relevant', 'additions');
71
+ return node;
72
+ }
73
+ destroy() {
74
+ this.cancelFirstDelay();
75
+ if (!this.node) {
76
+ return;
77
+ }
78
+ const parent = this.node.parentNode;
79
+ if (parent) {
80
+ parent.removeChild(this.node);
81
+ }
82
+ this.node = null;
83
+ this.assertiveLog = null;
84
+ this.politeLog = null;
85
+ this.repeatText.reset();
86
+ }
87
+ schedule(message, politeness = 'assertive', timeout = LIVEREGION_TIMEOUT_DELAY) {
88
+ if (this.ready || isTestAnnounceEnvironment()) {
89
+ this.announce(message, politeness, timeout);
90
+ return;
91
+ }
92
+ this.pending.push({ message, politeness, timeout });
93
+ if (this.firstDelayTimerId != null) {
94
+ return;
95
+ }
96
+ const win = this.doc.defaultView ?? globalThis;
97
+ this.firstDelayTimerId = win.setTimeout(() => {
98
+ this.firstDelayTimerId = null;
99
+ this.ready = true;
100
+ const queued = this.pending;
101
+ this.pending = [];
102
+ for (const item of queued) {
103
+ this.announce(item.message, item.politeness, item.timeout);
104
+ }
105
+ }, FIRST_ANNOUNCE_DELAY_MS);
106
+ }
107
+ announce(message, politeness = 'assertive', timeout = LIVEREGION_TIMEOUT_DELAY) {
108
+ if (!this.node) {
109
+ return;
110
+ }
111
+ const node = this.doc.createElement('div');
112
+ if (typeof message === 'object') {
113
+ node.setAttribute('role', 'img');
114
+ node.setAttribute('aria-labelledby', message['aria-labelledby']);
115
+ }
116
+ else {
117
+ node.textContent = this.repeatText.next(politeness, message);
118
+ }
119
+ if (politeness === 'assertive') {
120
+ this.assertiveLog?.appendChild(node);
121
+ }
122
+ else {
123
+ this.politeLog?.appendChild(node);
124
+ }
125
+ if (message !== '') {
126
+ const win = this.doc.defaultView ?? globalThis;
127
+ win.setTimeout(() => {
128
+ node.remove();
129
+ }, timeout);
130
+ }
131
+ }
132
+ clear(politeness) {
133
+ this.pending = politeness
134
+ ? this.pending.filter((item) => item.politeness !== politeness)
135
+ : [];
136
+ if (!this.node) {
137
+ return;
138
+ }
139
+ if ((!politeness || politeness === 'assertive') && this.assertiveLog) {
140
+ this.assertiveLog.innerHTML = '';
141
+ this.repeatText.reset('assertive');
142
+ }
143
+ if ((!politeness || politeness === 'polite') && this.politeLog) {
144
+ this.politeLog.innerHTML = '';
145
+ this.repeatText.reset('polite');
146
+ }
147
+ }
148
+ cancelFirstDelay() {
149
+ if (this.firstDelayTimerId != null) {
150
+ const win = this.doc.defaultView ?? globalThis;
151
+ win.clearTimeout(this.firstDelayTimerId);
152
+ this.firstDelayTimerId = null;
153
+ }
154
+ this.pending = [];
155
+ }
156
+ }
157
+ const LIVEREGION_TIMEOUT_DELAY = 7000;
158
+ const FIRST_ANNOUNCE_DELAY_MS = 100;
159
+ const announcersByDocument = new WeakMap();
160
+ const getLiveAnnouncer = (doc) => {
161
+ return announcersByDocument.get(doc) ?? null;
162
+ };
163
+ const ensureLiveAnnouncer = (doc) => {
164
+ const existing = announcersByDocument.get(doc);
165
+ if (existing?.isAttached()) {
166
+ return existing;
167
+ }
168
+ existing?.destroy();
169
+ const announcer = new LiveAnnouncer(doc);
170
+ announcersByDocument.set(doc, announcer);
171
+ return announcer;
172
+ };
173
+ const clearLiveAnnouncerRegistry = (doc) => {
174
+ if (doc) {
175
+ const announcer = announcersByDocument.get(doc);
176
+ announcer?.destroy();
177
+ announcersByDocument.delete(doc);
178
+ return;
179
+ }
180
+ };
181
+ const isTestAnnounceEnvironment = () => {
182
+ const globalObj = globalThis;
183
+ if (typeof globalObj.IS_REACT_ACT_ENVIRONMENT === 'boolean') {
184
+ return globalObj.IS_REACT_ACT_ENVIRONMENT;
185
+ }
186
+ return typeof jest !== 'undefined';
187
+ };
188
+
189
+ const resolveDocument = (doc) => {
190
+ if (doc) {
191
+ return doc;
192
+ }
193
+ if (typeof document !== 'undefined') {
194
+ return document;
195
+ }
196
+ return undefined;
197
+ };
198
+ const resolveTimeoutAndDocument = (timeoutOrOptions) => {
199
+ if (typeof timeoutOrOptions === 'number') {
200
+ return { timeout: timeoutOrOptions };
201
+ }
202
+ if (timeoutOrOptions && typeof timeoutOrOptions === 'object') {
203
+ return {
204
+ timeout: timeoutOrOptions.timeout ?? LIVEREGION_TIMEOUT_DELAY,
205
+ document: timeoutOrOptions.document,
206
+ };
207
+ }
208
+ return { timeout: LIVEREGION_TIMEOUT_DELAY };
209
+ };
210
+ function announce(message, politeness = 'assertive', timeoutOrOptions) {
211
+ const { timeout, document: optionDocument } = resolveTimeoutAndDocument(timeoutOrOptions);
212
+ const doc = resolveDocument(optionDocument);
213
+ if (!doc?.body) {
214
+ return;
215
+ }
216
+ const liveAnnouncer = ensureLiveAnnouncer(doc);
217
+ liveAnnouncer.schedule(message, politeness, timeout);
218
+ }
219
+ function clearAnnouncer(politeness, doc) {
220
+ const target = resolveDocument(doc);
221
+ if (!target) {
222
+ return;
223
+ }
224
+ getLiveAnnouncer(target)?.clear(politeness);
225
+ }
226
+ function destroyAnnouncer(doc) {
227
+ const target = resolveDocument(doc);
228
+ if (!target) {
229
+ return;
230
+ }
231
+ clearLiveAnnouncerRegistry(target);
232
+ }
233
+
234
+ export { LIVEREGION_TIMEOUT_DELAY as L, announce as a, LIVE_ANNOUNCER_ATTR as b, clearAnnouncer as c, destroyAnnouncer as d, applyVisuallyHiddenStyles as e };
package/chunks/twMerge.js CHANGED
@@ -109,6 +109,9 @@ const createAriaHideOutsideController = (targets, options = {}) => {
109
109
  }
110
110
  };
111
111
  const hide = (node) => {
112
+ if (hiddenNodes.has(node)) {
113
+ return;
114
+ }
112
115
  const mechanism = getHideMechanism(node);
113
116
  const refCountMap = getRefCountMap(currentWindow, mechanism);
114
117
  const refCount = refCountMap.get(node) ?? 0;
@@ -214,10 +217,23 @@ const createAriaHideOutsideController = (targets, options = {}) => {
214
217
  targetNodes.add(target);
215
218
  visibleNodes.add(target);
216
219
  const path = getTargetPath(target);
217
- for (const node of path) {
218
- restoreHiddenNode(node);
220
+ const restoredAncestorIndexes = [];
221
+ for (let index = 0; index < path.length; index++) {
222
+ if (index > 0 && hiddenNodes.has(path[index])) {
223
+ restoredAncestorIndexes.push(index);
224
+ }
225
+ restoreHiddenNode(path[index]);
219
226
  }
220
227
  targetPaths.set(target, path);
228
+ for (const index of restoredAncestorIndexes) {
229
+ const ancestor = path[index];
230
+ const pathChild = path[index - 1];
231
+ for (const child of ancestor.children) {
232
+ if (child !== pathChild) {
233
+ walk(child, true);
234
+ }
235
+ }
236
+ }
221
237
  };
222
238
  const removeTarget = (target) => {
223
239
  if (!targetNodes.delete(target)) {
package/hooks.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { unstable_useEventCallback, unstable_ownerDocument, unstable_Timeout, unstable_useForkRef } from '@mui/utils';
2
2
  export { unstable_useControlled as useControlled } from '@mui/utils';
3
3
  import * as React from 'react';
4
- import { useState, useRef, useEffect, useCallback, useLayoutEffect, useMemo } from 'react';
4
+ import React__default, { useState, useRef, useEffect, useCallback, useLayoutEffect, useMemo } from 'react';
5
5
  import { useWindowContext, useResponsiveContext, getMatchDetail } from './contexts.js';
6
+ import { b as LIVE_ANNOUNCER_ATTR, e as applyVisuallyHiddenStyles, a as announce } from './chunks/index.js';
6
7
  import uniqueId from 'lodash/uniqueId';
7
8
  import { useButton as useButton$1 } from '@mui/base/useButton';
8
9
  import { c as combineProps, d as createAriaHideOutsideController, g as getRefElement, l as logInDev, o as ownerWindow } from './chunks/twMerge.js';
@@ -128,7 +129,7 @@ function useResultRef(cb) {
128
129
  }
129
130
 
130
131
  const DEFAULT_PREFIX = 'spring-id-';
131
- function useId(id, prefixMode) {
132
+ function useLegacyId(id, prefixMode) {
132
133
  const { current: resultId } = useResultRef(() => {
133
134
  if (id) {
134
135
  if (prefixMode) {
@@ -140,27 +141,40 @@ function useId(id, prefixMode) {
140
141
  });
141
142
  return resultId;
142
143
  }
144
+ const reactUseId = React__default.useId;
145
+ function useReactId(id, prefixMode) {
146
+ const reactId = reactUseId();
147
+ if (id) {
148
+ return prefixMode ? `${id}-${reactId}` : id;
149
+ }
150
+ return `${DEFAULT_PREFIX}${reactId}`;
151
+ }
152
+ const useId = reactUseId ? useReactId : useLegacyId;
143
153
 
144
- const visuallyHiddenStyles = `
145
- clip-path: inset(100%);
146
- clip: rect(1px, 1px, 1px, 1px);
147
- height: 1px;
148
- overflow: hidden;
149
- position: absolute;
150
- white-space: nowrap;
151
- width: 1px;
152
- `;
153
- const useAnnouncer = (id) => {
154
+ const isUseAnnouncerOptions = (value) => {
155
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
156
+ };
157
+ function useAnnouncer(idOrOptions, maybeOptions) {
158
+ const id = typeof idOrOptions === 'string' ? idOrOptions : undefined;
159
+ const options = isUseAnnouncerOptions(idOrOptions)
160
+ ? idOrOptions
161
+ : maybeOptions;
162
+ const politeness = options?.politeness ?? 'assertive';
154
163
  const _id = useId(id ? `rc-announcer-${id}` : 'rc-announcer', !id);
155
164
  const { document: targetDocument, externalWindow, globalThis: contextGlobal, } = useWindowContext();
156
165
  const win = externalWindow ?? contextGlobal;
157
166
  const ref = useRef(null);
158
167
  const lastMessageRef = useRef();
159
168
  const pendingTimerRef = useRef(null);
169
+ const pendingRemovalRef = useRef(null);
160
170
  const clearPendingTimer = useCallback(() => {
161
171
  pendingTimerRef.current?.();
162
172
  pendingTimerRef.current = null;
163
173
  }, []);
174
+ const clearPendingRemoval = useCallback(() => {
175
+ pendingRemovalRef.current?.();
176
+ pendingRemovalRef.current = null;
177
+ }, []);
164
178
  useEffect(() => {
165
179
  const currentDocument = targetDocument ??
166
180
  (typeof contextGlobal.document !== 'undefined'
@@ -169,18 +183,30 @@ const useAnnouncer = (id) => {
169
183
  if (!currentDocument?.body) {
170
184
  return undefined;
171
185
  }
172
- const el = currentDocument.createElement('div');
173
- ref.current = el;
174
- el.id = _id;
175
- el.setAttribute('aria-live', 'assertive');
176
- el.setAttribute('aria-atomic', 'true');
177
- el.setAttribute('aria-hidden', 'false');
178
- el.setAttribute('data-live-announcer', 'true');
179
- el.setAttribute('style', visuallyHiddenStyles);
180
- currentDocument.body.appendChild(el);
186
+ const currentEl = ref.current;
187
+ const canReuseCurrentEl = currentEl?.ownerDocument === currentDocument &&
188
+ currentEl.id === _id &&
189
+ currentDocument.body.contains(currentEl);
190
+ const el = canReuseCurrentEl
191
+ ? currentEl
192
+ : currentDocument.createElement('div');
193
+ if (canReuseCurrentEl) {
194
+ clearPendingRemoval();
195
+ el.setAttribute('aria-live', politeness);
196
+ }
197
+ else {
198
+ ref.current = el;
199
+ el.id = _id;
200
+ el.setAttribute('aria-live', politeness);
201
+ el.setAttribute('aria-atomic', 'true');
202
+ el.setAttribute('aria-hidden', 'false');
203
+ el.setAttribute(LIVE_ANNOUNCER_ATTR, 'true');
204
+ applyVisuallyHiddenStyles(el);
205
+ currentDocument.body.appendChild(el);
206
+ }
181
207
  return () => {
182
208
  clearPendingTimer();
183
- win.setTimeout(() => {
209
+ const timerId = win.setTimeout(() => {
184
210
  const body = currentDocument.body;
185
211
  if (body.contains(el)) {
186
212
  body.removeChild(el);
@@ -188,9 +214,24 @@ const useAnnouncer = (id) => {
188
214
  if (el === ref.current) {
189
215
  ref.current = null;
190
216
  }
217
+ if (pendingRemovalRef.current === cancelRemoval) {
218
+ pendingRemovalRef.current = null;
219
+ }
191
220
  });
221
+ const cancelRemoval = () => {
222
+ win.clearTimeout(timerId);
223
+ };
224
+ pendingRemovalRef.current = cancelRemoval;
192
225
  };
193
- }, [_id, targetDocument, contextGlobal, win, clearPendingTimer]);
226
+ }, [
227
+ _id,
228
+ politeness,
229
+ targetDocument,
230
+ contextGlobal,
231
+ win,
232
+ clearPendingTimer,
233
+ clearPendingRemoval,
234
+ ]);
194
235
  return useCallback((message) => {
195
236
  const el = ref.current;
196
237
  if (!el)
@@ -221,7 +262,20 @@ const useAnnouncer = (id) => {
221
262
  };
222
263
  pendingTimerRef.current = cancelTimer;
223
264
  }, [clearPendingTimer, win]);
224
- };
265
+ }
266
+
267
+ function useGlobalAnnouncer() {
268
+ const { document: targetDocument } = useWindowContext();
269
+ return useCallback((message, politeness = 'assertive', timeoutOrOptions) => {
270
+ const timeout = typeof timeoutOrOptions === 'number'
271
+ ? timeoutOrOptions
272
+ : timeoutOrOptions?.timeout;
273
+ announce(message, politeness, {
274
+ timeout,
275
+ document: targetDocument,
276
+ });
277
+ }, [targetDocument]);
278
+ }
225
279
 
226
280
  function useButton(parameters) {
227
281
  const muiButtonResult = useButton$1(parameters);
@@ -287,9 +341,9 @@ const useAudio = (init) => {
287
341
  return audio;
288
342
  });
289
343
  useEffect(() => {
344
+ const audio = audioRef.current;
290
345
  return () => {
291
- audioRef.current.srcObject = null;
292
- audioRef.current = null;
346
+ audio.srcObject = null;
293
347
  };
294
348
  }, []);
295
349
  return audioRef.current;
@@ -1123,13 +1177,14 @@ const useKeyboardMoveFocus = ({ infinite, options = [], focusedIndexRef, onFocus
1123
1177
  };
1124
1178
 
1125
1179
  const MOVE_ATTR_KEY = 'data-move-focused';
1180
+ const MOVE_INDEX_ATTR_KEY = 'data-move-index';
1126
1181
  const useOnlyOneFocusable = ({ focusedIndexRef, containerRef, }) => {
1127
- const moveFocusedId = useId(MOVE_ATTR_KEY, true);
1128
- const getIndexElm = (toIndex) => containerRef.current?.querySelector(`[${moveFocusedId}="${toIndex}"]`);
1182
+ const moveFocusedId = useId();
1183
+ const getIndexElm = (toIndex) => containerRef.current?.querySelector(`[${MOVE_ATTR_KEY}="${moveFocusedId}"][${MOVE_INDEX_ATTR_KEY}="${toIndex}"]`);
1129
1184
  const setIndexTabIndex = (toIndex) => {
1130
1185
  const toElm = getIndexElm(toIndex);
1131
1186
  if (toElm) {
1132
- const prevElm = containerRef.current?.querySelector(`[${moveFocusedId}][tabindex="0"]`);
1187
+ const prevElm = containerRef.current?.querySelector(`[${MOVE_ATTR_KEY}="${moveFocusedId}"][tabindex="0"]`);
1133
1188
  prevElm?.setAttribute('tabindex', '-1');
1134
1189
  toElm.setAttribute('tabindex', '0');
1135
1190
  }
@@ -1144,13 +1199,14 @@ const useOnlyOneFocusable = ({ focusedIndexRef, containerRef, }) => {
1144
1199
  setIndexTabIndex,
1145
1200
  getItemProps: (index, options) => {
1146
1201
  return combineProps({
1147
- [moveFocusedId]: index,
1202
+ [MOVE_ATTR_KEY]: moveFocusedId,
1203
+ [MOVE_INDEX_ATTR_KEY]: String(index),
1148
1204
  tabIndex: index === focusedIndexRef.current ? 0 : -1,
1149
1205
  onFocus: () => {
1150
1206
  focusedIndexRef.current = index;
1151
1207
  setIndexTabIndex(index);
1152
1208
  },
1153
- }, options);
1209
+ }, options ?? {});
1154
1210
  },
1155
1211
  };
1156
1212
  };
@@ -1966,4 +2022,4 @@ const useVirtuosoExternalWindow = () => {
1966
2022
  return undefined;
1967
2023
  };
1968
2024
 
1969
- export { createGlobalListener, globalListenerEventMap, isElmEqualOrContain, isTouchEvent, moveIndexInTwoDimension, teardown, useA11yKeyEvent, useActive, useAnnouncer, useAriaHideOutside, useAudio, useBodyScrollLock, useButton, useChange, useClickAway, useDebounce, useDepsChange, useEventCallback, useEventListener, useEver, useFocusVisible, useFontLoaded, useForceUpdate, useForkRef, useGlobalListener, useHiddenTabindex, useHover, useHoverDirty, useId, useImgLoaded, useInterval, useIsFocusVisible, useKeyDownOnce, useKeyboardMoveFocus, useLongPress, useMountState, useOnLayoutReRender, useOnReRender, useOnlyOneFocusable, useOverflow, usePrevious, useRefState, useResizeObserver, useResponsiveMatch, useResultRef, useRetry, useSleep, useTabOut, useTestRunnerDetection, useThrottle, useTimeout, useTouchMouseEvent, useVirtuosoExternalWindow, visuallyHiddenStyles };
2025
+ export { createGlobalListener, globalListenerEventMap, isElmEqualOrContain, isTouchEvent, moveIndexInTwoDimension, teardown, useA11yKeyEvent, useActive, useAnnouncer, useAriaHideOutside, useAudio, useBodyScrollLock, useButton, useChange, useClickAway, useDebounce, useDepsChange, useEventCallback, useEventListener, useEver, useFocusVisible, useFontLoaded, useForceUpdate, useForkRef, useGlobalAnnouncer, useGlobalListener, useHiddenTabindex, useHover, useHoverDirty, useId, useImgLoaded, useInterval, useIsFocusVisible, useKeyDownOnce, useKeyboardMoveFocus, useLongPress, useMountState, useOnLayoutReRender, useOnReRender, useOnlyOneFocusable, useOverflow, usePrevious, useRefState, useResizeObserver, useResponsiveMatch, useResultRef, useRetry, useSleep, useTabOut, useTestRunnerDetection, useThrottle, useTimeout, useTouchMouseEvent, useVirtuosoExternalWindow };
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  export { configure, rcConfiguration } from './config.js';
2
2
  export { RcResponsiveContext, WindowContext, breakpointList, getMatchDetail, useResponsiveContext, useWindowContext } from './contexts.js';
3
- export { createGlobalListener, globalListenerEventMap, isElmEqualOrContain, isTouchEvent, moveIndexInTwoDimension, teardown, useA11yKeyEvent, useActive, useAnnouncer, useAriaHideOutside, useAudio, useBodyScrollLock, useButton, useChange, useClickAway, useDebounce, useDepsChange, useEventCallback, useEventListener, useEver, useFocusVisible, useFontLoaded, useForceUpdate, useForkRef, useGlobalListener, useHiddenTabindex, useHover, useHoverDirty, useId, useImgLoaded, useInterval, useIsFocusVisible, useKeyDownOnce, useKeyboardMoveFocus, useLongPress, useMountState, useOnLayoutReRender, useOnReRender, useOnlyOneFocusable, useOverflow, usePrevious, useRefState, useResizeObserver, useResponsiveMatch, useResultRef, useRetry, useSleep, useTabOut, useTestRunnerDetection, useThrottle, useTimeout, useTouchMouseEvent, useVirtuosoExternalWindow, visuallyHiddenStyles } from './hooks.js';
3
+ export { createGlobalListener, globalListenerEventMap, isElmEqualOrContain, isTouchEvent, moveIndexInTwoDimension, teardown, useA11yKeyEvent, useActive, useAnnouncer, useAriaHideOutside, useAudio, useBodyScrollLock, useButton, useChange, useClickAway, useDebounce, useDepsChange, useEventCallback, useEventListener, useEver, useFocusVisible, useFontLoaded, useForceUpdate, useForkRef, useGlobalAnnouncer, useGlobalListener, useHiddenTabindex, useHover, useHoverDirty, useId, useImgLoaded, useInterval, useIsFocusVisible, useKeyDownOnce, useKeyboardMoveFocus, useLongPress, useMountState, useOnLayoutReRender, useOnReRender, useOnlyOneFocusable, useOverflow, usePrevious, useRefState, useResizeObserver, useResponsiveMatch, useResultRef, useRetry, useSleep, useTabOut, useTestRunnerDetection, useThrottle, useTimeout, useTouchMouseEvent, useVirtuosoExternalWindow } from './hooks.js';
4
4
  export { unstable_ownerDocument as ownerDocument, unstable_useControlled as useControlled } from '@mui/utils';
5
+ export { L as LIVEREGION_TIMEOUT_DELAY, a as announce, c as clearAnnouncer, d as destroyAnnouncer } from './chunks/index.js';
5
6
  export { a11yKeyboard, capitalize, checkDefaultPrevented, checkElementName, clearWindowSelection, getSelectionPosition, getTitle, globalStateClasses, isDeleteKeyboardEvent, isSuiElement, isUrl, isWebKit154, omit, outerSize, pick, preloadImg, removeClassName, runKeyHandler, setSelectionPosition, useClasses } from './utils.js';
6
7
  export { a as ariaHideOutside, b as combineClasses, c as combineProps, g as getRefElement, i as isRef, l as logInDev, o as ownerWindow, t as twMerge } from './chunks/twMerge.js';
7
8
  import 'react';
@@ -0,0 +1 @@
1
+ export { L as LIVEREGION_TIMEOUT_DELAY, a as announce, c as clearAnnouncer, d as destroyAnnouncer } from './chunks/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ringcentral/spring-base",
3
- "version": "1.11.2",
3
+ "version": "1.12.0",
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",
@@ -16,9 +16,9 @@
16
16
  "type-fest": "^2.12.2"
17
17
  },
18
18
  "peerDependencies": {
19
- "@types/react": ">=17.0.3",
20
- "react": ">=17.0.2",
21
- "react-dom": ">=17.0.2"
19
+ "@types/react": "^17.0.3 || ^18.0.0 || ^19.0.0",
20
+ "react": "^17.0.2 || ^18.0.0 || ^19.0.0",
21
+ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0"
22
22
  },
23
23
  "sideEffects": false,
24
24
  "license": "MIT",
@@ -1,6 +1,7 @@
1
1
  export * from './useA11yKeyEvent';
2
2
  export * from './useActive';
3
3
  export * from './useAnnouncer';
4
+ export * from './useGlobalAnnouncer';
4
5
  export * from './useButton';
5
6
  export * from './useAriaHideOutside';
6
7
  export * from './useAudio';
@@ -1,2 +1,5 @@
1
- export declare const visuallyHiddenStyles = "\n clip-path: inset(100%);\n clip: rect(1px, 1px, 1px, 1px);\n height: 1px;\n overflow: hidden;\n position: absolute;\n white-space: nowrap;\n width: 1px;\n";
2
- export declare const useAnnouncer: (id?: string) => (message?: string) => void;
1
+ import type { AnnouncerPoliteness } from '../../liveAnnouncer';
2
+ export type UseAnnouncerOptions = {
3
+ politeness?: AnnouncerPoliteness;
4
+ };
5
+ export declare function useAnnouncer(idOrOptions?: string | UseAnnouncerOptions, maybeOptions?: UseAnnouncerOptions): (message?: string) => void;
@@ -0,0 +1 @@
1
+ export * from './useGlobalAnnouncer';
@@ -0,0 +1,5 @@
1
+ import type { AnnounceMessage, AnnouncerPoliteness } from '../../liveAnnouncer/types';
2
+ export type GlobalAnnounceOptions = {
3
+ timeout?: number;
4
+ };
5
+ export declare function useGlobalAnnouncer(): (message: AnnounceMessage, politeness?: AnnouncerPoliteness, timeoutOrOptions?: number | GlobalAnnounceOptions) => void;
@@ -1 +1,3 @@
1
- export declare function useId(id?: string, prefixMode?: boolean): string;
1
+ declare function useReactId(id?: string, prefixMode?: boolean): string;
2
+ export declare const useId: typeof useReactId;
3
+ export {};
@@ -1,11 +1,14 @@
1
1
  /// <reference types="react" />
2
+ import type { HTMLDataAttribute } from '../../typings';
2
3
  export type UseOnlyOneFocusableParams = {
3
4
  focusedIndexRef: React.MutableRefObject<number>;
4
5
  containerRef: React.RefObject<HTMLElement>;
5
6
  };
7
+ type MoveFocusedItemProps = Omit<React.HTMLAttributes<HTMLElement>, 'children'> & HTMLDataAttribute;
6
8
  export declare const useOnlyOneFocusable: ({ focusedIndexRef, containerRef, }: UseOnlyOneFocusableParams) => {
7
9
  moveFocusedId: string;
8
10
  focusIndex: (toIndex: number) => void;
9
11
  setIndexTabIndex: (toIndex: number) => void;
10
- getItemProps: (index: number, options?: Omit<React.HTMLAttributes<HTMLElement>, 'children'>) => Omit<import("react").HTMLAttributes<HTMLElement>, "children">;
12
+ getItemProps: (index: number, options?: Omit<React.HTMLAttributes<HTMLElement>, 'children'>) => NonNullable<MoveFocusedItemProps>;
11
13
  };
14
+ export {};
package/src/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './config';
2
2
  export * from './contexts';
3
3
  export * from './hooks';
4
+ export * from './liveAnnouncer';
4
5
  export * from './typings';
5
6
  export * from './utils';
@@ -0,0 +1,25 @@
1
+ import type { AnnounceMessage, AnnouncerPoliteness } from './types';
2
+ export declare class LiveAnnouncer {
3
+ node: HTMLElement | null;
4
+ assertiveLog: HTMLElement | null;
5
+ politeLog: HTMLElement | null;
6
+ private readonly doc;
7
+ private readonly repeatText;
8
+ private ready;
9
+ private pending;
10
+ private firstDelayTimerId;
11
+ constructor(doc: Document);
12
+ isAttached(): boolean;
13
+ createLog(ariaLive: AnnouncerPoliteness): HTMLElement;
14
+ destroy(): void;
15
+ schedule(message: AnnounceMessage, politeness?: AnnouncerPoliteness, timeout?: number): void;
16
+ announce(message: AnnounceMessage, politeness?: AnnouncerPoliteness, timeout?: number): void;
17
+ clear(politeness?: AnnouncerPoliteness): void;
18
+ private cancelFirstDelay;
19
+ }
20
+ export declare const LIVEREGION_TIMEOUT_DELAY = 7000;
21
+ export declare const FIRST_ANNOUNCE_DELAY_MS = 100;
22
+ export declare const getLiveAnnouncer: (doc: Document) => LiveAnnouncer | null;
23
+ export declare const ensureLiveAnnouncer: (doc: Document) => LiveAnnouncer;
24
+ export declare const clearLiveAnnouncerRegistry: (doc?: Document) => void;
25
+ export declare const isTestAnnounceEnvironment: () => boolean;
@@ -0,0 +1,4 @@
1
+ import type { AnnounceMessage, AnnounceOptions, AnnouncerPoliteness } from './types';
2
+ export declare function announce(message: AnnounceMessage, politeness?: AnnouncerPoliteness, timeoutOrOptions?: number | AnnounceOptions): void;
3
+ export declare function clearAnnouncer(politeness?: AnnouncerPoliteness, doc?: Document): void;
4
+ export declare function destroyAnnouncer(doc?: Document): void;
@@ -0,0 +1,3 @@
1
+ export type { AnnounceMessage, AnnounceOptions, AnnouncerPoliteness, } from './types';
2
+ export { announce, clearAnnouncer, destroyAnnouncer } from './announce';
3
+ export { LIVEREGION_TIMEOUT_DELAY } from './LiveAnnouncer';
@@ -0,0 +1,8 @@
1
+ export type AnnouncerPoliteness = 'assertive' | 'polite';
2
+ export type AnnounceMessage = string | {
3
+ 'aria-labelledby': string;
4
+ };
5
+ export type AnnounceOptions = {
6
+ timeout?: number;
7
+ document?: Document;
8
+ };
@@ -0,0 +1,3 @@
1
+ export declare const LIVE_ANNOUNCER_ATTR = "data-live-announcer";
2
+ export declare const visuallyHiddenStyleProperties: Partial<CSSStyleDeclaration>;
3
+ export declare const applyVisuallyHiddenStyles: (el: HTMLElement) => void;