@ringcentral/spring-base 1.7.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/LICENSE +22 -0
- package/README.md +61 -0
- package/chunks/twMerge.js +409 -0
- package/config.js +78 -0
- package/contexts.js +71 -0
- package/hooks.js +2101 -0
- package/index.d.ts +1 -0
- package/index.js +16 -0
- package/package.json +30 -0
- package/src/config.d.ts +40 -0
- package/src/contexts/ResponsiveContext/ResponsiveContext.d.ts +7 -0
- package/src/contexts/ResponsiveContext/breakpointList.d.ts +3 -0
- package/src/contexts/ResponsiveContext/getMatchDetail.d.ts +2 -0
- package/src/contexts/ResponsiveContext/index.d.ts +4 -0
- package/src/contexts/ResponsiveContext/types.d.ts +24 -0
- package/src/contexts/WindowContext.d.ts +9 -0
- package/src/contexts/index.d.ts +2 -0
- package/src/hooks/index.d.ts +43 -0
- package/src/hooks/useA11yKeyEvent/index.d.ts +1 -0
- package/src/hooks/useA11yKeyEvent/useA11yKeyEvent.d.ts +23 -0
- package/src/hooks/useActive/index.d.ts +1 -0
- package/src/hooks/useActive/useActive.d.ts +23 -0
- package/src/hooks/useAnnouncer/index.d.ts +1 -0
- package/src/hooks/useAnnouncer/useAnnouncer.d.ts +12 -0
- package/src/hooks/useAriaHideOutside/index.d.ts +1 -0
- package/src/hooks/useAriaHideOutside/useAriaHideOutside.d.ts +12 -0
- package/src/hooks/useAudio/index.d.ts +1 -0
- package/src/hooks/useAudio/useAudio.d.ts +21 -0
- package/src/hooks/useBodyScrollLock/index.d.ts +1 -0
- package/src/hooks/useBodyScrollLock/useBodyScrollLock.d.ts +25 -0
- package/src/hooks/useChange/index.d.ts +1 -0
- package/src/hooks/useChange/useChange.d.ts +12 -0
- package/src/hooks/useClickAway/index.d.ts +1 -0
- package/src/hooks/useClickAway/useClickAway.d.ts +22 -0
- package/src/hooks/useControlled/index.d.ts +1 -0
- package/src/hooks/useControlled/useControlled.d.ts +1 -0
- package/src/hooks/useDebounce/index.d.ts +1 -0
- package/src/hooks/useDebounce/useDebounce.d.ts +8 -0
- package/src/hooks/useEventCallback/index.d.ts +1 -0
- package/src/hooks/useEventCallback/useEventCallback.d.ts +1 -0
- package/src/hooks/useEventListener/index.d.ts +1 -0
- package/src/hooks/useEventListener/useEventListener.d.ts +32 -0
- package/src/hooks/useEver/index.d.ts +1 -0
- package/src/hooks/useEver/useEver.d.ts +13 -0
- package/src/hooks/useFocusVisible/index.d.ts +1 -0
- package/src/hooks/useFocusVisible/useFocusVisible.d.ts +22 -0
- package/src/hooks/useFontLoaded/index.d.ts +1 -0
- package/src/hooks/useFontLoaded/useFontLoaded.d.ts +13 -0
- package/src/hooks/useForceUpdate/index.d.ts +1 -0
- package/src/hooks/useForceUpdate/useForceUpdate.d.ts +5 -0
- package/src/hooks/useForkRef/index.d.ts +1 -0
- package/src/hooks/useForkRef/useForkRef.d.ts +19 -0
- package/src/hooks/useGlobalListener/createGlobalListener.d.ts +55 -0
- package/src/hooks/useGlobalListener/index.d.ts +2 -0
- package/src/hooks/useGlobalListener/useGlobalListener.d.ts +14 -0
- package/src/hooks/useHiddenTabindex/index.d.ts +1 -0
- package/src/hooks/useHiddenTabindex/useHiddenTabindex.d.ts +9 -0
- package/src/hooks/useHover/index.d.ts +2 -0
- package/src/hooks/useHover/useHover.d.ts +17 -0
- package/src/hooks/useHover/useHoverDirty.d.ts +8 -0
- package/src/hooks/useId/index.d.ts +1 -0
- package/src/hooks/useId/useId.d.ts +15 -0
- package/src/hooks/useImgLoaded/index.d.ts +1 -0
- package/src/hooks/useImgLoaded/useImgLoaded.d.ts +24 -0
- package/src/hooks/useInterval/index.d.ts +1 -0
- package/src/hooks/useInterval/useInterval.d.ts +43 -0
- package/src/hooks/useIsFocusVisible/index.d.ts +2 -0
- package/src/hooks/useIsFocusVisible/useIsFocusVisible.d.ts +9 -0
- package/src/hooks/useKeyDownOnce/index.d.ts +1 -0
- package/src/hooks/useKeyDownOnce/useKeyDownOnce.d.ts +15 -0
- package/src/hooks/useKeyboardMoveFocus/index.d.ts +3 -0
- package/src/hooks/useKeyboardMoveFocus/moveIndexInTwoDimension.d.ts +18 -0
- package/src/hooks/useKeyboardMoveFocus/useKeyboardMoveFocus.d.ts +49 -0
- package/src/hooks/useKeyboardMoveFocus/useOnlyOneFocusable.d.ts +17 -0
- package/src/hooks/useLongPress/index.d.ts +1 -0
- package/src/hooks/useLongPress/useLongPress.d.ts +39 -0
- package/src/hooks/useMountState/index.d.ts +1 -0
- package/src/hooks/useMountState/useMountState.d.ts +16 -0
- package/src/hooks/useOnLayoutReRender/index.d.ts +1 -0
- package/src/hooks/useOnLayoutReRender/useOnLayoutReRender.d.ts +5 -0
- package/src/hooks/useOnReRender/index.d.ts +1 -0
- package/src/hooks/useOnReRender/useOnReRender.d.ts +5 -0
- package/src/hooks/useOverflow/index.d.ts +1 -0
- package/src/hooks/useOverflow/useOverflow.d.ts +11 -0
- package/src/hooks/usePrevious/index.d.ts +1 -0
- package/src/hooks/usePrevious/usePrevious.d.ts +7 -0
- package/src/hooks/useRefState/index.d.ts +1 -0
- package/src/hooks/useRefState/useRefState.d.ts +21 -0
- package/src/hooks/useResizeObserver/index.d.ts +1 -0
- package/src/hooks/useResizeObserver/useResizeObserver.d.ts +24 -0
- package/src/hooks/useResponsiveMatch/index.d.ts +1 -0
- package/src/hooks/useResponsiveMatch/useResponsiveMatch.d.ts +7 -0
- package/src/hooks/useResultRef/index.d.ts +1 -0
- package/src/hooks/useResultRef/useResultRef.d.ts +10 -0
- package/src/hooks/useRetry/index.d.ts +1 -0
- package/src/hooks/useRetry/useRetry.d.ts +35 -0
- package/src/hooks/useSleep/index.d.ts +1 -0
- package/src/hooks/useSleep/useSleep.d.ts +22 -0
- package/src/hooks/useTabOut/index.d.ts +1 -0
- package/src/hooks/useTabOut/useTabOut.d.ts +10 -0
- package/src/hooks/useTestRunnerDetection.d.ts +7 -0
- package/src/hooks/useThrottle/index.d.ts +1 -0
- package/src/hooks/useThrottle/useThrottle.d.ts +8 -0
- package/src/hooks/useTimeout/index.d.ts +1 -0
- package/src/hooks/useTimeout/useTimeout.d.ts +2 -0
- package/src/hooks/useTouchMouseEvent/index.d.ts +1 -0
- package/src/hooks/useTouchMouseEvent/useTouchMouseEvent.d.ts +56 -0
- package/src/index.d.ts +5 -0
- package/src/typings/BaseColor.d.ts +14 -0
- package/src/typings/BaseSize.d.ts +15 -0
- package/src/typings/ComponentTypes.d.ts +50 -0
- package/src/typings/ExtractValue.d.ts +2 -0
- package/src/typings/OverridableTypes.d.ts +45 -0
- package/src/typings/UnionOmit.d.ts +3 -0
- package/src/typings/UnitMap.d.ts +28 -0
- package/src/typings/index.d.ts +7 -0
- package/src/utils/a11yKeyboardCode.d.ts +8 -0
- package/src/utils/ariaHideOutside.d.ts +9 -0
- package/src/utils/capitalize.d.ts +1 -0
- package/src/utils/checkDefaultPrevented.d.ts +3 -0
- package/src/utils/checkElementName.d.ts +2 -0
- package/src/utils/checkKeyboardEvent.d.ts +2 -0
- package/src/utils/classes/globalStateClasses.d.ts +14 -0
- package/src/utils/classes/index.d.ts +2 -0
- package/src/utils/classes/useClasses.d.ts +41 -0
- package/src/utils/combineProps.d.ts +90 -0
- package/src/utils/devlog.d.ts +7 -0
- package/src/utils/getRefElement.d.ts +6 -0
- package/src/utils/getTitle.d.ts +1 -0
- package/src/utils/index.d.ts +25 -0
- package/src/utils/isRef.d.ts +2 -0
- package/src/utils/isSuiElement.d.ts +1 -0
- package/src/utils/isUrl.d.ts +1 -0
- package/src/utils/isWebKit154.d.ts +9 -0
- package/src/utils/omit.d.ts +2 -0
- package/src/utils/outerSize.d.ts +8 -0
- package/src/utils/ownerDocument.d.ts +1 -0
- package/src/utils/ownerWindow.d.ts +1 -0
- package/src/utils/pick.d.ts +2 -0
- package/src/utils/preloadImg.d.ts +2 -0
- package/src/utils/removeClassName.d.ts +35 -0
- package/src/utils/runKeyHandler.d.ts +12 -0
- package/src/utils/selectionHandler.d.ts +23 -0
- package/src/utils/twMerge.d.ts +4 -0
- package/typings.js +1 -0
- package/utils.js +333 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 RingCentral, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @ringcentral/spring-base
|
|
2
|
+
|
|
3
|
+
Core utilities and hooks for Spring UI.
|
|
4
|
+
|
|
5
|
+
Note: If you already use `@ringcentral/spring-ui`, these utilities are re-exported there. You can import them directly from `@ringcentral/spring-ui` (recommended), or from `@ringcentral/spring-base` if you only use the base package.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Using yarn:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
yarn add @ringcentral/spring-base
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Using npm:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i @ringcentral/spring-base
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// If your app already installs @ringcentral/spring-ui (recommended)
|
|
26
|
+
import { twMerge } from '@ringcentral/spring-ui';
|
|
27
|
+
|
|
28
|
+
// Or, when using the package directly
|
|
29
|
+
import { twMerge } from '@ringcentral/spring-base';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Utilities
|
|
33
|
+
|
|
34
|
+
### twMerge
|
|
35
|
+
Merge Tailwind classes with Spring UI class groups.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { twMerge } from '@ringcentral/spring-base';
|
|
39
|
+
|
|
40
|
+
twMerge('typography-label', 'typography-detailBold');
|
|
41
|
+
// => 'typography-detailBold'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Example Hook
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import * as React from 'react';
|
|
48
|
+
import { useDebounce } from '@ringcentral/spring-base';
|
|
49
|
+
|
|
50
|
+
function Search({ onQuery }: { onQuery: (q: string) => void }) {
|
|
51
|
+
const [value, setValue] = React.useState('');
|
|
52
|
+
const debounced = useDebounce(value, 300);
|
|
53
|
+
React.useEffect(() => onQuery(debounced), [debounced, onQuery]);
|
|
54
|
+
return <input value={value} onChange={e => setValue(e.target.value)} />;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
61
|
+
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { unstable_ownerWindow } from '@mui/utils';
|
|
2
|
+
import { isValidElement } from 'react';
|
|
3
|
+
import isPlainObject from 'lodash/isPlainObject';
|
|
4
|
+
import { rcConfiguration } from '../config.js';
|
|
5
|
+
import { extendTailwindMerge } from 'tailwind-merge';
|
|
6
|
+
|
|
7
|
+
const ownerWindow = unstable_ownerWindow;
|
|
8
|
+
|
|
9
|
+
// Copy from https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/ariaHideOutside.ts
|
|
10
|
+
// And support multiple window
|
|
11
|
+
const ARIA_HIDDEN = 'aria-hidden';
|
|
12
|
+
const REF_COUNT_MAP = '__SUI_ARIA_HIDE_OUTSIDE_REF_COUNT_MAP__';
|
|
13
|
+
const getRefCountMap = (window) => {
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
if (!window[REF_COUNT_MAP]) {
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
window[REF_COUNT_MAP] = new WeakMap();
|
|
18
|
+
}
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
return window[REF_COUNT_MAP];
|
|
21
|
+
};
|
|
22
|
+
const OBSERVER_STACK = '__SUI_ARIA_HIDE_OUTSIDE_OBSERVER_STACK__';
|
|
23
|
+
const getObserverStack = (window) => {
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
if (!window[OBSERVER_STACK]) {
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
window[OBSERVER_STACK] = [];
|
|
28
|
+
}
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
return window[OBSERVER_STACK];
|
|
31
|
+
};
|
|
32
|
+
const isHTMLOrSVGElement = (node) => {
|
|
33
|
+
return 'dataset' in node;
|
|
34
|
+
};
|
|
35
|
+
const isElement = (node) => {
|
|
36
|
+
return node.nodeType === Node.ELEMENT_NODE;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Hides all elements in the DOM outside the given targets from screen readers using aria-hidden,
|
|
40
|
+
* and returns a function to revert these changes. In addition, changes to the DOM are watched
|
|
41
|
+
* and new elements outside the targets are automatically hidden.
|
|
42
|
+
* @param targets - The elements that should remain visible.
|
|
43
|
+
* @param root - Nothing will be hidden above this element.
|
|
44
|
+
* @returns - A function to restore all hidden elements.
|
|
45
|
+
*/
|
|
46
|
+
const ariaHideOutside = (targets, root = document.body) => {
|
|
47
|
+
const currentWindow = ownerWindow(root);
|
|
48
|
+
if (!currentWindow) {
|
|
49
|
+
// Current JS environment doesn't have a window object, do nothing.
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
51
|
+
return () => { };
|
|
52
|
+
}
|
|
53
|
+
const visibleNodes = new Set(targets);
|
|
54
|
+
const hiddenNodes = new Set();
|
|
55
|
+
// Keeps a ref count of all hidden elements. Added to when hiding an element, and
|
|
56
|
+
// subtracted from when showing it again. When it reaches zero, aria-hidden is removed.
|
|
57
|
+
const refCountMap = getRefCountMap(currentWindow);
|
|
58
|
+
const observerStack = getObserverStack(currentWindow);
|
|
59
|
+
const walker = currentWindow.document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
|
60
|
+
acceptNode(node) {
|
|
61
|
+
// If this node is a live announcer, add it to the set of nodes to keep visible.
|
|
62
|
+
if (isHTMLOrSVGElement(node) &&
|
|
63
|
+
node.dataset['liveAnnouncer'] === 'true') {
|
|
64
|
+
visibleNodes.add(node);
|
|
65
|
+
}
|
|
66
|
+
// Skip this node and its children if it is one of the target nodes, or a live announcer.
|
|
67
|
+
// Also skip children of already hidden nodes, as aria-hidden is recursive. An exception is
|
|
68
|
+
// made for elements with role="row" since VoiceOver on iOS has issues hiding elements with role="row".
|
|
69
|
+
// For that case we want to hide the cells inside as well (https://bugs.webkit.org/show_bug.cgi?id=222623).
|
|
70
|
+
if (visibleNodes.has(node) ||
|
|
71
|
+
(node.parentElement &&
|
|
72
|
+
hiddenNodes.has(node.parentElement) &&
|
|
73
|
+
node.parentElement.getAttribute('role') !== 'row')) {
|
|
74
|
+
return NodeFilter.FILTER_REJECT;
|
|
75
|
+
}
|
|
76
|
+
// Skip this node but continue to children if one of the targets is inside the node.
|
|
77
|
+
if (targets.some((target) => node.contains(target))) {
|
|
78
|
+
return NodeFilter.FILTER_SKIP;
|
|
79
|
+
}
|
|
80
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const hide = (node) => {
|
|
84
|
+
const refCount = refCountMap.get(node) ?? 0;
|
|
85
|
+
// If already aria-hidden, and the ref count is zero, then this element
|
|
86
|
+
// was already hidden and there's nothing for us to do.
|
|
87
|
+
if (node.getAttribute(ARIA_HIDDEN) === 'true' && refCount === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (refCount === 0) {
|
|
91
|
+
node.setAttribute(ARIA_HIDDEN, 'true');
|
|
92
|
+
}
|
|
93
|
+
hiddenNodes.add(node);
|
|
94
|
+
refCountMap.set(node, refCount + 1);
|
|
95
|
+
};
|
|
96
|
+
// If there is already a MutationObserver listening from a previous call,
|
|
97
|
+
// disconnect it so the new on takes over.
|
|
98
|
+
if (observerStack.length) {
|
|
99
|
+
observerStack[observerStack.length - 1].disconnect();
|
|
100
|
+
}
|
|
101
|
+
let node = walker.nextNode();
|
|
102
|
+
while (node != null) {
|
|
103
|
+
hide(node);
|
|
104
|
+
node = walker.nextNode();
|
|
105
|
+
}
|
|
106
|
+
const observer = new currentWindow.MutationObserver((changes) => {
|
|
107
|
+
for (const change of changes) {
|
|
108
|
+
if (change.type !== 'childList' || change.addedNodes.length === 0) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// If the parent element of the added nodes is not within one of the targets,
|
|
112
|
+
// and not already inside a hidden node, hide all of the new children.
|
|
113
|
+
if (![...visibleNodes, ...hiddenNodes].some((node) => node.contains(change.target))) {
|
|
114
|
+
for (const node of change.addedNodes) {
|
|
115
|
+
if (isHTMLOrSVGElement(node) &&
|
|
116
|
+
node.dataset['liveAnnouncer'] === 'true') {
|
|
117
|
+
visibleNodes.add(node);
|
|
118
|
+
}
|
|
119
|
+
else if (isElement(node)) {
|
|
120
|
+
hide(node);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
127
|
+
const observerWrapper = {
|
|
128
|
+
observe() {
|
|
129
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
130
|
+
},
|
|
131
|
+
disconnect() {
|
|
132
|
+
observer.disconnect();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
observerStack.push(observerWrapper);
|
|
136
|
+
return () => {
|
|
137
|
+
observer.disconnect();
|
|
138
|
+
for (const node of hiddenNodes) {
|
|
139
|
+
const count = refCountMap.get(node);
|
|
140
|
+
if (count === 1) {
|
|
141
|
+
node.removeAttribute(ARIA_HIDDEN);
|
|
142
|
+
refCountMap.delete(node);
|
|
143
|
+
}
|
|
144
|
+
else if (typeof count === 'number') {
|
|
145
|
+
refCountMap.set(node, count - 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Remove this observer from the stack, and start the previous one.
|
|
149
|
+
if (observerWrapper === observerStack[observerStack.length - 1]) {
|
|
150
|
+
observerStack.pop();
|
|
151
|
+
if (observerStack.length) {
|
|
152
|
+
observerStack[observerStack.length - 1].observe();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
observerStack.splice(observerStack.indexOf(observerWrapper), 1);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Combine props object together
|
|
163
|
+
*
|
|
164
|
+
* that will group className, classes together
|
|
165
|
+
* function will auto combine together,
|
|
166
|
+
* trigger order is `intoProps.someFn(e) => defaultProps.someFn(e)`
|
|
167
|
+
*
|
|
168
|
+
* @param baseProps base props you want to combine together
|
|
169
|
+
* @param newProps want to combine in to props
|
|
170
|
+
* @param coverPropKeys some prop key you want to cover, not combine when type is function, ex. `ComponentType`
|
|
171
|
+
*
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```tsx
|
|
175
|
+
* combineProps(
|
|
176
|
+
{
|
|
177
|
+
children: <A />,
|
|
178
|
+
value: '1',
|
|
179
|
+
className: 'a1',
|
|
180
|
+
classes: {
|
|
181
|
+
root: 'root1'
|
|
182
|
+
},
|
|
183
|
+
count: 1,
|
|
184
|
+
size: 'large',
|
|
185
|
+
onClick: fn1
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
children: <B />,
|
|
189
|
+
value: '2',
|
|
190
|
+
className: 'a2',
|
|
191
|
+
classes: {
|
|
192
|
+
root: 'root2'
|
|
193
|
+
},
|
|
194
|
+
count: 2,
|
|
195
|
+
size: undefined,
|
|
196
|
+
onClick: fn2
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
// output result:
|
|
200
|
+
{
|
|
201
|
+
children: <B />,
|
|
202
|
+
value: '2',
|
|
203
|
+
className: 'a1 a2',
|
|
204
|
+
classes: {
|
|
205
|
+
root: 'root1 root2'
|
|
206
|
+
},
|
|
207
|
+
count: 2,
|
|
208
|
+
size: 'large',
|
|
209
|
+
onClick: (e) => {
|
|
210
|
+
fn2(e);
|
|
211
|
+
fn1(e);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
const combineProps = (baseProps, newProps, coverPropKeys) => innerCombineProps({
|
|
217
|
+
baseProps,
|
|
218
|
+
newProps,
|
|
219
|
+
coverPropKeys,
|
|
220
|
+
});
|
|
221
|
+
/**
|
|
222
|
+
* Combine two classes
|
|
223
|
+
*
|
|
224
|
+
* @param baseClasses base classes you want to combine together
|
|
225
|
+
* @param newClasses new classes want to combine in to props
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* combineClasses(
|
|
230
|
+
{
|
|
231
|
+
root: 'a1',
|
|
232
|
+
input: 'a1',
|
|
233
|
+
un1: undefined,
|
|
234
|
+
un2: 'a4',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
root: 'a1',
|
|
238
|
+
input: 'a2',
|
|
239
|
+
un1: 'a3',
|
|
240
|
+
un2: undefined,
|
|
241
|
+
un3: 'a5',
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
// output result:
|
|
245
|
+
{
|
|
246
|
+
root: 'a1 a1',
|
|
247
|
+
input: 'a1 a2',
|
|
248
|
+
un1: 'a3',
|
|
249
|
+
un2: 'a4',
|
|
250
|
+
un3: 'a5',
|
|
251
|
+
}
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
const combineClasses = (baseClasses, newClasses) => innerCombineProps({
|
|
255
|
+
baseProps: baseClasses,
|
|
256
|
+
newProps: newClasses,
|
|
257
|
+
combineAllString: true,
|
|
258
|
+
});
|
|
259
|
+
const innerCombineProps = (params) => {
|
|
260
|
+
const { baseProps, newProps } = params;
|
|
261
|
+
if (!newProps || Object.keys(newProps).length === 0) {
|
|
262
|
+
return baseProps;
|
|
263
|
+
}
|
|
264
|
+
const outputProps = mergeChildProps(params);
|
|
265
|
+
return {
|
|
266
|
+
...newProps,
|
|
267
|
+
...outputProps,
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
function mergeChildProps({ baseProps, newProps, combineAllString, coverPropKeys, }) {
|
|
271
|
+
const result = Object.keys(baseProps || {}).reduce((output, curr) => {
|
|
272
|
+
const prop = baseProps[curr];
|
|
273
|
+
const propTo = newProps[curr];
|
|
274
|
+
if (propTo !== undefined) {
|
|
275
|
+
// * all typeof type below
|
|
276
|
+
// "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
|
|
277
|
+
switch (typeof prop) {
|
|
278
|
+
case 'undefined':
|
|
279
|
+
output[curr] = propTo;
|
|
280
|
+
break;
|
|
281
|
+
case 'object': {
|
|
282
|
+
if (!isValidElement(propTo) &&
|
|
283
|
+
!isValidElement(prop) &&
|
|
284
|
+
isPlainObject(propTo) &&
|
|
285
|
+
isPlainObject(prop)) {
|
|
286
|
+
output[curr] = mergeChildProps({
|
|
287
|
+
baseProps: prop,
|
|
288
|
+
newProps: propTo || {},
|
|
289
|
+
combineAllString: combineAllString || curr === 'classes',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
output[curr] = propTo;
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case 'string':
|
|
298
|
+
if (combineAllString || curr === 'className') {
|
|
299
|
+
output[curr] = [prop, propTo]
|
|
300
|
+
.filter((x) => typeof x === 'string' && x)
|
|
301
|
+
.join(' ');
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
output[curr] = propTo;
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
case 'boolean':
|
|
308
|
+
case 'symbol':
|
|
309
|
+
case 'bigint':
|
|
310
|
+
case 'number':
|
|
311
|
+
output[curr] = propTo;
|
|
312
|
+
break;
|
|
313
|
+
case 'function':
|
|
314
|
+
if (coverPropKeys && coverPropKeys.includes(curr) && propTo) {
|
|
315
|
+
output[curr] = propTo;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
output[curr] = (...args) => {
|
|
319
|
+
// * exec outside first
|
|
320
|
+
propTo?.(...args);
|
|
321
|
+
prop?.(...args);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
output[curr] = prop;
|
|
329
|
+
}
|
|
330
|
+
return output;
|
|
331
|
+
}, {});
|
|
332
|
+
// if result is an empty {} reset that to undefined
|
|
333
|
+
if (Object.keys(result).length === 0) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
...newProps,
|
|
338
|
+
...result,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* eslint-disable no-console */
|
|
343
|
+
function logInDev({ scope, message, level = 'warn', condition = true, }) {
|
|
344
|
+
if (process.env.NODE_ENV !== 'production' &&
|
|
345
|
+
condition &&
|
|
346
|
+
!rcConfiguration.WARNING_IGNORE) {
|
|
347
|
+
const log = `SPRING [${scope}]: ${message}`;
|
|
348
|
+
if (process.env.NODE_ENV === 'test' && level === 'error') {
|
|
349
|
+
throw Error(log);
|
|
350
|
+
}
|
|
351
|
+
console[level](log);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const isRef = (obj) => {
|
|
356
|
+
return obj && 'current' in obj;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* get element from React ref or callback
|
|
361
|
+
*/
|
|
362
|
+
const getRefElement = (ref) => {
|
|
363
|
+
if (typeof ref === 'function') {
|
|
364
|
+
return ref();
|
|
365
|
+
}
|
|
366
|
+
if (typeof ref === 'object' && isRef(ref)) {
|
|
367
|
+
return ref.current;
|
|
368
|
+
}
|
|
369
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
370
|
+
return ref;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Merge tailwind classes with additional class groups for spring-ui
|
|
375
|
+
*/
|
|
376
|
+
const twMerge = extendTailwindMerge({
|
|
377
|
+
extend: {
|
|
378
|
+
classGroups: {
|
|
379
|
+
typography: [
|
|
380
|
+
{
|
|
381
|
+
typography: [() => true],
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
'focus-ring-rounded': [
|
|
385
|
+
'focus-ring-rounded',
|
|
386
|
+
{
|
|
387
|
+
'focus-ring-rounded': [() => true],
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
'focus-ring-border': [
|
|
391
|
+
'focus-ring-border',
|
|
392
|
+
{
|
|
393
|
+
'focus-ring-border': [() => true],
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
'focus-ring-border-recoup': [
|
|
397
|
+
'focus-ring-border-recoup',
|
|
398
|
+
'no-focus-ring-border-recoup',
|
|
399
|
+
],
|
|
400
|
+
'focus-ring': [
|
|
401
|
+
{
|
|
402
|
+
'focus-ring': [() => true],
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
export { ariaHideOutside as a, combineClasses as b, combineProps as c, getRefElement as g, isRef as i, logInDev as l, ownerWindow as o, twMerge as t };
|
package/config.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// TODO: refactor config
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
const SPRING_WARNING_IGNORE_KEY = '__SPRING_WARNING_IGNORE__';
|
|
4
|
+
// * use that way to avoid that code be exist in production code base
|
|
5
|
+
/**
|
|
6
|
+
* config global setting
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* configure({
|
|
10
|
+
* warning: false
|
|
11
|
+
* })
|
|
12
|
+
* ```
|
|
13
|
+
* ## WARNING related method and only work in `development mode`
|
|
14
|
+
*
|
|
15
|
+
* `WARNING_IGNORE`: only work in `development mode`
|
|
16
|
+
* `WARNING_FUNCTION` only work in `development mode`
|
|
17
|
+
*/
|
|
18
|
+
let rcConfiguration = {};
|
|
19
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
20
|
+
class RcConfig {
|
|
21
|
+
_WARNING_IGNORE = false;
|
|
22
|
+
_WARNING_FUNCTION = (message, options) => {
|
|
23
|
+
console[options?.level || 'warn'](message);
|
|
24
|
+
};
|
|
25
|
+
constructor() {
|
|
26
|
+
if (typeof window !== 'undefined') {
|
|
27
|
+
// SSR support
|
|
28
|
+
// * get value once when init
|
|
29
|
+
this._WARNING_IGNORE =
|
|
30
|
+
window?.localStorage?.getItem(SPRING_WARNING_IGNORE_KEY) === 'true';
|
|
31
|
+
// * also provide user to set variable from window
|
|
32
|
+
window.__SPRING__ = this;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
set WARNING_IGNORE(value) {
|
|
36
|
+
if (value) {
|
|
37
|
+
console.log('---------------- [Spring] ----------------');
|
|
38
|
+
console.log(`Don't recommended you close it, we should avoid use any deprecated props or methods.`);
|
|
39
|
+
console.log('---------------- [Spring] ----------------');
|
|
40
|
+
}
|
|
41
|
+
this._WARNING_IGNORE = !!value;
|
|
42
|
+
window?.localStorage?.setItem(SPRING_WARNING_IGNORE_KEY, `${this._WARNING_IGNORE}`);
|
|
43
|
+
}
|
|
44
|
+
get WARNING_IGNORE() {
|
|
45
|
+
return this._WARNING_IGNORE;
|
|
46
|
+
}
|
|
47
|
+
set WARNING_FUNCTION(func) {
|
|
48
|
+
if (typeof func === 'function') {
|
|
49
|
+
this._WARNING_FUNCTION = func;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.warn('[Spring] config warningFunction is not a function.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
get WARNING_FUNCTION() {
|
|
56
|
+
return this._WARNING_FUNCTION;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
rcConfiguration = new RcConfig();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* set config with Spring
|
|
63
|
+
*
|
|
64
|
+
* ## WARNING method only work in `development mode`
|
|
65
|
+
*/
|
|
66
|
+
const configure = (options) => {
|
|
67
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
68
|
+
const { warning, warningFunction } = options;
|
|
69
|
+
if (warning !== undefined) {
|
|
70
|
+
rcConfiguration.WARNING_IGNORE = !warning;
|
|
71
|
+
}
|
|
72
|
+
if (warningFunction !== undefined) {
|
|
73
|
+
rcConfiguration.WARNING_FUNCTION = warningFunction;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export { configure, rcConfiguration };
|
package/contexts.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
const WindowContext = createContext({
|
|
4
|
+
document: typeof window !== 'undefined'
|
|
5
|
+
? window.document
|
|
6
|
+
: globalThis.window?.document
|
|
7
|
+
? globalThis.window.document
|
|
8
|
+
: undefined,
|
|
9
|
+
globalThis: globalThis,
|
|
10
|
+
externalWindow: typeof window !== 'undefined' ? window : globalThis.window,
|
|
11
|
+
});
|
|
12
|
+
const useWindowContext = () => useContext(WindowContext);
|
|
13
|
+
|
|
14
|
+
/** small to large */
|
|
15
|
+
const breakpointList = ['xs', 'sm', 'md', 'lg', 'xl'];
|
|
16
|
+
|
|
17
|
+
const matchCache = new Map();
|
|
18
|
+
const getMatchGroups = (breakpoint) => {
|
|
19
|
+
const ltMatchGroup = [];
|
|
20
|
+
const gtMatchGroup = [];
|
|
21
|
+
if (breakpoint === null) {
|
|
22
|
+
for (let i = 0; i < breakpointList.length; i++) {
|
|
23
|
+
ltMatchGroup.push(`lt-${breakpointList[i]}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const bpIndex = breakpointList.indexOf(breakpoint);
|
|
28
|
+
for (let i = 0; i < bpIndex; i++) {
|
|
29
|
+
gtMatchGroup.unshift(`gt-${breakpointList[i]}`);
|
|
30
|
+
}
|
|
31
|
+
for (let i = bpIndex + 1; i < breakpointList.length; i++) {
|
|
32
|
+
ltMatchGroup.push(`lt-${breakpointList[i]}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { ltMatchGroup, gtMatchGroup };
|
|
36
|
+
};
|
|
37
|
+
const getMatchMap = ({ ltMatchGroup, gtMatchGroup }, matchBP) => {
|
|
38
|
+
const matchMap = {};
|
|
39
|
+
if (matchBP !== null)
|
|
40
|
+
matchMap[matchBP] = true;
|
|
41
|
+
for (const ltBP of ltMatchGroup) {
|
|
42
|
+
const [, bp] = ltBP.split('-');
|
|
43
|
+
matchMap[`lt${bp.toUpperCase()}`] = true;
|
|
44
|
+
matchMap[ltBP] = true;
|
|
45
|
+
}
|
|
46
|
+
for (const gtBP of gtMatchGroup) {
|
|
47
|
+
const [, bp] = gtBP.split('-');
|
|
48
|
+
matchMap[`gt${bp.toUpperCase()}`] = true;
|
|
49
|
+
matchMap[gtBP] = true;
|
|
50
|
+
}
|
|
51
|
+
return matchMap;
|
|
52
|
+
};
|
|
53
|
+
const getMatchDetail = (breakpoint) => {
|
|
54
|
+
if (!matchCache.has(breakpoint)) {
|
|
55
|
+
const matchGroups = getMatchGroups(breakpoint);
|
|
56
|
+
const matchMap = getMatchMap(matchGroups, breakpoint);
|
|
57
|
+
matchCache.set(breakpoint, {
|
|
58
|
+
...matchGroups,
|
|
59
|
+
matchBP: breakpoint,
|
|
60
|
+
matchMap,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return matchCache.get(breakpoint);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const RcResponsiveContext = createContext(undefined);
|
|
67
|
+
const useResponsiveContext = () => {
|
|
68
|
+
return useContext(RcResponsiveContext);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export { RcResponsiveContext, WindowContext, breakpointList, getMatchDetail, useResponsiveContext, useWindowContext };
|