@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/utils.js CHANGED
@@ -1,12 +1,10 @@
1
- import { g as getRefElement } from './chunks/twMerge.js';
2
- export { a as ariaHideOutside, b as combineClasses, c as combineProps, i as isRef, l as logInDev, o as ownerWindow, t as twMerge } from './chunks/twMerge.js';
3
- import React__default, { isValidElement, useCallback } from 'react';
4
- import lodashOmit from 'lodash/omit';
5
- export { unstable_ownerDocument as ownerDocument } from '@mui/utils';
6
- import lodashPick from 'lodash/pick';
7
- import 'lodash/isPlainObject';
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 (isValidElement(element) &&
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
- const simpleCamelCase = (str) => str
59
- .split('-')
60
- .map((item, index) => index ? item.charAt(0).toUpperCase() + item.slice(1).toLowerCase() : item)
61
- .join('');
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
- const useClasses = (prefix, inputClasses, className) => {
64
- return useCallback((...classValues) => {
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 (let i = 0; i < classValues.length; i++) {
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
- else {
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
- }, [className, inputClasses, prefix]);
534
+ }, [
535
+ className,
536
+ inputClasses,
537
+ prefix
538
+ ]);
111
539
  };
112
540
 
113
- const getTitle = (text) => {
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
- const isSuiElement = (element, suiNames) => {
118
- return (React__default.isValidElement(element) &&
119
- suiNames.indexOf(element.type.displayName) !== -1);
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
- const isWebKit154 = typeof navigator !== 'undefined' &&
128
- /^((?!chrome|android).)*(safari|mobile)/i.test(navigator.userAgent) &&
129
- /(os |version\/)15(.|_)[4-9]/i.test(navigator.userAgent);
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
- const omit = (classes, keys) => {
132
- return lodashOmit(classes, keys);
780
+
781
+ /** enhance lodash type */ const omit_omit = (classes, keys)=>{
782
+ return omit(classes, keys);
133
783
  };
134
784
 
135
- function outerSize(el) {
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 (el.offsetHeight +
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
- const pick = (classes, keys) => {
152
- return lodashPick(classes, keys);
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
- function removeClassName(target, removeClass, { scanChildren = false, fullMatch = false } = {}) {
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) => (node.className = cleanClass(node, removeClass, fullMatch)));
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
- const runKeyHandler = (e, keyHandlers) => {
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
- function getSelectionPosition(sourceTarget) {
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: { start: currentPosition || 0, end: currentPositionEnd || 0 },
887
+ position: {
888
+ start: currentPosition || 0,
889
+ end: currentPositionEnd || 0
890
+ }
212
891
  };
213
892
  }
214
- function setSelectionPosition(target, options) {
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
- if (options.scrollIntoView)
218
- elm.blur();
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
- export { a11yKeyboard, capitalize, checkDefaultPrevented, checkElementName, clearWindowSelection, getRefElement, getSelectionPosition, getTitle, globalStateClasses, isDeleteKeyboardEvent, isSuiElement, isUrl, isWebKit154, omit, outerSize, pick, preloadImg, removeClassName, runKeyHandler, setSelectionPosition, useClasses };
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 };