@ringcentral/spring-base 1.11.2 → 1.13.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.
- package/config.js +32 -12
- package/contexts.js +42 -24
- package/hooks.js +1688 -615
- package/index.js +15 -16
- package/liveAnnouncer.js +303 -0
- package/package.json +5 -5
- package/src/hooks/index.d.ts +1 -0
- package/src/hooks/useAnnouncer/useAnnouncer.d.ts +5 -2
- package/src/hooks/useButton/useButton.d.ts +1 -1
- package/src/hooks/useGlobalAnnouncer/index.d.ts +1 -0
- package/src/hooks/useGlobalAnnouncer/useGlobalAnnouncer.d.ts +5 -0
- package/src/hooks/useId/useId.d.ts +3 -1
- package/src/hooks/useKeyboardMoveFocus/useOnlyOneFocusable.d.ts +4 -1
- package/src/index.d.ts +1 -0
- package/src/liveAnnouncer/LiveAnnouncer.d.ts +25 -0
- package/src/liveAnnouncer/announce.d.ts +4 -0
- package/src/liveAnnouncer/index.d.ts +3 -0
- package/src/liveAnnouncer/types.d.ts +8 -0
- package/src/liveAnnouncer/visuallyHidden.d.ts +3 -0
- package/typings.js +7 -0
- package/utils.js +835 -81
- package/chunks/twMerge.js +0 -487
package/utils.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import './config.js';
|
|
9
|
-
import 'tailwind-merge';
|
|
1
|
+
import { unstable_ownerDocument, unstable_ownerWindow } from "@mui/utils";
|
|
2
|
+
import react, { isValidElement, useCallback } from "react";
|
|
3
|
+
import isPlainObject from "lodash/isPlainObject";
|
|
4
|
+
import omit from "lodash/omit";
|
|
5
|
+
import pick from "lodash/pick";
|
|
6
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
7
|
+
import { rcConfiguration } from "./config.js";
|
|
10
8
|
|
|
11
9
|
const a11yKeyboard = {
|
|
12
10
|
ArrowLeft: 'ArrowLeft',
|
|
@@ -14,9 +12,402 @@ const a11yKeyboard = {
|
|
|
14
12
|
Space: ' ',
|
|
15
13
|
Escape: 'Escape',
|
|
16
14
|
Enter: 'Enter',
|
|
17
|
-
Tab: 'Tab'
|
|
15
|
+
Tab: 'Tab'
|
|
18
16
|
};
|
|
19
17
|
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const ownerWindow = unstable_ownerWindow;
|
|
21
|
+
|
|
22
|
+
// Based on React Aria's ariaHideOutside implementation:
|
|
23
|
+
// https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/ariaHideOutside.ts
|
|
24
|
+
// Extended for Spring UI's multi-window support, native inert, and dynamic targets.
|
|
25
|
+
|
|
26
|
+
const ARIA_HIDDEN = 'aria-hidden';
|
|
27
|
+
const TOP_LAYER_ATTR = 'data-sui-top-layer';
|
|
28
|
+
const alwaysVisibleSelector = `[data-live-announcer="true"], [${TOP_LAYER_ATTR}]`;
|
|
29
|
+
// Preserve the existing numeric aria-hidden map for interoperability with
|
|
30
|
+
// Spring UI bundles that predate native inert support.
|
|
31
|
+
const ARIA_HIDDEN_REF_COUNT_MAP = '__SUI_ARIA_HIDE_OUTSIDE_REF_COUNT_MAP__';
|
|
32
|
+
const INERT_REF_COUNT_MAP = '__SUI_ARIA_HIDE_OUTSIDE_INERT_REF_COUNT_MAP__';
|
|
33
|
+
const getRefCountMap = (window, mechanism)=>{
|
|
34
|
+
const key = mechanism === 'inert' ? INERT_REF_COUNT_MAP : ARIA_HIDDEN_REF_COUNT_MAP;
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
if (!window[key]) {
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
window[key] = new WeakMap();
|
|
39
|
+
}
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
return window[key];
|
|
42
|
+
};
|
|
43
|
+
const OBSERVER_STACK = '__SUI_ARIA_HIDE_OUTSIDE_OBSERVER_STACK__';
|
|
44
|
+
const getObserverStack = (window)=>{
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
if (!window[OBSERVER_STACK]) {
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
window[OBSERVER_STACK] = [];
|
|
49
|
+
}
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
return window[OBSERVER_STACK];
|
|
52
|
+
};
|
|
53
|
+
const isElement = (node)=>{
|
|
54
|
+
return typeof node === 'object' && node != null && 'nodeType' in node && node.nodeType === 1;
|
|
55
|
+
};
|
|
56
|
+
const isAlwaysVisibleNode = (node)=>{
|
|
57
|
+
return node.getAttribute('data-live-announcer') === 'true' || node.hasAttribute(TOP_LAYER_ATTR);
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Makes DOM content outside the given targets unavailable. Targets can be
|
|
61
|
+
* updated without rebuilding the observer or restoring unrelated nodes.
|
|
62
|
+
* @param targets - The elements that should remain available.
|
|
63
|
+
* @param options - Root and native inert configuration.
|
|
64
|
+
* @returns - An incremental target controller.
|
|
65
|
+
*/ const createAriaHideOutsideController = (targets, options = {})=>{
|
|
66
|
+
const isLegacyRoot = isElement(options);
|
|
67
|
+
const configuredRoot = isLegacyRoot ? options : options.root;
|
|
68
|
+
const currentWindow = ownerWindow(configuredRoot ?? targets[0]);
|
|
69
|
+
if (!currentWindow) {
|
|
70
|
+
// Current JS environment doesn't have a window object, do nothing.
|
|
71
|
+
return {
|
|
72
|
+
addTarget: ()=>{},
|
|
73
|
+
removeTarget: ()=>{},
|
|
74
|
+
dispose: ()=>{}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const root = configuredRoot ?? currentWindow.document.body;
|
|
78
|
+
const shouldUseInert = !isLegacyRoot && options.shouldUseInert && 'inert' in currentWindow.HTMLElement.prototype;
|
|
79
|
+
const visibleNodes = new Set(targets);
|
|
80
|
+
const targetNodes = new Set(targets);
|
|
81
|
+
const automaticVisibleNodes = new Set();
|
|
82
|
+
const hiddenNodes = new Set();
|
|
83
|
+
// A target may detach before it is unregistered. Retain the ancestor path
|
|
84
|
+
// restored during registration so surviving ancestors can be hidden again.
|
|
85
|
+
const targetPaths = new Map();
|
|
86
|
+
const getTargetPath = (target)=>{
|
|
87
|
+
const path = [];
|
|
88
|
+
let node = target;
|
|
89
|
+
while(node && root.contains(node)){
|
|
90
|
+
path.push(node);
|
|
91
|
+
if (node === root) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
node = node.parentElement;
|
|
95
|
+
}
|
|
96
|
+
return path;
|
|
97
|
+
};
|
|
98
|
+
for (const target of targets){
|
|
99
|
+
targetPaths.set(target, getTargetPath(target));
|
|
100
|
+
}
|
|
101
|
+
const observerStack = getObserverStack(currentWindow);
|
|
102
|
+
const getHideMechanism = (node)=>shouldUseInert && node instanceof currentWindow.HTMLElement ? 'inert' : 'aria-hidden';
|
|
103
|
+
const getHidden = (node, mechanism)=>mechanism === 'inert' ? node.inert : node.getAttribute(ARIA_HIDDEN) === 'true';
|
|
104
|
+
const setHidden = (node, mechanism, hidden)=>{
|
|
105
|
+
if (mechanism === 'inert') {
|
|
106
|
+
node.inert = hidden;
|
|
107
|
+
} else if (hidden) {
|
|
108
|
+
node.setAttribute(ARIA_HIDDEN, 'true');
|
|
109
|
+
} else {
|
|
110
|
+
node.removeAttribute(ARIA_HIDDEN);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const restoreHiddenNode = (node)=>{
|
|
114
|
+
if (!hiddenNodes.delete(node)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const mechanism = getHideMechanism(node);
|
|
118
|
+
const refCountMap = getRefCountMap(currentWindow, mechanism);
|
|
119
|
+
const count = refCountMap.get(node);
|
|
120
|
+
if (typeof count !== 'number') {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (count === 1) {
|
|
124
|
+
setHidden(node, mechanism, false);
|
|
125
|
+
refCountMap.delete(node);
|
|
126
|
+
} else {
|
|
127
|
+
refCountMap.set(node, count - 1);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const hide = (node)=>{
|
|
131
|
+
// This controller already owns a hide for the node. Counting it twice would
|
|
132
|
+
// leave the node unavailable after cleanup.
|
|
133
|
+
if (hiddenNodes.has(node)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const mechanism = getHideMechanism(node);
|
|
137
|
+
const refCountMap = getRefCountMap(currentWindow, mechanism);
|
|
138
|
+
const refCount = refCountMap.get(node) ?? 0;
|
|
139
|
+
// If the node was already unavailable through this mechanism, and Spring
|
|
140
|
+
// does not own a matching hide, leave the existing state untouched.
|
|
141
|
+
if (getHidden(node, mechanism) && refCount === 0) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (refCount === 0) {
|
|
145
|
+
setHidden(node, mechanism, true);
|
|
146
|
+
}
|
|
147
|
+
hiddenNodes.add(node);
|
|
148
|
+
refCountMap.set(node, refCount + 1);
|
|
149
|
+
};
|
|
150
|
+
const addAlwaysVisibleNode = (node)=>{
|
|
151
|
+
automaticVisibleNodes.add(node);
|
|
152
|
+
visibleNodes.add(node);
|
|
153
|
+
};
|
|
154
|
+
const addAlwaysVisibleNodes = (node)=>{
|
|
155
|
+
if (isAlwaysVisibleNode(node)) {
|
|
156
|
+
addAlwaysVisibleNode(node);
|
|
157
|
+
}
|
|
158
|
+
for (const alwaysVisibleNode of node.querySelectorAll(alwaysVisibleSelector)){
|
|
159
|
+
addAlwaysVisibleNode(alwaysVisibleNode);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const removeDisconnectedAlwaysVisibleNodes = (node)=>{
|
|
163
|
+
for (const alwaysVisibleNode of automaticVisibleNodes){
|
|
164
|
+
if (!node.contains(alwaysVisibleNode) || root.contains(alwaysVisibleNode)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
automaticVisibleNodes.delete(alwaysVisibleNode);
|
|
168
|
+
if (!targetNodes.has(alwaysVisibleNode)) {
|
|
169
|
+
visibleNodes.delete(alwaysVisibleNode);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const containsNode = (nodes, node)=>{
|
|
174
|
+
for (const candidate of nodes){
|
|
175
|
+
if (candidate.contains(node)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
const containsVisibleNode = (node, nodes)=>{
|
|
182
|
+
for (const visibleNode of nodes){
|
|
183
|
+
if (node.contains(visibleNode)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
189
|
+
const isCoveredByHiddenNode = (node)=>{
|
|
190
|
+
let ancestor = node.parentElement;
|
|
191
|
+
while(ancestor && root.contains(ancestor)){
|
|
192
|
+
if (hiddenNodes.has(ancestor)) {
|
|
193
|
+
return !(ancestor === node.parentElement && ancestor.getAttribute('role') === 'row');
|
|
194
|
+
}
|
|
195
|
+
ancestor = ancestor.parentElement;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
};
|
|
199
|
+
const shouldAdoptHiddenNode = (node)=>{
|
|
200
|
+
return root.contains(node) && !hiddenNodes.has(node) && !containsNode(visibleNodes, node) && !containsVisibleNode(node, visibleNodes) && !isCoveredByHiddenNode(node);
|
|
201
|
+
};
|
|
202
|
+
const acceptNode = (node)=>{
|
|
203
|
+
// Skip this node and its children if it must remain available. Also skip
|
|
204
|
+
// children of already unavailable nodes, as both aria-hidden and inert are
|
|
205
|
+
// recursive. An exception is made for role="row" because VoiceOver on iOS
|
|
206
|
+
// requires its cells to be hidden individually.
|
|
207
|
+
if (visibleNodes.has(node) || node.parentElement && hiddenNodes.has(node.parentElement) && node.parentElement.getAttribute('role') !== 'row') {
|
|
208
|
+
return currentWindow.NodeFilter.FILTER_REJECT;
|
|
209
|
+
}
|
|
210
|
+
// Keep the complete ancestor path for overlay targets and explicitly marked
|
|
211
|
+
// top layers so neither mechanism makes a visible node unavailable through
|
|
212
|
+
// one of its ancestors.
|
|
213
|
+
if (containsVisibleNode(node, visibleNodes)) {
|
|
214
|
+
return currentWindow.NodeFilter.FILTER_SKIP;
|
|
215
|
+
}
|
|
216
|
+
return currentWindow.NodeFilter.FILTER_ACCEPT;
|
|
217
|
+
};
|
|
218
|
+
// `walkRoot` must not sit inside a visible node's subtree. Descendants of a
|
|
219
|
+
// target are kept available by the tree walker rejecting the target itself,
|
|
220
|
+
// which only happens when the walk starts at or above it. Every caller
|
|
221
|
+
// guarantees this through `containsNode(visibleNodes, ...)`.
|
|
222
|
+
const walk = (walkRoot, includeRoot)=>{
|
|
223
|
+
// A body-sibling portal can contain its marker below the node that is added
|
|
224
|
+
// to the document. Discover the whole subtree before considering its root.
|
|
225
|
+
addAlwaysVisibleNodes(walkRoot);
|
|
226
|
+
const rootFilter = acceptNode(walkRoot);
|
|
227
|
+
if (includeRoot && rootFilter === currentWindow.NodeFilter.FILTER_ACCEPT) {
|
|
228
|
+
hide(walkRoot);
|
|
229
|
+
}
|
|
230
|
+
if (rootFilter === currentWindow.NodeFilter.FILTER_REJECT) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const walker = currentWindow.document.createTreeWalker(walkRoot, currentWindow.NodeFilter.SHOW_ELEMENT, {
|
|
234
|
+
acceptNode
|
|
235
|
+
});
|
|
236
|
+
let node = walker.nextNode();
|
|
237
|
+
while(node != null){
|
|
238
|
+
hide(node);
|
|
239
|
+
node = walker.nextNode();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const addTarget = (target)=>{
|
|
243
|
+
if (!root.contains(target)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
targetNodes.add(target);
|
|
247
|
+
visibleNodes.add(target);
|
|
248
|
+
const path = getTargetPath(target);
|
|
249
|
+
const restoredAncestorIndexes = [];
|
|
250
|
+
for(let index = 0; index < path.length; index++){
|
|
251
|
+
// Only ancestors that were actually restored can expose previously covered
|
|
252
|
+
// siblings. Skip the target itself (index 0) and nodes that were never hidden.
|
|
253
|
+
if (index > 0 && hiddenNodes.has(path[index])) {
|
|
254
|
+
restoredAncestorIndexes.push(index);
|
|
255
|
+
}
|
|
256
|
+
restoreHiddenNode(path[index]);
|
|
257
|
+
}
|
|
258
|
+
targetPaths.set(target, path);
|
|
259
|
+
for (const index of restoredAncestorIndexes){
|
|
260
|
+
const ancestor = path[index];
|
|
261
|
+
const pathChild = path[index - 1];
|
|
262
|
+
for (const child of ancestor.children){
|
|
263
|
+
if (child !== pathChild) {
|
|
264
|
+
walk(child, true);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const removeTarget = (target)=>{
|
|
270
|
+
if (!targetNodes.delete(target)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (!automaticVisibleNodes.has(target)) {
|
|
274
|
+
visibleNodes.delete(target);
|
|
275
|
+
}
|
|
276
|
+
const targetPath = targetPaths.get(target);
|
|
277
|
+
targetPaths.delete(target);
|
|
278
|
+
// The target may already be detached. Resume hiding from the first
|
|
279
|
+
// surviving ancestor restored during registration.
|
|
280
|
+
const connectedAncestor = targetPath?.find((node)=>root.contains(node));
|
|
281
|
+
if (!connectedAncestor) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// A branch can live inside another target's subtree, e.g. a submenu
|
|
285
|
+
// portaled into its parent menu. That subtree stays available through the
|
|
286
|
+
// enclosing target, so hiding from here would make the enclosing overlay
|
|
287
|
+
// unavailable as well.
|
|
288
|
+
if (containsNode(visibleNodes, connectedAncestor)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
let walkRoot = connectedAncestor;
|
|
292
|
+
while(walkRoot.parentElement && walkRoot.parentElement !== root && !containsVisibleNode(walkRoot.parentElement, visibleNodes)){
|
|
293
|
+
walkRoot = walkRoot.parentElement;
|
|
294
|
+
}
|
|
295
|
+
walk(walkRoot, true);
|
|
296
|
+
};
|
|
297
|
+
// If there is already a MutationObserver listening from a previous call,
|
|
298
|
+
// disconnect it so the new one takes over.
|
|
299
|
+
if (observerStack.length) {
|
|
300
|
+
observerStack[observerStack.length - 1].disconnect();
|
|
301
|
+
}
|
|
302
|
+
walk(root, false);
|
|
303
|
+
const processChanges = (changes)=>{
|
|
304
|
+
for (const change of changes){
|
|
305
|
+
if (change.type !== 'childList') {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
for (const node of change.removedNodes){
|
|
309
|
+
if (isElement(node)) {
|
|
310
|
+
removeDisconnectedAlwaysVisibleNodes(node);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (!change.target.isConnected || change.addedNodes.length === 0) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
// If the parent element of the added nodes is not within one of the targets,
|
|
317
|
+
// and not already inside a hidden node, hide all of the new children.
|
|
318
|
+
if (!containsNode(visibleNodes, change.target) && !containsNode(hiddenNodes, change.target)) {
|
|
319
|
+
for (const node of change.addedNodes){
|
|
320
|
+
if (isElement(node) && root.contains(node)) {
|
|
321
|
+
walk(node, true);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
const observer = new currentWindow.MutationObserver(processChanges);
|
|
328
|
+
observer.observe(root, {
|
|
329
|
+
childList: true,
|
|
330
|
+
subtree: true
|
|
331
|
+
});
|
|
332
|
+
const observerWrapper = {
|
|
333
|
+
observe () {
|
|
334
|
+
observer.observe(root, {
|
|
335
|
+
childList: true,
|
|
336
|
+
subtree: true
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
resume (releasedHiddenNodes, releasedAlwaysVisibleNodes) {
|
|
340
|
+
for (const node of releasedAlwaysVisibleNodes){
|
|
341
|
+
if (root.contains(node)) {
|
|
342
|
+
addAlwaysVisibleNode(node);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
for (const node of releasedHiddenNodes){
|
|
346
|
+
if (shouldAdoptHiddenNode(node)) {
|
|
347
|
+
hide(node);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
observerWrapper.observe();
|
|
351
|
+
},
|
|
352
|
+
disconnect () {
|
|
353
|
+
observer.disconnect();
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
observerStack.push(observerWrapper);
|
|
357
|
+
let isCleanedUp = false;
|
|
358
|
+
const dispose = ()=>{
|
|
359
|
+
if (isCleanedUp) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
isCleanedUp = true;
|
|
363
|
+
const pendingChanges = observer.takeRecords();
|
|
364
|
+
observer.disconnect();
|
|
365
|
+
processChanges(pendingChanges);
|
|
366
|
+
// Hand ownership of nodes hidden while the previous observer was paused
|
|
367
|
+
// back before releasing this controller's references.
|
|
368
|
+
if (observerWrapper === observerStack[observerStack.length - 1]) {
|
|
369
|
+
observerStack.pop();
|
|
370
|
+
const previousObserver = observerStack[observerStack.length - 1];
|
|
371
|
+
if (previousObserver?.resume) {
|
|
372
|
+
previousObserver.resume(hiddenNodes, automaticVisibleNodes);
|
|
373
|
+
} else {
|
|
374
|
+
previousObserver?.observe();
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
observerStack.splice(observerStack.indexOf(observerWrapper), 1);
|
|
378
|
+
}
|
|
379
|
+
for (const node of [
|
|
380
|
+
...hiddenNodes
|
|
381
|
+
]){
|
|
382
|
+
restoreHiddenNode(node);
|
|
383
|
+
}
|
|
384
|
+
targetPaths.clear();
|
|
385
|
+
targetNodes.clear();
|
|
386
|
+
automaticVisibleNodes.clear();
|
|
387
|
+
};
|
|
388
|
+
return {
|
|
389
|
+
addTarget: (target)=>{
|
|
390
|
+
if (!isCleanedUp) {
|
|
391
|
+
addTarget(target);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
removeTarget: (target)=>{
|
|
395
|
+
if (!isCleanedUp) {
|
|
396
|
+
removeTarget(target);
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
dispose
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
/**
|
|
403
|
+
* Makes DOM content outside the given targets unavailable and returns a
|
|
404
|
+
* function that restores those changes.
|
|
405
|
+
*/ const ariaHideOutside = (targets, options = {})=>createAriaHideOutsideController(targets, options).dispose;
|
|
406
|
+
|
|
407
|
+
// It should to be noted that this function isn't equivalent to `text-transform: capitalize`.
|
|
408
|
+
//
|
|
409
|
+
// A strict capitalization should uppercase the first letter of each word in the sentence.
|
|
410
|
+
// We only handle the first word.
|
|
20
411
|
function capitalize(string) {
|
|
21
412
|
if (typeof string !== 'string') {
|
|
22
413
|
throw new Error('SPRING: `capitalize(string)` expects a string argument.');
|
|
@@ -24,20 +415,20 @@ function capitalize(string) {
|
|
|
24
415
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
25
416
|
}
|
|
26
417
|
|
|
27
|
-
const checkDefaultPrevented = (fc)
|
|
28
|
-
return (e, ...args)
|
|
29
|
-
if (e.defaultPrevented)
|
|
30
|
-
return;
|
|
418
|
+
/** check is that event preventDefault */ const checkDefaultPrevented = (fc)=>{
|
|
419
|
+
return (e, ...args)=>{
|
|
420
|
+
if (e.defaultPrevented) return;
|
|
31
421
|
fc(e, ...args);
|
|
32
422
|
};
|
|
33
423
|
};
|
|
34
424
|
|
|
425
|
+
|
|
426
|
+
|
|
35
427
|
function checkElementName(element, names) {
|
|
36
|
-
return
|
|
37
|
-
names.indexOf(element.type?.displayName) !== -1);
|
|
428
|
+
return isValidElement(element) && names.indexOf(element.type?.displayName) !== -1;
|
|
38
429
|
}
|
|
39
430
|
|
|
40
|
-
const isDeleteKeyboardEvent = (keyboardEvent)
|
|
431
|
+
const isDeleteKeyboardEvent = (keyboardEvent)=>{
|
|
41
432
|
return keyboardEvent.key === 'Backspace' || keyboardEvent.key === 'Delete';
|
|
42
433
|
};
|
|
43
434
|
|
|
@@ -52,115 +443,378 @@ const globalStateClasses = {
|
|
|
52
443
|
focusVisibleWithin: 'sui-focus-visible-within',
|
|
53
444
|
required: 'sui-required',
|
|
54
445
|
selected: 'sui-selected',
|
|
55
|
-
dragged: 'sui-dragged'
|
|
446
|
+
dragged: 'sui-dragged'
|
|
56
447
|
};
|
|
57
448
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
449
|
+
|
|
450
|
+
/* oxlint-disable sonarjs/cognitive-complexity */
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* simple camel case for better performance, because this is a very open execution function, below is the benchmark of lodash camelCase vs simpleCamelCase and regex camelCase.
|
|
454
|
+
*
|
|
455
|
+
* https://www.measurethat.net/Benchmarks/Show/31672/0/regex-vs-javascript-camelcase-long
|
|
456
|
+
*
|
|
457
|
+
* https://www.measurethat.net/Benchmarks/Show/12906/0/lodash-camelcase-vs-javascript-camelcase-long
|
|
458
|
+
*/ const simpleCamelCase = (str)=>str.split('-').map((item, index)=>index ? item.charAt(0).toUpperCase() + item.slice(1).toLowerCase() : item).join('');
|
|
62
459
|
const DEFAULT_PREFIX = 'sui-';
|
|
63
|
-
|
|
64
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Custom hook for generating CSS classes based on input parameters.
|
|
462
|
+
*
|
|
463
|
+
* pick class from object with prefix of object like `sui-${prefix}-${key}`
|
|
464
|
+
*
|
|
465
|
+
* `className` will be apply to the `cls('root')` class.
|
|
466
|
+
*
|
|
467
|
+
* camelCase will be used for the key in the inputClasses object.
|
|
468
|
+
*
|
|
469
|
+
* and apply to kebabCase in the class name.
|
|
470
|
+
*
|
|
471
|
+
* ```tsx
|
|
472
|
+
* type BaseButtonClasses =
|
|
473
|
+
* | 'root'
|
|
474
|
+
* | 'content'
|
|
475
|
+
* | 'start-slot';
|
|
476
|
+
*
|
|
477
|
+
* const cls = useClasses<BaseButtonClasses>('button', {
|
|
478
|
+
* root: 'custom-button-root',
|
|
479
|
+
* content: 'custom-button-content',
|
|
480
|
+
* startSlot: 'custom-button-start-slot',
|
|
481
|
+
* }, 'custom-root-class');
|
|
482
|
+
*
|
|
483
|
+
* <button className={cls('root')}>Button</button> // 'sui-button-root custom-button-root custom-root-class'
|
|
484
|
+
* <button className={cls('content')}>Button</button> // 'sui-button-content custom-button-content'
|
|
485
|
+
*
|
|
486
|
+
* <button className={cls('start-slot')}>Button</button> // 'sui-button-start-slot custom-button-start-slot'
|
|
487
|
+
* ```
|
|
488
|
+
*/ const useClasses = (prefix, inputClasses, className)=>{
|
|
489
|
+
return useCallback((...classValues)=>{
|
|
65
490
|
const classList = [];
|
|
66
491
|
const selectedMap = new Map();
|
|
67
492
|
let isRoot = false;
|
|
68
493
|
const hasCustomClasses = inputClasses && Object.keys(inputClasses).length > 0;
|
|
69
|
-
const applyClass = (classValue)
|
|
494
|
+
const applyClass = (classValue)=>{
|
|
70
495
|
const isDefaultClasses = globalStateClasses[classValue];
|
|
71
496
|
if (isDefaultClasses) {
|
|
72
497
|
classList.push(isDefaultClasses);
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
498
|
+
} else {
|
|
75
499
|
classList.push(`${DEFAULT_PREFIX}${prefix}-${classValue}`);
|
|
76
500
|
}
|
|
77
501
|
if (hasCustomClasses) {
|
|
502
|
+
// add selected class to map for use in inputClasses
|
|
78
503
|
selectedMap.set(simpleCamelCase(classValue), 1);
|
|
79
504
|
}
|
|
80
505
|
};
|
|
81
|
-
for
|
|
506
|
+
for(let i = 0; i < classValues.length; i++){
|
|
82
507
|
const classValue = classValues[i];
|
|
83
|
-
if (!classValue)
|
|
84
|
-
continue;
|
|
508
|
+
if (!classValue) continue;
|
|
85
509
|
if (typeof classValue === 'string') {
|
|
86
510
|
if (classValue === 'root') {
|
|
87
511
|
isRoot = true;
|
|
88
512
|
classList.push(`${DEFAULT_PREFIX}${prefix}`);
|
|
89
513
|
}
|
|
90
514
|
applyClass(classValue);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
for (const key in classValue) {
|
|
515
|
+
} else {
|
|
516
|
+
for(const key in classValue){
|
|
94
517
|
if (classValue[key]) {
|
|
95
518
|
applyClass(key);
|
|
96
519
|
}
|
|
97
520
|
}
|
|
98
521
|
}
|
|
99
522
|
}
|
|
100
|
-
selectedMap.forEach((_, key)
|
|
523
|
+
selectedMap.forEach((_, key)=>{
|
|
101
524
|
const inputClassValue = inputClasses?.[key];
|
|
102
525
|
if (inputClassValue) {
|
|
103
526
|
classList.push(inputClassValue);
|
|
104
527
|
}
|
|
105
528
|
});
|
|
106
529
|
if (isRoot && className) {
|
|
530
|
+
// add root class and className
|
|
107
531
|
classList.push(className);
|
|
108
532
|
}
|
|
109
533
|
return classList.join(' ');
|
|
110
|
-
}, [
|
|
534
|
+
}, [
|
|
535
|
+
className,
|
|
536
|
+
inputClasses,
|
|
537
|
+
prefix
|
|
538
|
+
]);
|
|
111
539
|
};
|
|
112
540
|
|
|
113
|
-
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Combine props object together
|
|
550
|
+
*
|
|
551
|
+
* that will group className, classes together
|
|
552
|
+
* function will auto combine together,
|
|
553
|
+
* trigger order is `intoProps.someFn(e) => defaultProps.someFn(e)`
|
|
554
|
+
*
|
|
555
|
+
* @param baseProps base props you want to combine together
|
|
556
|
+
* @param newProps want to combine in to props
|
|
557
|
+
* @param coverPropKeys some prop key you want to cover, not combine when type is function, ex. `ComponentType`
|
|
558
|
+
*
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```tsx
|
|
562
|
+
* combineProps(
|
|
563
|
+
{
|
|
564
|
+
children: <A />,
|
|
565
|
+
value: '1',
|
|
566
|
+
className: 'a1',
|
|
567
|
+
classes: {
|
|
568
|
+
root: 'root1'
|
|
569
|
+
},
|
|
570
|
+
count: 1,
|
|
571
|
+
size: 'large',
|
|
572
|
+
onClick: fn1
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
children: <B />,
|
|
576
|
+
value: '2',
|
|
577
|
+
className: 'a2',
|
|
578
|
+
classes: {
|
|
579
|
+
root: 'root2'
|
|
580
|
+
},
|
|
581
|
+
count: 2,
|
|
582
|
+
size: undefined,
|
|
583
|
+
onClick: fn2
|
|
584
|
+
},
|
|
585
|
+
)
|
|
586
|
+
// output result:
|
|
587
|
+
{
|
|
588
|
+
children: <B />,
|
|
589
|
+
value: '2',
|
|
590
|
+
className: 'a1 a2',
|
|
591
|
+
classes: {
|
|
592
|
+
root: 'root1 root2'
|
|
593
|
+
},
|
|
594
|
+
count: 2,
|
|
595
|
+
size: 'large',
|
|
596
|
+
onClick: (e) => {
|
|
597
|
+
fn2(e);
|
|
598
|
+
fn1(e);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
* ```
|
|
602
|
+
*/ const combineProps = (baseProps, newProps, coverPropKeys)=>innerCombineProps({
|
|
603
|
+
baseProps,
|
|
604
|
+
newProps,
|
|
605
|
+
coverPropKeys
|
|
606
|
+
});
|
|
607
|
+
/**
|
|
608
|
+
* Combine two classes
|
|
609
|
+
*
|
|
610
|
+
* @param baseClasses base classes you want to combine together
|
|
611
|
+
* @param newClasses new classes want to combine in to props
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```ts
|
|
615
|
+
* combineClasses(
|
|
616
|
+
{
|
|
617
|
+
root: 'a1',
|
|
618
|
+
input: 'a1',
|
|
619
|
+
un1: undefined,
|
|
620
|
+
un2: 'a4',
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
root: 'a1',
|
|
624
|
+
input: 'a2',
|
|
625
|
+
un1: 'a3',
|
|
626
|
+
un2: undefined,
|
|
627
|
+
un3: 'a5',
|
|
628
|
+
},
|
|
629
|
+
)
|
|
630
|
+
// output result:
|
|
631
|
+
{
|
|
632
|
+
root: 'a1 a1',
|
|
633
|
+
input: 'a1 a2',
|
|
634
|
+
un1: 'a3',
|
|
635
|
+
un2: 'a4',
|
|
636
|
+
un3: 'a5',
|
|
637
|
+
}
|
|
638
|
+
* ```
|
|
639
|
+
*/ const combineClasses = (baseClasses, newClasses)=>innerCombineProps({
|
|
640
|
+
baseProps: baseClasses,
|
|
641
|
+
newProps: newClasses,
|
|
642
|
+
combineAllString: true
|
|
643
|
+
});
|
|
644
|
+
const innerCombineProps = (params)=>{
|
|
645
|
+
const { baseProps, newProps } = params;
|
|
646
|
+
if (!newProps || Object.keys(newProps).length === 0) {
|
|
647
|
+
return baseProps;
|
|
648
|
+
}
|
|
649
|
+
const outputProps = mergeChildProps(params);
|
|
650
|
+
return {
|
|
651
|
+
...newProps,
|
|
652
|
+
...outputProps
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
function mergeChildProps({ baseProps, newProps, combineAllString, coverPropKeys }) {
|
|
656
|
+
const result = Object.keys(baseProps || {}).reduce((output, curr)=>{
|
|
657
|
+
const prop = baseProps[curr];
|
|
658
|
+
const propTo = newProps[curr];
|
|
659
|
+
if (propTo !== undefined) {
|
|
660
|
+
// * all typeof type below
|
|
661
|
+
// "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
|
662
|
+
switch(typeof prop){
|
|
663
|
+
case 'undefined':
|
|
664
|
+
output[curr] = propTo;
|
|
665
|
+
break;
|
|
666
|
+
case 'object':
|
|
667
|
+
{
|
|
668
|
+
if (!isValidElement(propTo) && !isValidElement(prop) && isPlainObject(propTo) && isPlainObject(prop)) {
|
|
669
|
+
output[curr] = mergeChildProps({
|
|
670
|
+
baseProps: prop,
|
|
671
|
+
newProps: propTo || {},
|
|
672
|
+
combineAllString: combineAllString || curr === 'classes'
|
|
673
|
+
});
|
|
674
|
+
} else {
|
|
675
|
+
output[curr] = propTo;
|
|
676
|
+
}
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
case 'string':
|
|
680
|
+
if (combineAllString || curr === 'className') {
|
|
681
|
+
output[curr] = [
|
|
682
|
+
prop,
|
|
683
|
+
propTo
|
|
684
|
+
].filter((x)=>typeof x === 'string' && x).join(' ');
|
|
685
|
+
} else {
|
|
686
|
+
output[curr] = propTo;
|
|
687
|
+
}
|
|
688
|
+
break;
|
|
689
|
+
case 'boolean':
|
|
690
|
+
case 'symbol':
|
|
691
|
+
case 'bigint':
|
|
692
|
+
case 'number':
|
|
693
|
+
output[curr] = propTo;
|
|
694
|
+
break;
|
|
695
|
+
case 'function':
|
|
696
|
+
if (coverPropKeys && coverPropKeys.includes(curr) && propTo) {
|
|
697
|
+
output[curr] = propTo;
|
|
698
|
+
} else {
|
|
699
|
+
output[curr] = (...args)=>{
|
|
700
|
+
// * exec outside first
|
|
701
|
+
propTo?.(...args);
|
|
702
|
+
prop?.(...args);
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
break;
|
|
706
|
+
default:
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
output[curr] = prop;
|
|
710
|
+
}
|
|
711
|
+
return output;
|
|
712
|
+
}, {});
|
|
713
|
+
// if result is an empty {} reset that to undefined
|
|
714
|
+
if (Object.keys(result).length === 0) {
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
...newProps,
|
|
719
|
+
...result
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* oxlint-disable no-console */
|
|
724
|
+
function logInDev({ scope, message, level = 'warn', condition = true }) {
|
|
725
|
+
if (process.env.NODE_ENV !== 'production' && condition && !rcConfiguration.WARNING_IGNORE) {
|
|
726
|
+
const log = `SPRING [${scope}]: ${message}`;
|
|
727
|
+
if (process.env.NODE_ENV === 'test' && level === 'error') {
|
|
728
|
+
throw Error(log);
|
|
729
|
+
}
|
|
730
|
+
console[level](log);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const isRef = (obj)=>{
|
|
735
|
+
return obj && 'current' in obj;
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* get element from React ref or callback
|
|
741
|
+
*/ const getRefElement = (ref)=>{
|
|
742
|
+
if (typeof ref === 'function') {
|
|
743
|
+
return ref();
|
|
744
|
+
}
|
|
745
|
+
if (typeof ref === 'object' && isRef(ref)) {
|
|
746
|
+
return ref.current;
|
|
747
|
+
}
|
|
748
|
+
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
|
|
749
|
+
return ref;
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
// oxlint-disable-next-line @typescript-eslint/no-explicit-any
|
|
753
|
+
const getTitle = (text)=>{
|
|
114
754
|
return typeof text === 'string' ? text : undefined;
|
|
115
755
|
};
|
|
116
756
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
const isSuiElement = (element, suiNames)=>{
|
|
760
|
+
return react.isValidElement(element) && suiNames.indexOf(element.type.displayName) !== -1;
|
|
120
761
|
};
|
|
121
762
|
|
|
122
763
|
function isUrl(url) {
|
|
764
|
+
// oxlint-disable-next-line no-useless-escape
|
|
123
765
|
const exp = /(\b(https?):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi;
|
|
124
766
|
return url.match(exp);
|
|
125
767
|
}
|
|
126
768
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
769
|
+
// * this whole file should be remove after webkit154 issue be fixed
|
|
770
|
+
/**
|
|
771
|
+
* Conditionally apply a workaround for the CSS transition bug in Safari 15.4 / WebKit browsers.
|
|
772
|
+
* Remove this workaround once the WebKit bug fix is released.
|
|
773
|
+
*
|
|
774
|
+
* follow up:
|
|
775
|
+
* https://github.com/mui/material-ui/pull/31975/files
|
|
776
|
+
* https://github.com/mui/material-ui/pull/32202/files
|
|
777
|
+
*/ const isWebKit154 = typeof navigator !== 'undefined' && /^((?!chrome|android).)*(safari|mobile)/i.test(navigator.userAgent) && /(os |version\/)15(.|_)[4-9]/i.test(navigator.userAgent);
|
|
778
|
+
|
|
130
779
|
|
|
131
|
-
|
|
132
|
-
|
|
780
|
+
|
|
781
|
+
/** enhance lodash type */ const omit_omit = (classes, keys)=>{
|
|
782
|
+
return omit(classes, keys);
|
|
133
783
|
};
|
|
134
784
|
|
|
135
|
-
|
|
785
|
+
/**
|
|
786
|
+
* get that full height and margin with margin
|
|
787
|
+
* that will ignore transform animation
|
|
788
|
+
*/ function outerSize(el) {
|
|
136
789
|
const style = getComputedStyle(el);
|
|
137
790
|
return {
|
|
138
|
-
get height() {
|
|
139
|
-
return
|
|
140
|
-
parseInt(style.marginTop, 10) +
|
|
141
|
-
parseInt(style.marginBottom, 10));
|
|
142
|
-
},
|
|
143
|
-
get width() {
|
|
144
|
-
return (el.offsetWidth +
|
|
145
|
-
parseInt(style.marginLeft, 10) +
|
|
146
|
-
parseInt(style.marginRight, 10));
|
|
791
|
+
get height () {
|
|
792
|
+
return el.offsetHeight + parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);
|
|
147
793
|
},
|
|
794
|
+
get width () {
|
|
795
|
+
return el.offsetWidth + parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);
|
|
796
|
+
}
|
|
148
797
|
};
|
|
149
798
|
}
|
|
150
799
|
|
|
151
|
-
|
|
152
|
-
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
/** enhance lodash type */ const pick_pick = (classes, keys)=>{
|
|
806
|
+
return pick(classes, keys);
|
|
153
807
|
};
|
|
154
808
|
|
|
155
|
-
function preloadImg(url) {
|
|
156
|
-
return new Promise((resolve, reject)
|
|
809
|
+
/** preload image file */ function preloadImg(url) {
|
|
810
|
+
return new Promise((resolve, reject)=>{
|
|
157
811
|
const img = new Image();
|
|
158
812
|
img.style.display = 'none';
|
|
159
|
-
img.onload = ()
|
|
813
|
+
img.onload = ()=>{
|
|
160
814
|
document.body.removeChild(img);
|
|
161
815
|
resolve();
|
|
162
816
|
};
|
|
163
|
-
img.onerror = ()
|
|
817
|
+
img.onerror = ()=>{
|
|
164
818
|
document.body.removeChild(img);
|
|
165
819
|
reject();
|
|
166
820
|
};
|
|
@@ -169,66 +823,166 @@ function preloadImg(url) {
|
|
|
169
823
|
});
|
|
170
824
|
}
|
|
171
825
|
|
|
172
|
-
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* remove className from element, default will not scan child nodes, if you want to scan child nodes, set `scanChildren` to `true`.
|
|
829
|
+
*
|
|
830
|
+
* @param target target element reference or element or callback
|
|
831
|
+
* @param removeClass className to remove, default will use class `includes` to check, if you want to use full match, set `fullMatch` to `true`
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* ```js
|
|
835
|
+
* elm => <div class="abc bcd efg abc-def"></div>
|
|
836
|
+
*
|
|
837
|
+
* removeClassName(elmRef, 'bcd');
|
|
838
|
+
*
|
|
839
|
+
* elm => <div class="abc efg abc-def"></div>
|
|
840
|
+
*
|
|
841
|
+
* removeClassName(elmRef, 'abc');
|
|
842
|
+
* elm => <div class="efg"></div>
|
|
843
|
+
* ```
|
|
844
|
+
*/ function removeClassName(target, removeClass, { scanChildren = false, fullMatch = false } = {}) {
|
|
173
845
|
const elm = getRefElement(target);
|
|
174
|
-
if (!elm)
|
|
175
|
-
return;
|
|
846
|
+
if (!elm) return;
|
|
176
847
|
if (elm.className.includes(removeClass)) {
|
|
177
848
|
elm.className = cleanClass(elm, removeClass, fullMatch);
|
|
178
849
|
}
|
|
179
850
|
if (scanChildren) {
|
|
180
851
|
const nodes = elm.querySelectorAll(`[class*="${removeClass}"]`);
|
|
181
852
|
if (nodes?.length > 0) {
|
|
182
|
-
nodes.forEach((node)
|
|
853
|
+
nodes.forEach((node)=>node.className = cleanClass(node, removeClass, fullMatch));
|
|
183
854
|
}
|
|
184
855
|
}
|
|
185
856
|
}
|
|
186
857
|
function cleanClass(elm, removeClass, fullMatch) {
|
|
187
|
-
return elm.className
|
|
188
|
-
.trim()
|
|
189
|
-
.split(/\s+/)
|
|
190
|
-
.filter((className) => !(fullMatch
|
|
191
|
-
? className === removeClass
|
|
192
|
-
: className.includes(removeClass)))
|
|
193
|
-
.join(' ');
|
|
858
|
+
return elm.className.trim().split(/\s+/).filter((className)=>!(fullMatch ? className === removeClass : className.includes(removeClass))).join(' ');
|
|
194
859
|
}
|
|
195
860
|
|
|
196
|
-
|
|
861
|
+
/**
|
|
862
|
+
* Executes the corresponding key handler function based on the provided keyboard event.
|
|
863
|
+
* If a handler is found for the pressed key, it will be invoked.
|
|
864
|
+
* Additionally, it prevents any side effects caused by the event, such as page scrolling.
|
|
865
|
+
*
|
|
866
|
+
* @param e - The keyboard event.
|
|
867
|
+
* @param keyHandlers - An object containing key-handler mappings.
|
|
868
|
+
*/ const runKeyHandler = (e, keyHandlers)=>{
|
|
197
869
|
const handler = keyHandlers[e.key];
|
|
198
870
|
if (handler) {
|
|
199
871
|
handler();
|
|
872
|
+
// if event was handled prevent other side effects (e.g. page scroll)
|
|
200
873
|
e.preventDefault();
|
|
201
874
|
}
|
|
202
875
|
};
|
|
203
876
|
|
|
204
|
-
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* get current input element selection position and is that selectRange state
|
|
880
|
+
*/ function getSelectionPosition(sourceTarget) {
|
|
205
881
|
const input = getRefElement(sourceTarget);
|
|
206
882
|
const currentPosition = input?.selectionStart;
|
|
207
883
|
const currentPositionEnd = input?.selectionEnd;
|
|
208
884
|
const isSelectRange = currentPosition !== currentPositionEnd;
|
|
209
885
|
return {
|
|
210
886
|
isSelectRange,
|
|
211
|
-
position: {
|
|
887
|
+
position: {
|
|
888
|
+
start: currentPosition || 0,
|
|
889
|
+
end: currentPositionEnd || 0
|
|
890
|
+
}
|
|
212
891
|
};
|
|
213
892
|
}
|
|
214
|
-
|
|
893
|
+
/**
|
|
894
|
+
* set input element selection position position,
|
|
895
|
+
* if you need scroll that into view set `scrollIntoView` as true
|
|
896
|
+
*/ function setSelectionPosition(target, options) {
|
|
215
897
|
const elm = getRefElement(target);
|
|
216
898
|
if (elm && options.start !== null && options.end !== null) {
|
|
217
|
-
|
|
218
|
-
|
|
899
|
+
// * use blur and refocus to refocus position into view
|
|
900
|
+
if (options.scrollIntoView) elm.blur();
|
|
219
901
|
elm.setSelectionRange(options.start, options.end, options.direction);
|
|
220
|
-
if (options.scrollIntoView)
|
|
221
|
-
elm.focus();
|
|
902
|
+
if (options.scrollIntoView) elm.focus();
|
|
222
903
|
}
|
|
223
904
|
}
|
|
224
|
-
function clearWindowSelection() {
|
|
905
|
+
/** clean whole selection */ function clearWindowSelection() {
|
|
225
906
|
if (window?.getSelection) {
|
|
226
907
|
const selection = window.getSelection();
|
|
227
908
|
if (selection) {
|
|
909
|
+
// Chrome
|
|
228
910
|
selection.empty?.();
|
|
911
|
+
// Firefox
|
|
229
912
|
selection.removeAllRanges?.();
|
|
230
913
|
}
|
|
231
914
|
}
|
|
232
915
|
}
|
|
233
916
|
|
|
234
|
-
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Merge tailwind classes with additional class groups for spring-ui
|
|
921
|
+
*/ const twMerge = extendTailwindMerge({
|
|
922
|
+
extend: {
|
|
923
|
+
classGroups: {
|
|
924
|
+
typography: [
|
|
925
|
+
{
|
|
926
|
+
typography: [
|
|
927
|
+
()=>true
|
|
928
|
+
]
|
|
929
|
+
}
|
|
930
|
+
],
|
|
931
|
+
'focus-ring-rounded': [
|
|
932
|
+
'focus-ring-rounded',
|
|
933
|
+
{
|
|
934
|
+
'focus-ring-rounded': [
|
|
935
|
+
()=>true
|
|
936
|
+
]
|
|
937
|
+
}
|
|
938
|
+
],
|
|
939
|
+
'focus-ring-border': [
|
|
940
|
+
'focus-ring-border',
|
|
941
|
+
{
|
|
942
|
+
'focus-ring-border': [
|
|
943
|
+
()=>true
|
|
944
|
+
]
|
|
945
|
+
}
|
|
946
|
+
],
|
|
947
|
+
'focus-ring-border-recoup': [
|
|
948
|
+
'focus-ring-border-recoup',
|
|
949
|
+
'no-focus-ring-border-recoup'
|
|
950
|
+
],
|
|
951
|
+
'focus-ring': [
|
|
952
|
+
{
|
|
953
|
+
'focus-ring': [
|
|
954
|
+
()=>true
|
|
955
|
+
]
|
|
956
|
+
}
|
|
957
|
+
]
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
export { a11yKeyboard, ariaHideOutside, capitalize, checkDefaultPrevented, checkElementName, clearWindowSelection, combineClasses, combineProps, createAriaHideOutsideController, getRefElement, getSelectionPosition, getTitle, globalStateClasses, isDeleteKeyboardEvent, isRef, isSuiElement, isUrl, isWebKit154, logInDev, omit_omit as omit, outerSize, ownerWindow, pick_pick as pick, preloadImg, removeClassName, runKeyHandler, setSelectionPosition, twMerge, unstable_ownerDocument as ownerDocument, useClasses };
|