This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014-present yiminghe
Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+303
View File
@@ -0,0 +1,303 @@
# rc-util
Common Utils For React Component.
[![NPM version][npm-image]][npm-url]
[![npm download][download-image]][download-url]
[![build status][github-actions-image]][github-actions-url]
[![Codecov][codecov-image]][codecov-url]
[![bundle size][bundlephobia-image]][bundlephobia-url]
[![dumi][dumi-image]][dumi-url]
[npm-image]: http://img.shields.io/npm/v/rc-util.svg?style=flat-square
[npm-url]: http://npmjs.org/package/rc-util
[travis-image]: https://img.shields.io/travis/react-component/util/master?style=flat-square
[travis-url]: https://travis-ci.com/react-component/util
[github-actions-image]: https://github.com/react-component/util/actions/workflows/react-component-ci.yml/badge.svg
[github-actions-url]: https://github.com/react-component/util/actions/workflows/react-component-ci.yml
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/util/master.svg?style=flat-square
[codecov-url]: https://app.codecov.io/gh/react-component/util
[david-url]: https://david-dm.org/react-component/util
[david-image]: https://david-dm.org/react-component/util/status.svg?style=flat-square
[david-dev-url]: https://david-dm.org/react-component/util?type=dev
[david-dev-image]: https://david-dm.org/react-component/util/dev-status.svg?style=flat-square
[download-image]: https://img.shields.io/npm/dm/rc-util.svg?style=flat-square
[download-url]: https://npmjs.org/package/rc-util
[bundlephobia-url]: https://bundlephobia.com/package/rc-util
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-util
[dumi-url]: https://github.com/umijs/dumi
[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
## Install
[![rc-util](https://nodei.co/npm/rc-util.png)](https://npmjs.org/package/rc-util)
## API
### createChainedFunction
> (...functions): Function
Create a function which will call all the functions with it's arguments from left to right.
```jsx|pure
import createChainedFunction from 'rc-util/lib/createChainedFunction';
```
### deprecated
> (prop: string, instead: string, component: string): void
Log an error message to warn developers that `prop` is deprecated.
```jsx|pure
import deprecated from 'rc-util/lib/deprecated';
```
### getContainerRenderMixin
> (config: Object): Object
To generate a mixin which will render specific component into specific container automatically.
```jsx|pure
import getContainerRenderMixin from 'rc-util/lib/getContainerRenderMixin';
```
Fields in `config` and their meanings.
| Field | Type | Description | Default |
| ------------- | ---------------------------- | -------------------------------------------------------------------------- | ------- |
| autoMount | boolean | Whether to render component into container automatically | true |
| autoDestroy | boolean | Whether to remove container automatically while the component is unmounted | true |
| isVisible | (instance): boolean | A function to get current visibility of the component | - |
| isForceRender | (instance): boolean | A function to determine whether to render popup even it's not visible | - |
| getComponent | (instance, extra): ReactNode | A function to get the component which will be rendered into container | - |
| getContainer | (instance): HTMLElement | A function to get the container | |
### Portal
Render children to the specific container;
```jsx|pure
import Portal from 'rc-util/lib/Portal';
```
Props:
| Prop | Type | Description | Default |
| ------------ | --------------- | ------------------------------- | ------- |
| children | ReactChildren | Content render to the container | - |
| getContainer | (): HTMLElement | A function to get the container | - |
### getScrollBarSize
> (fresh?: boolean): number
Get the width of scrollbar.
```jsx|pure
import getScrollBarSize from 'rc-util/lib/getScrollBarSize';
```
### guid
> (): string
To generate a global unique id across current application.
```jsx|pure
import guid from 'rc-util/lib/guid';
```
### pickAttrs
> (props: Object): Object
Pick valid HTML attributes and events from props.
```jsx|pure
import pickAttrs from 'rc-util/lib/pickAttrs';
```
### warn
> (msg: string): void
A shallow wrapper of `console.warn`.
```jsx|pure
import warn from 'rc-util/lib/warn';
```
### warning
> (valid: boolean, msg: string): void
A shallow wrapper of [warning](https://github.com/BerkeleyTrue/warning), but only warning once for the same message.
```jsx|pure
import warning, { noteOnce } from 'rc-util/lib/warning';
warning(false, '[antd Component] test hello world');
// Low level note
noteOnce(false, '[antd Component] test hello world');
```
### Children
A collection of functions to operate React elements' children.
#### Children/mapSelf
> (children): children
Return a shallow copy of children.
```jsx|pure
import mapSelf from 'rc-util/lib/Children/mapSelf';
```
#### Children/toArray
> (children: ReactNode[]): ReactNode[]
Convert children into an array.
```jsx|pure
import toArray from 'rc-util/lib/Children/toArray';
```
### Dom
A collection of functions to operate DOM elements.
#### Dom/addEventlistener
> (target: ReactNode, eventType: string, listener: Function): { remove: Function }
A shallow wrapper of [add-dom-event-listener](https://github.com/yiminghe/add-dom-event-listener).
```jsx|pure
import addEventlistener from 'rc-util/lib/Dom/addEventlistener';
```
#### Dom/canUseDom
> (): boolean
Check if DOM is available.
```jsx|pure
import canUseDom from 'rc-util/lib/Dom/canUseDom';
```
#### Dom/class
A collection of functions to operate DOM nodes' class name.
- `hasClass(node: HTMLElement, className: string): boolean`
- `addClass(node: HTMLElement, className: string): void`
- `removeClass(node: HTMLElement, className: string): void`
```jsx|pure
import cssClass from 'rc-util/lib/Dom/class;
```
#### Dom/contains
> (root: HTMLElement, node: HTMLElement): boolean
Check if node is equal to root or in the subtree of root.
```jsx|pure
import contains from 'rc-util/lib/Dom/contains';
```
#### Dom/css
A collection of functions to get or set css styles.
- `get(node: HTMLElement, name?: string): any`
- `set(node: HTMLElement, name?: string, value: any) | set(node, object)`
- `getOuterWidth(el: HTMLElement): number`
- `getOuterHeight(el: HTMLElement): number`
- `getDocSize(): { width: number, height: number }`
- `getClientSize(): { width: number, height: number }`
- `getScroll(): { scrollLeft: number, scrollTop: number }`
- `getOffset(node: HTMLElement): { left: number, top: number }`
```jsx|pure
import css from 'rc-util/lib/Dom/css';
```
#### Dom/focus
A collection of functions to operate focus status of DOM node.
- `saveLastFocusNode(): void`
- `clearLastFocusNode(): void`
- `backLastFocusNode(): void`
- `getFocusNodeList(node: HTMLElement): HTMLElement[]` get a list of focusable nodes from the subtree of node.
- `limitTabRange(node: HTMLElement, e: Event): void`
```jsx|pure
import focus from 'rc-util/lib/Dom/focus';
```
#### Dom/support
> { animation: boolean | Object, transition: boolean | Object }
A flag to tell whether current environment supports `animationend` or `transitionend`.
```jsx|pure
import support from 'rc-util/lib/Dom/support';
```
### KeyCode
> Enum
Enum of KeyCode, please check the [definition](https://github.com/react-component/util/blob/master/src/KeyCode.ts) of it.
```jsx|pure
import KeyCode from 'rc-util/lib/KeyCode';
```
#### KeyCode.isTextModifyingKeyEvent
> (e: Event): boolean
Whether text and modified key is entered at the same time.
#### KeyCode.isCharacterKey
> (keyCode: KeyCode): boolean
Whether character is entered.
### ScrollLocker
> ScrollLocker<{lock: (options: {container: HTMLElement}) => void, unLock: () => void}>
improve shake when page scroll bar hidden.
`ScrollLocker` change body style, and add a class `ant-scrolling-effect` when called, so if you page look abnormal, please check this;
```js
import ScrollLocker from 'rc-util/lib/Dom/scrollLocker';
const scrollLocker = new ScrollLocker();
// lock
scrollLocker.lock()
// unLock
scrollLocker.unLock()
```
## License
[MIT](/LICENSE)
@@ -0,0 +1,5 @@
import React from 'react';
export interface Option {
keepEmpty?: boolean;
}
export default function toArray(children: React.ReactNode, option?: Option): React.ReactElement[];
+18
View File
@@ -0,0 +1,18 @@
import isFragment from "../React/isFragment";
import React from 'react';
export default function toArray(children, option = {}) {
let ret = [];
React.Children.forEach(children, child => {
if ((child === undefined || child === null) && !option.keepEmpty) {
return;
}
if (Array.isArray(child)) {
ret = ret.concat(toArray(child));
} else if (isFragment(child) && child.props) {
ret = ret.concat(toArray(child.props.children, option));
} else {
ret.push(child);
}
});
return ret;
}
@@ -0,0 +1 @@
export default function canUseDom(): boolean;
+3
View File
@@ -0,0 +1,3 @@
export default function canUseDom() {
return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
}
+1
View File
@@ -0,0 +1 @@
export default function contains(root: Node | null | undefined, n?: Node): boolean;
+20
View File
@@ -0,0 +1,20 @@
export default function contains(root, n) {
if (!root) {
return false;
}
// Use native if support
if (root.contains) {
return root.contains(n);
}
// `document.contains` not support with IE11
let node = n;
while (node) {
if (node === root) {
return true;
}
node = node.parentNode;
}
return false;
}
+25
View File
@@ -0,0 +1,25 @@
export type ContainerType = Element | ShadowRoot;
export type Prepend = boolean | 'queue';
export type AppendType = 'prependQueue' | 'append' | 'prepend';
interface Options {
attachTo?: ContainerType;
csp?: {
nonce?: string;
};
prepend?: Prepend;
/**
* Config the `priority` of `prependQueue`. Default is `0`.
* It's useful if you need to insert style before other style.
*/
priority?: number;
mark?: string;
styles?: HTMLElement[];
}
export declare function injectCSS(css: string, option?: Options): HTMLStyleElement;
export declare function removeCSS(key: string, option?: Options): void;
/**
* manually clear container cache to avoid global cache in unit testes
*/
export declare function clearContainerCache(): void;
export declare function updateCSS(css: string, key: string, originOption?: Options): HTMLElement;
export {};
+146
View File
@@ -0,0 +1,146 @@
import canUseDom from "./canUseDom";
import contains from "./contains";
const APPEND_ORDER = 'data-rc-order';
const APPEND_PRIORITY = 'data-rc-priority';
const MARK_KEY = `rc-util-key`;
const containerCache = new Map();
function getMark({
mark
} = {}) {
if (mark) {
return mark.startsWith('data-') ? mark : `data-${mark}`;
}
return MARK_KEY;
}
function getContainer(option) {
if (option.attachTo) {
return option.attachTo;
}
const head = document.querySelector('head');
return head || document.body;
}
function getOrder(prepend) {
if (prepend === 'queue') {
return 'prependQueue';
}
return prepend ? 'prepend' : 'append';
}
/**
* Find style which inject by rc-util
*/
function findStyles(container) {
return Array.from((containerCache.get(container) || container).children).filter(node => node.tagName === 'STYLE');
}
export function injectCSS(css, option = {}) {
if (!canUseDom()) {
return null;
}
const {
csp,
prepend,
priority = 0
} = option;
const mergedOrder = getOrder(prepend);
const isPrependQueue = mergedOrder === 'prependQueue';
const styleNode = document.createElement('style');
styleNode.setAttribute(APPEND_ORDER, mergedOrder);
if (isPrependQueue && priority) {
styleNode.setAttribute(APPEND_PRIORITY, `${priority}`);
}
if (csp?.nonce) {
styleNode.nonce = csp?.nonce;
}
styleNode.innerHTML = css;
const container = getContainer(option);
const {
firstChild
} = container;
if (prepend) {
// If is queue `prepend`, it will prepend first style and then append rest style
if (isPrependQueue) {
const existStyle = (option.styles || findStyles(container)).filter(node => {
// Ignore style which not injected by rc-util with prepend
if (!['prepend', 'prependQueue'].includes(node.getAttribute(APPEND_ORDER))) {
return false;
}
// Ignore style which priority less then new style
const nodePriority = Number(node.getAttribute(APPEND_PRIORITY) || 0);
return priority >= nodePriority;
});
if (existStyle.length) {
container.insertBefore(styleNode, existStyle[existStyle.length - 1].nextSibling);
return styleNode;
}
}
// Use `insertBefore` as `prepend`
container.insertBefore(styleNode, firstChild);
} else {
container.appendChild(styleNode);
}
return styleNode;
}
function findExistNode(key, option = {}) {
let {
styles
} = option;
styles ||= findStyles(getContainer(option));
return styles.find(node => node.getAttribute(getMark(option)) === key);
}
export function removeCSS(key, option = {}) {
const existNode = findExistNode(key, option);
if (existNode) {
const container = getContainer(option);
container.removeChild(existNode);
}
}
/**
* qiankun will inject `appendChild` to insert into other
*/
function syncRealContainer(container, option) {
const cachedRealContainer = containerCache.get(container);
// Find real container when not cached or cached container removed
if (!cachedRealContainer || !contains(document, cachedRealContainer)) {
const placeholderStyle = injectCSS('', option);
const {
parentNode
} = placeholderStyle;
containerCache.set(container, parentNode);
container.removeChild(placeholderStyle);
}
}
/**
* manually clear container cache to avoid global cache in unit testes
*/
export function clearContainerCache() {
containerCache.clear();
}
export function updateCSS(css, key, originOption = {}) {
const container = getContainer(originOption);
const styles = findStyles(container);
const option = {
...originOption,
styles
};
// Sync real parent
syncRealContainer(container, option);
const existNode = findExistNode(key, option);
if (existNode) {
if (option.csp?.nonce && existNode.nonce !== option.csp?.nonce) {
existNode.nonce = option.csp?.nonce;
}
if (existNode.innerHTML !== css) {
existNode.innerHTML = css;
}
return existNode;
}
const newNode = injectCSS(css, option);
newNode.setAttribute(getMark(option), key);
return newNode;
}
@@ -0,0 +1,14 @@
import React from 'react';
export declare function isDOM(node: any): node is HTMLElement | SVGElement;
/**
* Retrieves a DOM node via a ref, and does not invoke `findDOMNode`.
*/
export declare function getDOM(node: any): HTMLElement | SVGElement | null;
/**
* Return if a node is a DOM node. Else will return by `findDOMNode`
*/
export default function findDOMNode<T = Element | Text>(node: React.ReactInstance | HTMLElement | SVGElement | {
nativeElement: T;
} | {
current: T;
}): T | null;
+35
View File
@@ -0,0 +1,35 @@
export function isDOM(node) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element
// Since XULElement is also subclass of Element, we only need HTMLElement and SVGElement
return node instanceof HTMLElement || node instanceof SVGElement;
}
/**
* Retrieves a DOM node via a ref, and does not invoke `findDOMNode`.
*/
export function getDOM(node) {
if (node && typeof node === 'object' && isDOM(node.nativeElement)) {
return node.nativeElement;
}
if (isDOM(node)) {
return node;
}
return null;
}
/**
* Return if a node is a DOM node. Else will return by `findDOMNode`
*/
export default function findDOMNode(node) {
const domNode = getDOM(node);
if (domNode) {
return domNode;
}
if (node && typeof node === 'object' && 'current' in node) {
const refDomNode = getDOM(node.current);
if (refDomNode) {
return refDomNode;
}
}
return null;
}
+21
View File
@@ -0,0 +1,21 @@
export declare function getFocusNodeList(node: HTMLElement, includePositive?: boolean): HTMLElement[];
export interface InputFocusOptions extends FocusOptions {
cursor?: 'start' | 'end' | 'all';
}
/**
* Focus element and set cursor position for input/textarea elements.
*/
export declare function triggerFocus(element?: HTMLElement, option?: InputFocusOptions): void;
/**
* Lock focus in the element.
* It will force back to the first focusable element when focus leaves the element.
* @param id - A stable ID for this lock instance
*/
export declare function lockFocus(element: HTMLElement, id: string): VoidFunction;
/**
* Lock focus within an element.
* When locked, focus will be restricted to focusable elements within the specified element.
* If multiple elements are locked, only the last locked element will be effective.
* @returns A function to mark an element as ignored, which will temporarily allow focus on that element even if it's outside the locked area.
*/
export declare function useLockFocus(lock: boolean, getElement: () => HTMLElement | null): [ignoreElement: (ele: HTMLElement) => void];
+228
View File
@@ -0,0 +1,228 @@
import { useEffect, useRef, useState } from 'react';
import isVisible from "./isVisible";
import useId from "../hooks/useId";
function focusable(node, includePositive = false) {
if (isVisible(node)) {
const nodeName = node.nodeName.toLowerCase();
const isFocusableElement =
// Focusable element
['input', 'select', 'textarea', 'button'].includes(nodeName) ||
// Editable element
node.isContentEditable ||
// Anchor with href element
nodeName === 'a' && !!node.getAttribute('href');
// Get tabIndex
const tabIndexAttr = node.getAttribute('tabindex');
const tabIndexNum = Number(tabIndexAttr);
// Parse as number if validate
let tabIndex = null;
if (tabIndexAttr && !Number.isNaN(tabIndexNum)) {
tabIndex = tabIndexNum;
} else if (isFocusableElement && tabIndex === null) {
tabIndex = 0;
}
// Block focusable if disabled
if (isFocusableElement && node.disabled) {
tabIndex = null;
}
return tabIndex !== null && (tabIndex >= 0 || includePositive && tabIndex < 0);
}
return false;
}
export function getFocusNodeList(node, includePositive = false) {
const res = [...node.querySelectorAll('*')].filter(child => {
return focusable(child, includePositive);
});
if (focusable(node, includePositive)) {
res.unshift(node);
}
return res;
}
// Used for `rc-input` `rc-textarea` `rc-input-number`
/**
* Focus element and set cursor position for input/textarea elements.
*/
export function triggerFocus(element, option) {
if (!element) return;
element.focus(option);
// Selection content
const {
cursor
} = option || {};
if (cursor && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
const len = element.value.length;
switch (cursor) {
case 'start':
element.setSelectionRange(0, 0);
break;
case 'end':
element.setSelectionRange(len, len);
break;
default:
element.setSelectionRange(0, len);
}
}
}
// ======================================================
// == Lock Focus ==
// ======================================================
let lastFocusElement = null;
let focusElements = [];
// Map stable ID to lock element
const idToElementMap = new Map();
// Map stable ID to ignored element
const ignoredElementMap = new Map();
function getLastElement() {
return focusElements[focusElements.length - 1];
}
function isIgnoredElement(element) {
const lastElement = getLastElement();
if (element && lastElement) {
// Find the ID that maps to the last element
let lockId;
for (const [id, ele] of idToElementMap.entries()) {
if (ele === lastElement) {
lockId = id;
break;
}
}
const ignoredEle = ignoredElementMap.get(lockId);
return !!ignoredEle && (ignoredEle === element || ignoredEle.contains(element));
}
return false;
}
function hasFocus(element) {
const {
activeElement
} = document;
return element === activeElement || element.contains(activeElement);
}
function syncFocus() {
const lastElement = getLastElement();
const {
activeElement
} = document;
// If current focus is on an ignored element, don't force it back
if (isIgnoredElement(activeElement)) {
return;
}
if (lastElement && !hasFocus(lastElement)) {
const focusableList = getFocusNodeList(lastElement);
const matchElement = focusableList.includes(lastFocusElement) ? lastFocusElement : focusableList[0];
matchElement?.focus({
preventScroll: true
});
} else {
lastFocusElement = activeElement;
}
}
function onWindowKeyDown(e) {
if (e.key === 'Tab') {
const {
activeElement
} = document;
const lastElement = getLastElement();
const focusableList = getFocusNodeList(lastElement);
const last = focusableList[focusableList.length - 1];
if (e.shiftKey && activeElement === focusableList[0]) {
// Tab backward on first focusable element
lastFocusElement = last;
} else if (!e.shiftKey && activeElement === last) {
// Tab forward on last focusable element
lastFocusElement = focusableList[0];
}
}
}
/**
* Lock focus in the element.
* It will force back to the first focusable element when focus leaves the element.
* @param id - A stable ID for this lock instance
*/
export function lockFocus(element, id) {
if (element) {
// Store the mapping between ID and element
idToElementMap.set(id, element);
// Refresh focus elements
focusElements = focusElements.filter(ele => ele !== element);
focusElements.push(element);
// Just add event since it will de-duplicate
window.addEventListener('focusin', syncFocus);
window.addEventListener('keydown', onWindowKeyDown, true);
syncFocus();
}
// Always return unregister function
return () => {
lastFocusElement = null;
focusElements = focusElements.filter(ele => ele !== element);
idToElementMap.delete(id);
ignoredElementMap.delete(id);
if (focusElements.length === 0) {
window.removeEventListener('focusin', syncFocus);
window.removeEventListener('keydown', onWindowKeyDown, true);
}
};
}
/**
* Retry an effect until it reports ready.
* When `ready` is `false`, it will schedule one more effect cycle and call `func` again
* with the next `retryTimes`.
*/
function useRetryEffect(func, deps) {
/* eslint-disable react-hooks/exhaustive-deps */
const retryTimesRef = useRef(0);
const [retryMark, setRetryMark] = useState(0);
useEffect(() => {
retryTimesRef.current = 0;
}, deps);
useEffect(() => {
const [clearFn, ready] = func(retryTimesRef.current);
if (!ready) {
retryTimesRef.current += 1;
setRetryMark(count => count + 1);
}
return clearFn;
}, [...deps, retryMark]);
/* eslint-enable react-hooks/exhaustive-deps */
}
/**
* Lock focus within an element.
* When locked, focus will be restricted to focusable elements within the specified element.
* If multiple elements are locked, only the last locked element will be effective.
* @returns A function to mark an element as ignored, which will temporarily allow focus on that element even if it's outside the locked area.
*/
export function useLockFocus(lock, getElement) {
const id = useId();
const getElementRef = useRef(getElement);
getElementRef.current = getElement;
const lockEffect = retryTimes => {
if (!lock) {
return [undefined, true];
}
const element = getElementRef.current();
if (element) {
return [lockFocus(element, id), true];
}
return [undefined, retryTimes >= 1];
};
useRetryEffect(lockEffect, [id, lock]);
const ignoreElement = ele => {
if (ele) {
// Set the ignored element using stable ID
ignoredElementMap.set(id, ele);
}
};
return [ignoreElement];
}
@@ -0,0 +1,2 @@
declare const _default: (element: Element) => boolean;
export default _default;
+29
View File
@@ -0,0 +1,29 @@
export default (element => {
if (!element) {
return false;
}
if (element instanceof Element) {
if (element.offsetParent) {
return true;
}
if (element.getBBox) {
const {
width,
height
} = element.getBBox();
if (width || height) {
return true;
}
}
if (element.getBoundingClientRect) {
const {
width,
height
} = element.getBoundingClientRect();
if (width || height) {
return true;
}
}
}
return false;
});
@@ -0,0 +1,12 @@
export interface scrollLockOptions {
container: HTMLElement;
}
export default class ScrollLocker {
private lockTarget;
private options;
constructor(options?: scrollLockOptions);
getContainer: () => HTMLElement | undefined;
reLock: (options?: scrollLockOptions) => void;
lock: () => void;
unLock: () => void;
}
+109
View File
@@ -0,0 +1,109 @@
import getScrollBarSize from "../getScrollBarSize";
import setStyle from "../setStyle";
let uuid = 0;
let locks = [];
const scrollingEffectClassName = 'ant-scrolling-effect';
const scrollingEffectClassNameReg = new RegExp(`${scrollingEffectClassName}`, 'g');
// https://github.com/ant-design/ant-design/issues/19340
// https://github.com/ant-design/ant-design/issues/19332
const cacheStyle = new Map();
export default class ScrollLocker {
lockTarget;
options;
constructor(options) {
// eslint-disable-next-line no-plusplus
this.lockTarget = uuid++;
this.options = options;
}
getContainer = () => {
return this.options?.container;
};
// if options change...
reLock = options => {
const findLock = locks.find(({
target
}) => target === this.lockTarget);
if (findLock) {
this.unLock();
}
this.options = options;
if (findLock) {
findLock.options = options;
this.lock();
}
};
lock = () => {
// If lockTarget exist return
if (locks.some(({
target
}) => target === this.lockTarget)) {
return;
}
// If same container effect, return
if (locks.some(({
options
}) => options?.container === this.options?.container)) {
locks = [...locks, {
target: this.lockTarget,
options: this.options
}];
return;
}
let scrollBarSize = 0;
const container = this.options?.container || document.body;
if (container === document.body && window.innerWidth - document.documentElement.clientWidth > 0 || container.scrollHeight > container.clientHeight) {
if (getComputedStyle(container).overflow !== 'hidden') {
scrollBarSize = getScrollBarSize();
}
}
const containerClassName = container.className;
if (locks.filter(({
options
}) => options?.container === this.options?.container).length === 0) {
cacheStyle.set(container, setStyle({
width: scrollBarSize !== 0 ? `calc(100% - ${scrollBarSize}px)` : undefined,
overflow: 'hidden',
overflowX: 'hidden',
overflowY: 'hidden'
}, {
element: container
}));
}
// https://github.com/ant-design/ant-design/issues/19729
if (!scrollingEffectClassNameReg.test(containerClassName)) {
const addClassName = `${containerClassName} ${scrollingEffectClassName}`;
container.className = addClassName.trim();
}
locks = [...locks, {
target: this.lockTarget,
options: this.options
}];
};
unLock = () => {
const findLock = locks.find(({
target
}) => target === this.lockTarget);
locks = locks.filter(({
target
}) => target !== this.lockTarget);
if (!findLock || locks.some(({
options
}) => options?.container === findLock.options?.container)) {
return;
}
// Remove Effect
const container = this.options?.container || document.body;
const containerClassName = container.className;
if (!scrollingEffectClassNameReg.test(containerClassName)) return;
setStyle(cacheStyle.get(container), {
element: container
});
cacheStyle.delete(container);
container.className = container.className.replace(scrollingEffectClassNameReg, '').trim();
};
}
+8
View File
@@ -0,0 +1,8 @@
/**
* Check if is in shadowRoot
*/
export declare function inShadow(ele: Node): boolean;
/**
* Return shadowRoot if possible
*/
export declare function getShadowRoot(ele: Node): ShadowRoot;
+17
View File
@@ -0,0 +1,17 @@
function getRoot(ele) {
return ele?.getRootNode?.();
}
/**
* Check if is in shadowRoot
*/
export function inShadow(ele) {
return getRoot(ele) instanceof ShadowRoot;
}
/**
* Return shadowRoot if possible
*/
export function getShadowRoot(ele) {
return inShadow(ele) ? getRoot(ele) : null;
}
@@ -0,0 +1,2 @@
export declare function isStyleSupport(styleName: string | string[]): boolean;
export declare function isStyleSupport(styleName: string, styleValue: any): boolean;
+26
View File
@@ -0,0 +1,26 @@
import canUseDom from "./canUseDom";
const isStyleNameSupport = styleName => {
if (canUseDom() && window.document.documentElement) {
const styleNameList = Array.isArray(styleName) ? styleName : [styleName];
const {
documentElement
} = window.document;
return styleNameList.some(name => name in documentElement.style);
}
return false;
};
const isStyleValueSupport = (styleName, value) => {
if (!isStyleNameSupport(styleName)) {
return false;
}
const ele = document.createElement('div');
const origin = ele.style[styleName];
ele.style[styleName] = value;
return ele.style[styleName] !== origin;
};
export function isStyleSupport(styleName, styleValue) {
if (!Array.isArray(styleName) && styleValue !== undefined) {
return isStyleValueSupport(styleName, styleValue);
}
return isStyleNameSupport(styleName);
}
+437
View File
@@ -0,0 +1,437 @@
/**
* @ignore
* some key-codes definition and utils from closure-library
* @author yiminghe@gmail.com
*/
declare const KeyCode: {
/**
* MAC_ENTER
*/
MAC_ENTER: number;
/**
* BACKSPACE
*/
BACKSPACE: number;
/**
* TAB
*/
TAB: number;
/**
* NUMLOCK on FF/Safari Mac
*/
NUM_CENTER: number;
/**
* ENTER
*/
ENTER: number;
/**
* SHIFT
*/
SHIFT: number;
/**
* CTRL
*/
CTRL: number;
/**
* ALT
*/
ALT: number;
/**
* PAUSE
*/
PAUSE: number;
/**
* CAPS_LOCK
*/
CAPS_LOCK: number;
/**
* ESC
*/
ESC: number;
/**
* SPACE
*/
SPACE: number;
/**
* PAGE_UP
*/
PAGE_UP: number;
/**
* PAGE_DOWN
*/
PAGE_DOWN: number;
/**
* END
*/
END: number;
/**
* HOME
*/
HOME: number;
/**
* LEFT
*/
LEFT: number;
/**
* UP
*/
UP: number;
/**
* RIGHT
*/
RIGHT: number;
/**
* DOWN
*/
DOWN: number;
/**
* PRINT_SCREEN
*/
PRINT_SCREEN: number;
/**
* INSERT
*/
INSERT: number;
/**
* DELETE
*/
DELETE: number;
/**
* ZERO
*/
ZERO: number;
/**
* ONE
*/
ONE: number;
/**
* TWO
*/
TWO: number;
/**
* THREE
*/
THREE: number;
/**
* FOUR
*/
FOUR: number;
/**
* FIVE
*/
FIVE: number;
/**
* SIX
*/
SIX: number;
/**
* SEVEN
*/
SEVEN: number;
/**
* EIGHT
*/
EIGHT: number;
/**
* NINE
*/
NINE: number;
/**
* QUESTION_MARK
*/
QUESTION_MARK: number;
/**
* A
*/
A: number;
/**
* B
*/
B: number;
/**
* C
*/
C: number;
/**
* D
*/
D: number;
/**
* E
*/
E: number;
/**
* F
*/
F: number;
/**
* G
*/
G: number;
/**
* H
*/
H: number;
/**
* I
*/
I: number;
/**
* J
*/
J: number;
/**
* K
*/
K: number;
/**
* L
*/
L: number;
/**
* M
*/
M: number;
/**
* N
*/
N: number;
/**
* O
*/
O: number;
/**
* P
*/
P: number;
/**
* Q
*/
Q: number;
/**
* R
*/
R: number;
/**
* S
*/
S: number;
/**
* T
*/
T: number;
/**
* U
*/
U: number;
/**
* V
*/
V: number;
/**
* W
*/
W: number;
/**
* X
*/
X: number;
/**
* Y
*/
Y: number;
/**
* Z
*/
Z: number;
/**
* META
*/
META: number;
/**
* WIN_KEY_RIGHT
*/
WIN_KEY_RIGHT: number;
/**
* CONTEXT_MENU
*/
CONTEXT_MENU: number;
/**
* NUM_ZERO
*/
NUM_ZERO: number;
/**
* NUM_ONE
*/
NUM_ONE: number;
/**
* NUM_TWO
*/
NUM_TWO: number;
/**
* NUM_THREE
*/
NUM_THREE: number;
/**
* NUM_FOUR
*/
NUM_FOUR: number;
/**
* NUM_FIVE
*/
NUM_FIVE: number;
/**
* NUM_SIX
*/
NUM_SIX: number;
/**
* NUM_SEVEN
*/
NUM_SEVEN: number;
/**
* NUM_EIGHT
*/
NUM_EIGHT: number;
/**
* NUM_NINE
*/
NUM_NINE: number;
/**
* NUM_MULTIPLY
*/
NUM_MULTIPLY: number;
/**
* NUM_PLUS
*/
NUM_PLUS: number;
/**
* NUM_MINUS
*/
NUM_MINUS: number;
/**
* NUM_PERIOD
*/
NUM_PERIOD: number;
/**
* NUM_DIVISION
*/
NUM_DIVISION: number;
/**
* F1
*/
F1: number;
/**
* F2
*/
F2: number;
/**
* F3
*/
F3: number;
/**
* F4
*/
F4: number;
/**
* F5
*/
F5: number;
/**
* F6
*/
F6: number;
/**
* F7
*/
F7: number;
/**
* F8
*/
F8: number;
/**
* F9
*/
F9: number;
/**
* F10
*/
F10: number;
/**
* F11
*/
F11: number;
/**
* F12
*/
F12: number;
/**
* NUMLOCK
*/
NUMLOCK: number;
/**
* SEMICOLON
*/
SEMICOLON: number;
/**
* DASH
*/
DASH: number;
/**
* EQUALS
*/
EQUALS: number;
/**
* COMMA
*/
COMMA: number;
/**
* PERIOD
*/
PERIOD: number;
/**
* SLASH
*/
SLASH: number;
/**
* APOSTROPHE
*/
APOSTROPHE: number;
/**
* SINGLE_QUOTE
*/
SINGLE_QUOTE: number;
/**
* OPEN_SQUARE_BRACKET
*/
OPEN_SQUARE_BRACKET: number;
/**
* BACKSLASH
*/
BACKSLASH: number;
/**
* CLOSE_SQUARE_BRACKET
*/
CLOSE_SQUARE_BRACKET: number;
/**
* WIN_KEY
*/
WIN_KEY: number;
/**
* MAC_FF_META
*/
MAC_FF_META: number;
/**
* WIN_IME
*/
WIN_IME: number;
/**
* whether text and modified key is entered at the same time.
*/
isTextModifyingKeyEvent: (e: KeyboardEvent) => boolean;
/**
* whether character is entered.
*/
isCharacterKey: (keyCode: number) => boolean;
isEditableTarget: (e: KeyboardEvent) => boolean;
};
export default KeyCode;
+551
View File
@@ -0,0 +1,551 @@
/**
* @ignore
* some key-codes definition and utils from closure-library
* @author yiminghe@gmail.com
*/
const KeyCode = {
/**
* MAC_ENTER
*/
MAC_ENTER: 3,
/**
* BACKSPACE
*/
BACKSPACE: 8,
/**
* TAB
*/
TAB: 9,
/**
* NUMLOCK on FF/Safari Mac
*/
NUM_CENTER: 12,
// NUMLOCK on FF/Safari Mac
/**
* ENTER
*/
ENTER: 13,
/**
* SHIFT
*/
SHIFT: 16,
/**
* CTRL
*/
CTRL: 17,
/**
* ALT
*/
ALT: 18,
/**
* PAUSE
*/
PAUSE: 19,
/**
* CAPS_LOCK
*/
CAPS_LOCK: 20,
/**
* ESC
*/
ESC: 27,
/**
* SPACE
*/
SPACE: 32,
/**
* PAGE_UP
*/
PAGE_UP: 33,
// also NUM_NORTH_EAST
/**
* PAGE_DOWN
*/
PAGE_DOWN: 34,
// also NUM_SOUTH_EAST
/**
* END
*/
END: 35,
// also NUM_SOUTH_WEST
/**
* HOME
*/
HOME: 36,
// also NUM_NORTH_WEST
/**
* LEFT
*/
LEFT: 37,
// also NUM_WEST
/**
* UP
*/
UP: 38,
// also NUM_NORTH
/**
* RIGHT
*/
RIGHT: 39,
// also NUM_EAST
/**
* DOWN
*/
DOWN: 40,
// also NUM_SOUTH
/**
* PRINT_SCREEN
*/
PRINT_SCREEN: 44,
/**
* INSERT
*/
INSERT: 45,
// also NUM_INSERT
/**
* DELETE
*/
DELETE: 46,
// also NUM_DELETE
/**
* ZERO
*/
ZERO: 48,
/**
* ONE
*/
ONE: 49,
/**
* TWO
*/
TWO: 50,
/**
* THREE
*/
THREE: 51,
/**
* FOUR
*/
FOUR: 52,
/**
* FIVE
*/
FIVE: 53,
/**
* SIX
*/
SIX: 54,
/**
* SEVEN
*/
SEVEN: 55,
/**
* EIGHT
*/
EIGHT: 56,
/**
* NINE
*/
NINE: 57,
/**
* QUESTION_MARK
*/
QUESTION_MARK: 63,
// needs localization
/**
* A
*/
A: 65,
/**
* B
*/
B: 66,
/**
* C
*/
C: 67,
/**
* D
*/
D: 68,
/**
* E
*/
E: 69,
/**
* F
*/
F: 70,
/**
* G
*/
G: 71,
/**
* H
*/
H: 72,
/**
* I
*/
I: 73,
/**
* J
*/
J: 74,
/**
* K
*/
K: 75,
/**
* L
*/
L: 76,
/**
* M
*/
M: 77,
/**
* N
*/
N: 78,
/**
* O
*/
O: 79,
/**
* P
*/
P: 80,
/**
* Q
*/
Q: 81,
/**
* R
*/
R: 82,
/**
* S
*/
S: 83,
/**
* T
*/
T: 84,
/**
* U
*/
U: 85,
/**
* V
*/
V: 86,
/**
* W
*/
W: 87,
/**
* X
*/
X: 88,
/**
* Y
*/
Y: 89,
/**
* Z
*/
Z: 90,
/**
* META
*/
META: 91,
// WIN_KEY_LEFT
/**
* WIN_KEY_RIGHT
*/
WIN_KEY_RIGHT: 92,
/**
* CONTEXT_MENU
*/
CONTEXT_MENU: 93,
/**
* NUM_ZERO
*/
NUM_ZERO: 96,
/**
* NUM_ONE
*/
NUM_ONE: 97,
/**
* NUM_TWO
*/
NUM_TWO: 98,
/**
* NUM_THREE
*/
NUM_THREE: 99,
/**
* NUM_FOUR
*/
NUM_FOUR: 100,
/**
* NUM_FIVE
*/
NUM_FIVE: 101,
/**
* NUM_SIX
*/
NUM_SIX: 102,
/**
* NUM_SEVEN
*/
NUM_SEVEN: 103,
/**
* NUM_EIGHT
*/
NUM_EIGHT: 104,
/**
* NUM_NINE
*/
NUM_NINE: 105,
/**
* NUM_MULTIPLY
*/
NUM_MULTIPLY: 106,
/**
* NUM_PLUS
*/
NUM_PLUS: 107,
/**
* NUM_MINUS
*/
NUM_MINUS: 109,
/**
* NUM_PERIOD
*/
NUM_PERIOD: 110,
/**
* NUM_DIVISION
*/
NUM_DIVISION: 111,
/**
* F1
*/
F1: 112,
/**
* F2
*/
F2: 113,
/**
* F3
*/
F3: 114,
/**
* F4
*/
F4: 115,
/**
* F5
*/
F5: 116,
/**
* F6
*/
F6: 117,
/**
* F7
*/
F7: 118,
/**
* F8
*/
F8: 119,
/**
* F9
*/
F9: 120,
/**
* F10
*/
F10: 121,
/**
* F11
*/
F11: 122,
/**
* F12
*/
F12: 123,
/**
* NUMLOCK
*/
NUMLOCK: 144,
/**
* SEMICOLON
*/
SEMICOLON: 186,
// needs localization
/**
* DASH
*/
DASH: 189,
// needs localization
/**
* EQUALS
*/
EQUALS: 187,
// needs localization
/**
* COMMA
*/
COMMA: 188,
// needs localization
/**
* PERIOD
*/
PERIOD: 190,
// needs localization
/**
* SLASH
*/
SLASH: 191,
// needs localization
/**
* APOSTROPHE
*/
APOSTROPHE: 192,
// needs localization
/**
* SINGLE_QUOTE
*/
SINGLE_QUOTE: 222,
// needs localization
/**
* OPEN_SQUARE_BRACKET
*/
OPEN_SQUARE_BRACKET: 219,
// needs localization
/**
* BACKSLASH
*/
BACKSLASH: 220,
// needs localization
/**
* CLOSE_SQUARE_BRACKET
*/
CLOSE_SQUARE_BRACKET: 221,
// needs localization
/**
* WIN_KEY
*/
WIN_KEY: 224,
/**
* MAC_FF_META
*/
MAC_FF_META: 224,
// Firefox (Gecko) fires this for the meta key instead of 91
/**
* WIN_IME
*/
WIN_IME: 229,
// ======================== Function ========================
/**
* whether text and modified key is entered at the same time.
*/
isTextModifyingKeyEvent: function isTextModifyingKeyEvent(e) {
const {
keyCode
} = e;
if (e.altKey && !e.ctrlKey || e.metaKey ||
// Function keys don't generate text
keyCode >= KeyCode.F1 && keyCode <= KeyCode.F12) {
return false;
}
// The following keys are quite harmless, even in combination with
// CTRL, ALT or SHIFT.
switch (keyCode) {
case KeyCode.ALT:
case KeyCode.CAPS_LOCK:
case KeyCode.CONTEXT_MENU:
case KeyCode.CTRL:
case KeyCode.DOWN:
case KeyCode.END:
case KeyCode.ESC:
case KeyCode.HOME:
case KeyCode.INSERT:
case KeyCode.LEFT:
case KeyCode.MAC_FF_META:
case KeyCode.META:
case KeyCode.NUMLOCK:
case KeyCode.NUM_CENTER:
case KeyCode.PAGE_DOWN:
case KeyCode.PAGE_UP:
case KeyCode.PAUSE:
case KeyCode.PRINT_SCREEN:
case KeyCode.RIGHT:
case KeyCode.SHIFT:
case KeyCode.UP:
case KeyCode.WIN_KEY:
case KeyCode.WIN_KEY_RIGHT:
return false;
default:
return true;
}
},
/**
* whether character is entered.
*/
isCharacterKey: function isCharacterKey(keyCode) {
if (keyCode >= KeyCode.ZERO && keyCode <= KeyCode.NINE) {
return true;
}
if (keyCode >= KeyCode.NUM_ZERO && keyCode <= KeyCode.NUM_MULTIPLY) {
return true;
}
if (keyCode >= KeyCode.A && keyCode <= KeyCode.Z) {
return true;
}
// Safari sends zero key code for non-latin characters.
if (window.navigator.userAgent.indexOf('WebKit') !== -1 && keyCode === 0) {
return true;
}
switch (keyCode) {
case KeyCode.SPACE:
case KeyCode.QUESTION_MARK:
case KeyCode.NUM_PLUS:
case KeyCode.NUM_MINUS:
case KeyCode.NUM_PERIOD:
case KeyCode.NUM_DIVISION:
case KeyCode.SEMICOLON:
case KeyCode.DASH:
case KeyCode.EQUALS:
case KeyCode.COMMA:
case KeyCode.PERIOD:
case KeyCode.SLASH:
case KeyCode.APOSTROPHE:
case KeyCode.SINGLE_QUOTE:
case KeyCode.OPEN_SQUARE_BRACKET:
case KeyCode.BACKSLASH:
case KeyCode.CLOSE_SQUARE_BRACKET:
return true;
default:
return false;
}
},
isEditableTarget: function isEditableTarget(e) {
const target = e.target;
if (!(target instanceof HTMLElement)) {
return false;
}
const tagName = target.tagName;
if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT' || target.isContentEditable) {
return true;
}
return false;
}
};
export default KeyCode;
+9
View File
@@ -0,0 +1,9 @@
import type * as React from 'react';
export type PortalRef = {};
export interface PortalProps {
didUpdate?: (prevProps: PortalProps) => void;
getContainer: () => HTMLElement;
children?: React.ReactNode;
}
declare const Portal: React.ForwardRefExoticComponent<PortalProps & React.RefAttributes<PortalRef>>;
export default Portal;
+43
View File
@@ -0,0 +1,43 @@
import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';
import canUseDom from "./Dom/canUseDom";
const Portal = /*#__PURE__*/forwardRef((props, ref) => {
const {
didUpdate,
getContainer,
children
} = props;
const parentRef = useRef(null);
const containerRef = useRef(null);
// Ref return nothing, only for wrapper check exist
useImperativeHandle(ref, () => ({}));
// Create container in client side with sync to avoid useEffect not get ref
const initRef = useRef(false);
if (!initRef.current && canUseDom()) {
containerRef.current = getContainer();
parentRef.current = containerRef.current.parentNode;
initRef.current = true;
}
// [Legacy] Used by `rc-trigger`
useEffect(() => {
didUpdate?.(props);
});
useEffect(() => {
// Restore container to original place
// React 18 StrictMode will unmount first and mount back for effect test:
// https://reactjs.org/blog/2022/03/29/react-v18.html#new-strict-mode-behaviors
if (containerRef.current.parentNode === null && parentRef.current !== null) {
parentRef.current.appendChild(containerRef.current);
}
return () => {
// [Legacy] This should not be handle by Portal but parent PortalWrapper instead.
// Since some component use `Portal` directly, we have to keep the logic here.
containerRef.current?.parentNode?.removeChild(containerRef.current);
};
}, []);
return containerRef.current ? /*#__PURE__*/ReactDOM.createPortal(children, containerRef.current) : null;
});
export default Portal;
+51
View File
@@ -0,0 +1,51 @@
import * as React from 'react';
import type { PortalRef } from './Portal';
import ScrollLocker from './Dom/scrollLocker';
/** @private Test usage only */
export declare function getOpenCount(): number;
export type GetContainer = string | HTMLElement | (() => HTMLElement);
export interface PortalWrapperProps {
visible?: boolean;
getContainer?: GetContainer;
wrapperClassName?: string;
forceRender?: boolean;
children: (info: {
getOpenCount: () => number;
getContainer: () => HTMLElement;
switchScrollingEffect: () => void;
scrollLocker: ScrollLocker;
ref?: (c: any) => void;
}) => React.ReactNode;
}
declare class PortalWrapper extends React.Component<PortalWrapperProps> {
container?: HTMLElement;
componentRef: React.RefObject<PortalRef>;
rafId?: number;
scrollLocker: ScrollLocker;
constructor(props: PortalWrapperProps);
renderComponent?: (info: {
afterClose: (...params: any[]) => void;
onClose: (...params: any[]) => void;
visible: boolean;
}) => void;
componentDidMount(): void;
componentDidUpdate(prevProps: PortalWrapperProps): void;
updateScrollLocker: (prevProps?: Partial<PortalWrapperProps>) => void;
updateOpenCount: (prevProps?: Partial<PortalWrapperProps>) => void;
componentWillUnmount(): void;
attachToParent: (force?: boolean) => boolean;
getContainer: () => HTMLElement;
setWrapperClassName: () => void;
removeCurrentContainer: () => void;
/**
* Enhance ./switchScrollingEffect
* 1. Simulate document body scroll bar with
* 2. Record body has overflow style and recover when all of PortalWrapper invisible
* 3. Disable body scroll when PortalWrapper has open
*
* @memberof PortalWrapper
*/
switchScrollingEffect: () => void;
render(): any;
}
export default PortalWrapper;
+192
View File
@@ -0,0 +1,192 @@
/* eslint-disable no-underscore-dangle,react/require-default-props */
import * as React from 'react';
import raf from "./raf";
import Portal from "./Portal";
import canUseDom from "./Dom/canUseDom";
import setStyle from "./setStyle";
import ScrollLocker from "./Dom/scrollLocker";
let openCount = 0;
const supportDom = canUseDom();
/** @private Test usage only */
export function getOpenCount() {
return process.env.NODE_ENV === 'test' ? openCount : 0;
}
// https://github.com/ant-design/ant-design/issues/19340
// https://github.com/ant-design/ant-design/issues/19332
let cacheOverflow = {};
const getParent = getContainer => {
if (!supportDom) {
return null;
}
if (getContainer) {
if (typeof getContainer === 'string') {
return document.querySelectorAll(getContainer)[0];
}
if (typeof getContainer === 'function') {
return getContainer();
}
if (typeof getContainer === 'object' && getContainer instanceof window.HTMLElement) {
return getContainer;
}
}
return document.body;
};
class PortalWrapper extends React.Component {
container;
componentRef = /*#__PURE__*/React.createRef();
rafId;
scrollLocker;
constructor(props) {
super(props);
this.scrollLocker = new ScrollLocker({
container: getParent(props.getContainer)
});
}
renderComponent;
componentDidMount() {
this.updateOpenCount();
if (!this.attachToParent()) {
this.rafId = raf(() => {
this.forceUpdate();
});
}
}
componentDidUpdate(prevProps) {
this.updateOpenCount(prevProps);
this.updateScrollLocker(prevProps);
this.setWrapperClassName();
this.attachToParent();
}
updateScrollLocker = prevProps => {
const {
visible: prevVisible
} = prevProps || {};
const {
getContainer,
visible
} = this.props;
if (visible && visible !== prevVisible && supportDom && getParent(getContainer) !== this.scrollLocker.getContainer()) {
this.scrollLocker.reLock({
container: getParent(getContainer)
});
}
};
updateOpenCount = prevProps => {
const {
visible: prevVisible,
getContainer: prevGetContainer
} = prevProps || {};
const {
visible,
getContainer
} = this.props;
// Update count
if (visible !== prevVisible && supportDom && getParent(getContainer) === document.body) {
if (visible && !prevVisible) {
openCount += 1;
} else if (prevProps) {
openCount -= 1;
}
}
// Clean up container if needed
const getContainerIsFunc = typeof getContainer === 'function' && typeof prevGetContainer === 'function';
if (getContainerIsFunc ? getContainer.toString() !== prevGetContainer.toString() : getContainer !== prevGetContainer) {
this.removeCurrentContainer();
}
};
componentWillUnmount() {
const {
visible,
getContainer
} = this.props;
if (supportDom && getParent(getContainer) === document.body) {
// 离开时不会 render, 导到离开时数值不变,改用 func 。。
openCount = visible && openCount ? openCount - 1 : openCount;
}
this.removeCurrentContainer();
raf.cancel(this.rafId);
}
attachToParent = (force = false) => {
if (force || this.container && !this.container.parentNode) {
const parent = getParent(this.props.getContainer);
if (parent) {
parent.appendChild(this.container);
return true;
}
return false;
}
return true;
};
getContainer = () => {
if (!supportDom) {
return null;
}
if (!this.container) {
this.container = document.createElement('div');
this.attachToParent(true);
}
this.setWrapperClassName();
return this.container;
};
setWrapperClassName = () => {
const {
wrapperClassName
} = this.props;
if (this.container && wrapperClassName && wrapperClassName !== this.container.className) {
this.container.className = wrapperClassName;
}
};
removeCurrentContainer = () => {
// Portal will remove from `parentNode`.
// Let's handle this again to avoid refactor issue.
this.container?.parentNode?.removeChild(this.container);
};
/**
* Enhance ./switchScrollingEffect
* 1. Simulate document body scroll bar with
* 2. Record body has overflow style and recover when all of PortalWrapper invisible
* 3. Disable body scroll when PortalWrapper has open
*
* @memberof PortalWrapper
*/
switchScrollingEffect = () => {
if (openCount === 1 && !Object.keys(cacheOverflow).length) {
// Must be set after switchScrollingEffect
cacheOverflow = setStyle({
overflow: 'hidden',
overflowX: 'hidden',
overflowY: 'hidden'
});
} else if (!openCount) {
setStyle(cacheOverflow);
cacheOverflow = {};
}
};
render() {
const {
children,
forceRender,
visible
} = this.props;
let portal = null;
const childProps = {
getOpenCount: () => openCount,
getContainer: this.getContainer,
switchScrollingEffect: this.switchScrollingEffect,
scrollLocker: this.scrollLocker
};
if (forceRender || visible || this.componentRef.current) {
portal = /*#__PURE__*/React.createElement(Portal, {
getContainer: this.getContainer,
ref: this.componentRef
}, children(childProps));
}
return portal;
}
}
export default PortalWrapper;
@@ -0,0 +1,4 @@
/**
* Compatible with React 18 or 19 to check if node is a Fragment.
*/
export default function isFragment(object: any): boolean;
+17
View File
@@ -0,0 +1,17 @@
const REACT_ELEMENT_TYPE_18 = Symbol.for('react.element');
const REACT_ELEMENT_TYPE_19 = Symbol.for('react.transitional.element');
const REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');
/**
* Compatible with React 18 or 19 to check if node is a Fragment.
*/
export default function isFragment(object) {
return (
// Base object type
object && typeof object === 'object' && (
// React Element type
object.$$typeof === REACT_ELEMENT_TYPE_18 || object.$$typeof === REACT_ELEMENT_TYPE_19) &&
// React Fragment type
object.type === REACT_FRAGMENT_TYPE
);
}
+9
View File
@@ -0,0 +1,9 @@
import type * as React from 'react';
import type { Root } from 'react-dom/client';
declare const MARK = "__rc_react_root__";
type ContainerType = (Element | DocumentFragment) & {
[MARK]?: Root;
};
export declare function render(node: React.ReactElement, container: ContainerType): void;
export declare function unmount(container: ContainerType): Promise<void>;
export {};
+19
View File
@@ -0,0 +1,19 @@
import { createRoot } from 'react-dom/client';
const MARK = '__rc_react_root__';
// ========================== Render ==========================
export function render(node, container) {
const root = container[MARK] || createRoot(container);
root.render(node);
container[MARK] = root;
}
// ========================= Unmount ==========================
export async function unmount(container) {
// Delay to unmount to avoid React 18 sync warning
return Promise.resolve().then(() => {
container[MARK]?.unmount();
delete container[MARK];
});
}
+2
View File
@@ -0,0 +1,2 @@
declare function composeProps<T extends Record<string, any>>(originProps: T, patchProps: Partial<T>, isAll?: boolean): T;
export default composeProps;
+17
View File
@@ -0,0 +1,17 @@
function composeProps(originProps, patchProps, isAll) {
const composedProps = {
...originProps,
...(isAll ? patchProps : {})
};
Object.keys(patchProps).forEach(key => {
const func = patchProps[key];
if (typeof func === 'function') {
composedProps[key] = (...args) => {
func(...args);
return originProps[key]?.(...args);
};
}
});
return composedProps;
}
export default composeProps;
@@ -0,0 +1,7 @@
type ScrollBarSize = {
width: number;
height: number;
};
export default function getScrollBarSize(fresh?: boolean): number;
export declare function getTargetScrollBarSize(target: HTMLElement): ScrollBarSize;
export {};
+80
View File
@@ -0,0 +1,80 @@
/* eslint-disable no-param-reassign */
import { removeCSS, updateCSS } from "./Dom/dynamicCSS";
let cached;
function measureScrollbarSize(ele) {
const randomId = `rc-scrollbar-measure-${Math.random().toString(36).substring(7)}`;
const measureEle = document.createElement('div');
measureEle.id = randomId;
// Create Style
const measureStyle = measureEle.style;
measureStyle.position = 'absolute';
measureStyle.left = '0';
measureStyle.top = '0';
measureStyle.width = '100px';
measureStyle.height = '100px';
measureStyle.overflow = 'scroll';
// Clone Style if needed
let fallbackWidth;
let fallbackHeight;
if (ele) {
const targetStyle = getComputedStyle(ele);
measureStyle.scrollbarColor = targetStyle.scrollbarColor;
measureStyle.scrollbarWidth = targetStyle.scrollbarWidth;
// Set Webkit style
const webkitScrollbarStyle = getComputedStyle(ele, '::-webkit-scrollbar');
const width = parseInt(webkitScrollbarStyle.width, 10);
const height = parseInt(webkitScrollbarStyle.height, 10);
// Try wrap to handle CSP case
try {
const widthStyle = width ? `width: ${webkitScrollbarStyle.width};` : '';
const heightStyle = height ? `height: ${webkitScrollbarStyle.height};` : '';
updateCSS(`
#${randomId}::-webkit-scrollbar {
${widthStyle}
${heightStyle}
}`, randomId);
} catch (e) {
// Can't wrap, just log error
console.error(e);
// Get from style directly
fallbackWidth = width;
fallbackHeight = height;
}
}
document.body.appendChild(measureEle);
// Measure. Get fallback style if provided
const scrollWidth = ele && fallbackWidth && !Number.isNaN(fallbackWidth) ? fallbackWidth : measureEle.offsetWidth - measureEle.clientWidth;
const scrollHeight = ele && fallbackHeight && !Number.isNaN(fallbackHeight) ? fallbackHeight : measureEle.offsetHeight - measureEle.clientHeight;
// Clean up
document.body.removeChild(measureEle);
removeCSS(randomId);
return {
width: scrollWidth,
height: scrollHeight
};
}
export default function getScrollBarSize(fresh) {
if (typeof document === 'undefined') {
return 0;
}
if (fresh || cached === undefined) {
cached = measureScrollbarSize();
}
return cached.width;
}
export function getTargetScrollBarSize(target) {
if (typeof document === 'undefined' || !target || !(target instanceof Element)) {
return {
width: 0,
height: 0
};
}
return measureScrollbarSize(target);
}
@@ -0,0 +1,8 @@
type Updater<T> = (updater: T | ((origin: T) => T)) => void;
/**
* Similar to `useState` but will use props value if provided.
* From React 18, we do not need safe `useState` since it will not throw for unmounted update.
* This hooks remove the `onChange` & `postState` logic since we only need basic merged state logic.
*/
export default function useControlledState<T>(defaultStateValue: T | (() => T), value?: T): [T, Updater<T>];
export {};
@@ -0,0 +1,21 @@
import { useState } from 'react';
import useLayoutEffect from "./useLayoutEffect";
/**
* Similar to `useState` but will use props value if provided.
* From React 18, we do not need safe `useState` since it will not throw for unmounted update.
* This hooks remove the `onChange` & `postState` logic since we only need basic merged state logic.
*/
export default function useControlledState(defaultStateValue, value) {
const [innerValue, setInnerValue] = useState(defaultStateValue);
const mergedValue = value !== undefined ? value : innerValue;
useLayoutEffect(mount => {
if (!mount) {
setInnerValue(value);
}
}, [value]);
return [
// Value
mergedValue,
// Update function
setInnerValue];
}
@@ -0,0 +1,3 @@
/** As `React.useEffect` but pass origin value in callback and not need care deps length change. */
declare function useEffect(callback: (prevDeps: any[]) => void, deps: any[]): void;
export default useEffect;
+13
View File
@@ -0,0 +1,13 @@
import * as React from 'react';
/** As `React.useEffect` but pass origin value in callback and not need care deps length change. */
function useEffect(callback, deps) {
const prevRef = React.useRef(deps);
React.useEffect(() => {
if (deps.length !== prevRef.current.length || deps.some((dep, index) => dep !== prevRef.current[index])) {
callback(prevRef.current);
}
prevRef.current = deps;
});
}
export default useEffect;
@@ -0,0 +1,2 @@
declare const useEvent: <T extends (...args: any[]) => any>(callback: T) => undefined extends T ? (...args: Parameters<NonNullable<T>>) => ReturnType<NonNullable<T>> | undefined : T;
export default useEvent;
+8
View File
@@ -0,0 +1,8 @@
import * as React from 'react';
const useEvent = callback => {
const fnRef = React.useRef(callback);
fnRef.current = callback;
const memoFn = React.useCallback((...args) => fnRef.current?.(...args), []);
return memoFn;
};
export default useEvent;
+13
View File
@@ -0,0 +1,13 @@
import * as React from 'react';
/** @private Note only worked in develop env. Not work in production. */
export declare function resetUuid(): void;
/**
* Generate a valid HTML id from prefix and key.
* Sanitizes the key by replacing invalid characters with hyphens.
* @param prefix - The prefix for the id
* @param key - The key from React element, may contain spaces or invalid characters
* @returns A valid HTML id string
*/
export declare function getId(prefix: string, key: React.Key): string;
declare const _default: (id?: string) => string;
export default _default;
+73
View File
@@ -0,0 +1,73 @@
import * as React from 'react';
function getUseId() {
// We need fully clone React function here to avoid webpack warning React 17 do not export `useId`
const fullClone = {
...React
};
return fullClone.useId;
}
let uuid = 0;
/** @private Note only worked in develop env. Not work in production. */
export function resetUuid() {
if (process.env.NODE_ENV !== 'production') {
uuid = 0;
}
}
/**
* Generate a valid HTML id from prefix and key.
* Sanitizes the key by replacing invalid characters with hyphens.
* @param prefix - The prefix for the id
* @param key - The key from React element, may contain spaces or invalid characters
* @returns A valid HTML id string
*/
export function getId(prefix, key) {
// React.Key can be string | number, convert to string first
const keyStr = String(key);
// Valid id characters: letters, digits, hyphen, underscore, colon, period
// Replace all invalid characters (including spaces) with hyphens to preserve length
const sanitizedKey = keyStr.replace(/[^a-zA-Z0-9_.:-]/g, '-');
return `${prefix}-${sanitizedKey}`;
}
const useOriginId = getUseId();
export default useOriginId ?
// Use React `useId`
function useId(id) {
const reactId = useOriginId();
// Developer passed id is single source of truth
if (id) {
return id;
}
// Test env always return mock id
if (process.env.NODE_ENV === 'test') {
return 'test-id';
}
return reactId;
} :
// Use compatible of `useId`
function useCompatId(id) {
// Inner id for accessibility usage. Only work in client side
const [innerId, setInnerId] = React.useState('ssr-id');
React.useEffect(() => {
const nextId = uuid;
uuid += 1;
setInnerId(`rc_unique_${nextId}`);
}, []);
// Developer passed id is single source of truth
if (id) {
return id;
}
// Test env always return mock id
if (process.env.NODE_ENV === 'test') {
return 'test-id';
}
// Return react native id or inner id
return innerId;
};
@@ -0,0 +1,4 @@
import * as React from 'react';
declare const useLayoutEffect: (callback: (mount: boolean) => void | VoidFunction, deps?: React.DependencyList) => void;
export declare const useLayoutUpdateEffect: typeof React.useEffect;
export default useLayoutEffect;
@@ -0,0 +1,30 @@
import * as React from 'react';
import canUseDom from "../Dom/canUseDom";
/**
* Wrap `React.useLayoutEffect` which will not throw warning message in test env
*/
const useInternalLayoutEffect = process.env.NODE_ENV !== 'test' && canUseDom() ? React.useLayoutEffect : React.useEffect;
const useLayoutEffect = (callback, deps) => {
const firstMountRef = React.useRef(true);
useInternalLayoutEffect(() => {
return callback(firstMountRef.current);
}, deps);
// We tell react that first mount has passed
useInternalLayoutEffect(() => {
firstMountRef.current = false;
return () => {
firstMountRef.current = true;
};
}, []);
};
export const useLayoutUpdateEffect = (callback, deps) => {
useLayoutEffect(firstMount => {
if (!firstMount) {
return callback();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
};
export default useLayoutEffect;
@@ -0,0 +1 @@
export default function useMemo<Value, Condition = any[]>(getValue: () => Value, condition: Condition, shouldUpdate: (prev: Condition, next: Condition) => boolean): Value;
+9
View File
@@ -0,0 +1,9 @@
import * as React from 'react';
export default function useMemo(getValue, condition, shouldUpdate) {
const cacheRef = React.useRef({});
if (!('value' in cacheRef.current) || shouldUpdate(cacheRef.current.condition, condition)) {
cacheRef.current.value = getValue();
cacheRef.current.condition = condition;
}
return cacheRef.current.value;
}
@@ -0,0 +1,13 @@
type Updater<T> = (updater: T | ((origin: T) => T), ignoreDestroy?: boolean) => void;
/**
* @deprecated Please use `useControlledState` instead if not need support < React 18.
* Similar to `useState` but will use props value if provided.
* Note that internal use rc-util `useState` hook.
*/
export default function useMergedState<T, R = T>(defaultStateValue: T | (() => T), option?: {
defaultValue?: T | (() => T);
value?: T;
onChange?: (value: T, prevValue: T) => void;
postState?: (value: T) => T;
}): [R, Updater<T>];
export {};
@@ -0,0 +1,58 @@
import useEvent from "./useEvent";
import { useLayoutUpdateEffect } from "./useLayoutEffect";
import useState from "./useState";
/** We only think `undefined` is empty */
function hasValue(value) {
return value !== undefined;
}
/**
* @deprecated Please use `useControlledState` instead if not need support < React 18.
* Similar to `useState` but will use props value if provided.
* Note that internal use rc-util `useState` hook.
*/
export default function useMergedState(defaultStateValue, option) {
const {
defaultValue,
value,
onChange,
postState
} = option || {};
// ======================= Init =======================
const [innerValue, setInnerValue] = useState(() => {
if (hasValue(value)) {
return value;
} else if (hasValue(defaultValue)) {
return typeof defaultValue === 'function' ? defaultValue() : defaultValue;
} else {
return typeof defaultStateValue === 'function' ? defaultStateValue() : defaultStateValue;
}
});
const mergedValue = value !== undefined ? value : innerValue;
const postMergedValue = postState ? postState(mergedValue) : mergedValue;
// ====================== Change ======================
const onChangeFn = useEvent(onChange);
const [prevValue, setPrevValue] = useState([mergedValue]);
useLayoutUpdateEffect(() => {
const prev = prevValue[0];
if (innerValue !== prev) {
onChangeFn(innerValue, prev);
}
}, [prevValue]);
// Sync value back to `undefined` when it from control to un-control
useLayoutUpdateEffect(() => {
if (!hasValue(value)) {
setInnerValue(value);
}
}, [value]);
// ====================== Update ======================
const triggerChange = useEvent((updater, ignoreDestroy) => {
setInnerValue(updater, ignoreDestroy);
setPrevValue([mergedValue], ignoreDestroy);
});
return [postMergedValue, triggerChange];
}
@@ -0,0 +1,6 @@
/**
* Hook to detect if the user is on a mobile device
* Notice that this hook will only detect the device type in effect, so it will always be false in server side
*/
declare const useMobile: () => boolean;
export default useMobile;
+16
View File
@@ -0,0 +1,16 @@
import { useState } from 'react';
import isMobile from "../isMobile";
import useLayoutEffect from "./useLayoutEffect";
/**
* Hook to detect if the user is on a mobile device
* Notice that this hook will only detect the device type in effect, so it will always be false in server side
*/
const useMobile = () => {
const [mobile, setMobile] = useState(false);
useLayoutEffect(() => {
setMobile(isMobile());
}, []);
return mobile;
};
export default useMobile;
+14
View File
@@ -0,0 +1,14 @@
type Updater<T> = T | ((prevValue: T) => T);
export type SetState<T> = (nextValue: Updater<T>,
/**
* Will not update state when destroyed.
* Developer should make sure this is safe to ignore.
*/
ignoreDestroy?: boolean) => void;
/**
* Same as React.useState but `setState` accept `ignoreDestroy` param to not to setState after destroyed.
* We do not make this auto is to avoid real memory leak.
* Developer should confirm it's safe to ignore themselves.
*/
declare const useSafeState: <T>(defaultValue?: T | (() => T)) => [T, SetState<T>];
export default useSafeState;
+24
View File
@@ -0,0 +1,24 @@
import * as React from 'react';
/**
* Same as React.useState but `setState` accept `ignoreDestroy` param to not to setState after destroyed.
* We do not make this auto is to avoid real memory leak.
* Developer should confirm it's safe to ignore themselves.
*/
const useSafeState = defaultValue => {
const destroyRef = React.useRef(false);
const [value, setValue] = React.useState(defaultValue);
React.useEffect(() => {
destroyRef.current = false;
return () => {
destroyRef.current = true;
};
}, []);
function safeSetState(updater, ignoreDestroy) {
if (ignoreDestroy && destroyRef.current) {
return;
}
setValue(updater);
}
return [value, safeSetState];
};
export default useSafeState;
@@ -0,0 +1,9 @@
type Updater<T> = T | ((prevValue: T) => T);
export type SetState<T> = (nextValue: Updater<T>) => void;
/**
* Same as React.useState but will always get latest state.
* This is useful when React merge multiple state updates into one.
* e.g. onTransitionEnd trigger multiple event at once will be merged state update in React.
*/
declare function useSyncState<T>(defaultValue?: T): [get: () => T, set: SetState<T>];
export default useSyncState;
@@ -0,0 +1,20 @@
import * as React from 'react';
import useEvent from "./useEvent";
/**
* Same as React.useState but will always get latest state.
* This is useful when React merge multiple state updates into one.
* e.g. onTransitionEnd trigger multiple event at once will be merged state update in React.
*/
function useSyncState(defaultValue) {
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
const currentValueRef = React.useRef(defaultValue);
const getValue = useEvent(() => {
return currentValueRef.current;
});
const setValue = useEvent(updater => {
currentValueRef.current = typeof updater === 'function' ? updater(currentValueRef.current) : updater;
forceUpdate();
});
return [getValue, setValue];
}
export default useSyncState;
+10
View File
@@ -0,0 +1,10 @@
export { default as useEvent } from './hooks/useEvent';
export { default as useMergedState } from './hooks/useMergedState';
export { default as useControlledState } from './hooks/useControlledState';
export { supportNodeRef, supportRef, useComposeRef } from './ref';
export { default as get } from './utils/get';
export { default as set, merge, mergeWith } from './utils/set';
export { default as warning, noteOnce } from './warning';
export { default as omit } from './omit';
export { default as toArray } from './Children/toArray';
export { default as mergeProps } from './mergeProps';
+10
View File
@@ -0,0 +1,10 @@
export { default as useEvent } from "./hooks/useEvent";
export { default as useMergedState } from "./hooks/useMergedState";
export { default as useControlledState } from "./hooks/useControlledState";
export { supportNodeRef, supportRef, useComposeRef } from "./ref";
export { default as get } from "./utils/get";
export { default as set, merge, mergeWith } from "./utils/set";
export { default as warning, noteOnce } from "./warning";
export { default as omit } from "./omit";
export { default as toArray } from "./Children/toArray";
export { default as mergeProps } from "./mergeProps";
+9
View File
@@ -0,0 +1,9 @@
/**
* Deeply compares two object literals.
* @param obj1 object 1
* @param obj2 object 2
* @param shallow shallow compare
* @returns
*/
declare function isEqual(obj1: any, obj2: any, shallow?: boolean): boolean;
export default isEqual;
+50
View File
@@ -0,0 +1,50 @@
import warning from "./warning";
/**
* Deeply compares two object literals.
* @param obj1 object 1
* @param obj2 object 2
* @param shallow shallow compare
* @returns
*/
function isEqual(obj1, obj2, shallow = false) {
// https://github.com/mapbox/mapbox-gl-js/pull/5979/files#diff-fde7145050c47cc3a306856efd5f9c3016e86e859de9afbd02c879be5067e58f
const refSet = new Set();
function deepEqual(a, b, level = 1) {
const circular = refSet.has(a);
warning(!circular, 'Warning: There may be circular references');
if (circular) {
return false;
}
if (a === b) {
return true;
}
if (shallow && level > 1) {
return false;
}
refSet.add(a);
const newLevel = level + 1;
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i], newLevel)) {
return false;
}
}
return true;
}
if (a && b && typeof a === 'object' && typeof b === 'object') {
const keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) {
return false;
}
return keys.every(key => deepEqual(a[key], b[key], newLevel));
}
// other
return false;
}
return deepEqual(obj1, obj2);
}
export default isEqual;
+2
View File
@@ -0,0 +1,2 @@
declare const getIsMobile: () => boolean;
export default getIsMobile;
+9
View File
@@ -0,0 +1,9 @@
import isMobile from 'is-mobile';
let cached;
const getIsMobile = () => {
if (typeof cached === 'undefined') {
cached = isMobile();
}
return cached;
};
export default getIsMobile;
+8
View File
@@ -0,0 +1,8 @@
/**
* Merges multiple props objects into one. Unlike `Object.assign()` or `{ ...a, ...b }`, it skips
* properties whose value is explicitly set to `undefined`.
*/
declare function mergeProps<A, B>(a: A, b: B): B & A;
declare function mergeProps<A, B, C>(a: A, b: B, c: C): C & B & A;
declare function mergeProps<A, B, C, D>(a: A, b: B, c: C, d: D): D & C & B & A;
export default mergeProps;
+19
View File
@@ -0,0 +1,19 @@
/**
* Merges multiple props objects into one. Unlike `Object.assign()` or `{ ...a, ...b }`, it skips
* properties whose value is explicitly set to `undefined`.
*/
function mergeProps(...items) {
const ret = {};
for (const item of items) {
if (item) {
for (const key of Object.keys(item)) {
if (item[key] !== undefined) {
ret[key] = item[key];
}
}
}
}
return ret;
}
export default mergeProps;
+1
View File
@@ -0,0 +1 @@
export default function omit<T extends object, K extends keyof T>(obj: T, fields: K[] | readonly K[]): Omit<T, K>;
+9
View File
@@ -0,0 +1,9 @@
export default function omit(obj, fields) {
const clone = Object.assign({}, obj);
if (Array.isArray(fields)) {
fields.forEach(key => {
delete clone[key];
});
}
return clone;
}
+11
View File
@@ -0,0 +1,11 @@
export interface PickConfig {
aria?: boolean;
data?: boolean;
attr?: boolean;
}
/**
* Picker props from exist props with filter
* @param props Passed props
* @param ariaOnly boolean | { aria?: boolean; data?: boolean; attr?: boolean; } filter config
*/
export default function pickAttrs(props: object, ariaOnly?: boolean | PickConfig): {};
+63
View File
@@ -0,0 +1,63 @@
const attributes = `accept acceptCharset accessKey action allowFullScreen allowTransparency
alt async autoComplete autoFocus autoPlay capture cellPadding cellSpacing challenge
charSet checked classID className colSpan cols content contentEditable contextMenu
controls coords crossOrigin data dateTime default defer dir disabled download draggable
encType form formAction formEncType formMethod formNoValidate formTarget frameBorder
headers height hidden high href hrefLang htmlFor httpEquiv icon id inputMode integrity
is keyParams keyType kind label lang list loop low manifest marginHeight marginWidth max maxLength media
mediaGroup method min minLength multiple muted name noValidate nonce open
optimum pattern placeholder poster preload radioGroup readOnly rel required
reversed role rowSpan rows sandbox scope scoped scrolling seamless selected
shape size sizes span spellCheck src srcDoc srcLang srcSet start step style
summary tabIndex target title type useMap value width wmode wrap`;
const eventsName = `onCopy onCut onPaste onCompositionEnd onCompositionStart onCompositionUpdate onKeyDown
onKeyPress onKeyUp onFocus onBlur onChange onInput onSubmit onClick onContextMenu onDoubleClick
onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown
onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp onSelect onTouchCancel
onTouchEnd onTouchMove onTouchStart onScroll onWheel onAbort onCanPlay onCanPlayThrough
onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata
onLoadStart onPause onPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting onLoad onError`;
const propList = `${attributes} ${eventsName}`.split(/[\s\n]+/);
/* eslint-enable max-len */
const ariaPrefix = 'aria-';
const dataPrefix = 'data-';
function match(key, prefix) {
return key.indexOf(prefix) === 0;
}
/**
* Picker props from exist props with filter
* @param props Passed props
* @param ariaOnly boolean | { aria?: boolean; data?: boolean; attr?: boolean; } filter config
*/
export default function pickAttrs(props, ariaOnly = false) {
let mergedConfig;
if (ariaOnly === false) {
mergedConfig = {
aria: true,
data: true,
attr: true
};
} else if (ariaOnly === true) {
mergedConfig = {
aria: true
};
} else {
mergedConfig = {
...ariaOnly
};
}
const attrs = {};
Object.keys(props).forEach(key => {
if (
// Aria
mergedConfig.aria && (key === 'role' || match(key, ariaPrefix)) ||
// Data
mergedConfig.data && match(key, dataPrefix) ||
// Attr
mergedConfig.attr && propList.includes(key)) {
attrs[key] = props[key];
}
});
return attrs;
}
+4
View File
@@ -0,0 +1,4 @@
/**
* Proxy object if environment supported
*/
export default function proxyObject<Obj extends object, ExtendObj extends object>(obj: Obj, extendProps: ExtendObj): Obj & ExtendObj;
+19
View File
@@ -0,0 +1,19 @@
/**
* Proxy object if environment supported
*/
export default function proxyObject(obj, extendProps) {
if (typeof Proxy !== 'undefined' && obj) {
return new Proxy(obj, {
get(target, prop) {
if (extendProps[prop]) {
return extendProps[prop];
}
// Proxy origin property
const originProp = target[prop];
return typeof originProp === 'function' ? originProp.bind(target) : originProp;
}
});
}
return obj;
}
+6
View File
@@ -0,0 +1,6 @@
declare const wrapperRaf: {
(callback: () => void, times?: number): number;
cancel(id: number): void;
ids(): Map<number, number>;
};
export default wrapperRaf;
+43
View File
@@ -0,0 +1,43 @@
let raf = callback => +setTimeout(callback, 16);
let caf = num => clearTimeout(num);
if (typeof window !== 'undefined' && 'requestAnimationFrame' in window) {
raf = callback => window.requestAnimationFrame(callback);
caf = handle => window.cancelAnimationFrame(handle);
}
let rafUUID = 0;
const rafIds = new Map();
function cleanup(id) {
rafIds.delete(id);
}
const wrapperRaf = (callback, times = 1) => {
rafUUID += 1;
const id = rafUUID;
function callRef(leftTimes) {
if (leftTimes === 0) {
// Clean up
cleanup(id);
// Trigger
callback();
} else {
// Next raf
const realId = raf(() => {
callRef(leftTimes - 1);
});
// Bind real raf id
rafIds.set(id, realId);
}
}
callRef(times);
return id;
};
wrapperRaf.cancel = id => {
const realId = rafIds.get(id);
cleanup(id);
return caf(realId);
};
if (process.env.NODE_ENV !== 'production') {
wrapperRaf.ids = () => rafIds;
}
export default wrapperRaf;
+19
View File
@@ -0,0 +1,19 @@
import type * as React from 'react';
export declare const fillRef: <T>(ref: React.Ref<T>, node: T) => void;
/**
* Merge refs into one ref function to support ref passing.
*/
export declare const composeRef: <T>(...refs: React.Ref<T>[]) => React.Ref<T>;
export declare const useComposeRef: <T>(...refs: React.Ref<T>[]) => React.Ref<T>;
export declare const supportRef: (nodeOrComponent: any) => boolean;
interface RefAttributes<T> extends React.Attributes {
ref: React.Ref<T>;
}
export declare const supportNodeRef: <T = any>(node: React.ReactNode) => node is React.ReactElement<unknown, string | React.JSXElementConstructor<any>> & RefAttributes<T>;
/**
* In React 19. `ref` is not a property from node.
* But a property from `props.ref`.
* To check if `props.ref` exist or fallback to `ref`.
*/
export declare const getNodeRef: <T = any>(node: React.ReactNode) => React.Ref<T> | null;
export {};
+76
View File
@@ -0,0 +1,76 @@
import { isValidElement, version } from 'react';
import { ForwardRef, isMemo } from 'react-is';
import useMemo from "./hooks/useMemo";
import isFragment from "./React/isFragment";
const ReactMajorVersion = Number(version.split('.')[0]);
export const fillRef = (ref, node) => {
if (typeof ref === 'function') {
ref(node);
} else if (typeof ref === 'object' && ref && 'current' in ref) {
ref.current = node;
}
};
/**
* Merge refs into one ref function to support ref passing.
*/
export const composeRef = (...refs) => {
const refList = refs.filter(Boolean);
if (refList.length <= 1) {
return refList[0];
}
return node => {
refs.forEach(ref => {
fillRef(ref, node);
});
};
};
export const useComposeRef = (...refs) => {
return useMemo(() => composeRef(...refs),
// eslint-disable-next-line react-hooks/exhaustive-deps
refs, (prev, next) => prev.length !== next.length || prev.every((ref, i) => ref !== next[i]));
};
export const supportRef = nodeOrComponent => {
if (!nodeOrComponent) {
return false;
}
// React 19 no need `forwardRef` anymore. So just pass if is a React element.
if (isReactElement(nodeOrComponent) && ReactMajorVersion >= 19) {
return true;
}
const type = isMemo(nodeOrComponent) ? nodeOrComponent.type.type : nodeOrComponent.type;
// Function component node
if (typeof type === 'function' && !type.prototype?.render && type.$$typeof !== ForwardRef) {
return false;
}
// Class component
if (typeof nodeOrComponent === 'function' && !nodeOrComponent.prototype?.render && nodeOrComponent.$$typeof !== ForwardRef) {
return false;
}
return true;
};
function isReactElement(node) {
return /*#__PURE__*/isValidElement(node) && !isFragment(node);
}
export const supportNodeRef = node => {
return isReactElement(node) && supportRef(node);
};
/**
* In React 19. `ref` is not a property from node.
* But a property from `props.ref`.
* To check if `props.ref` exist or fallback to `ref`.
*/
export const getNodeRef = node => {
if (node && isReactElement(node)) {
const ele = node;
// Source from:
// https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/getReactNodeRef/getReactNodeRef.ts
return ele.props.propertyIsEnumerable('ref') ? ele.props.ref : ele.ref;
}
return null;
};
+12
View File
@@ -0,0 +1,12 @@
import type React from 'react';
export interface SetStyleOptions {
element?: HTMLElement;
}
/**
* Easy to set element style, return previous style
* IE browser compatible(IE browser doesn't merge overflow style, need to set it separately)
* https://github.com/ant-design/ant-design/issues/19393
*
*/
declare function setStyle(style: React.CSSProperties, options?: SetStyleOptions): React.CSSProperties;
export default setStyle;
+26
View File
@@ -0,0 +1,26 @@
/**
* Easy to set element style, return previous style
* IE browser compatible(IE browser doesn't merge overflow style, need to set it separately)
* https://github.com/ant-design/ant-design/issues/19393
*
*/
function setStyle(style, options = {}) {
if (!style) {
return {};
}
const {
element = document.body
} = options;
const oldStyle = {};
const styleKeys = Object.keys(style);
// IE browser compatible
styleKeys.forEach(key => {
oldStyle[key] = element.style[key];
});
styleKeys.forEach(key => {
element.style[key] = style[key];
});
return oldStyle;
}
export default setStyle;
+8
View File
@@ -0,0 +1,8 @@
export type ElementClass = Function;
export type Property = PropertyDescriptor | Function;
export declare function spyElementPrototypes<T extends ElementClass>(elementClass: T, properties: Record<string, Property>): {
mockRestore(): void;
};
export declare function spyElementPrototype(Element: ElementClass, propName: string, property: Property): {
mockRestore(): void;
};
+58
View File
@@ -0,0 +1,58 @@
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable no-param-reassign */
const NO_EXIST = {
__NOT_EXIST: true
};
export function spyElementPrototypes(elementClass, properties) {
const propNames = Object.keys(properties);
const originDescriptors = {};
propNames.forEach(propName => {
const originDescriptor = Object.getOwnPropertyDescriptor(elementClass.prototype, propName);
originDescriptors[propName] = originDescriptor || NO_EXIST;
const spyProp = properties[propName];
if (typeof spyProp === 'function') {
// If is a function
elementClass.prototype[propName] = function spyFunc(...args) {
return spyProp.call(this, originDescriptor, ...args);
};
} else {
// Otherwise tread as a property
Object.defineProperty(elementClass.prototype, propName, {
...spyProp,
set(value) {
if (spyProp.set) {
return spyProp.set.call(this, originDescriptor, value);
}
return originDescriptor.set(value);
},
get() {
if (spyProp.get) {
return spyProp.get.call(this, originDescriptor);
}
return originDescriptor.get();
},
configurable: true
});
}
});
return {
mockRestore() {
propNames.forEach(propName => {
const originDescriptor = originDescriptors[propName];
if (originDescriptor === NO_EXIST) {
delete elementClass.prototype[propName];
} else if (typeof originDescriptor === 'function') {
elementClass.prototype[propName] = originDescriptor;
} else {
Object.defineProperty(elementClass.prototype, propName, originDescriptor);
}
});
}
};
}
export function spyElementPrototype(Element, propName, property) {
return spyElementPrototypes(Element, {
[propName]: property
});
}
/* eslint-enable */
+1
View File
@@ -0,0 +1 @@
export default function get(entity: any, path: (string | number | symbol)[] | readonly (string | number | symbol)[]): any;
+10
View File
@@ -0,0 +1,10 @@
export default function get(entity, path) {
let current = entity;
for (let i = 0; i < path.length; i += 1) {
if (current === null || current === undefined) {
return undefined;
}
current = current[path[i]];
}
return current;
}
+18
View File
@@ -0,0 +1,18 @@
export type Path = (string | number | symbol)[];
export default function set<Entity = any, Output = Entity, Value = any>(entity: Entity, paths: Path, value: Value, removeIfUndefined?: boolean): Output;
export type MergeFn = (current: any, next: any) => any;
/**
* Merge multiple objects. Support custom merge logic.
* @param sources object sources
* @param config.prepareArray Customize array prepare function.
* It will return empty [] by default.
* So when match array, it will auto be override with next array in sources.
*/
export declare function mergeWith<T extends object>(sources: T[], config?: {
prepareArray?: MergeFn;
}): T;
/**
* Merge multiple objects into a new single object.
* Arrays will be replaced by default.
*/
export declare function merge<T extends object>(...sources: T[]): T;
+94
View File
@@ -0,0 +1,94 @@
import get from "./get";
function internalSet(entity, paths, value, removeIfUndefined) {
if (!paths.length) {
return value;
}
const [path, ...restPath] = paths;
let clone;
if (!entity && typeof path === 'number') {
clone = [];
} else if (Array.isArray(entity)) {
clone = [...entity];
} else {
clone = {
...entity
};
}
// Delete prop if `removeIfUndefined` and value is undefined
if (removeIfUndefined && value === undefined && restPath.length === 1) {
delete clone[path][restPath[0]];
} else {
clone[path] = internalSet(clone[path], restPath, value, removeIfUndefined);
}
return clone;
}
export default function set(entity, paths, value, removeIfUndefined = false) {
// Do nothing if `removeIfUndefined` and parent object not exist
if (paths.length && removeIfUndefined && value === undefined && !get(entity, paths.slice(0, -1))) {
return entity;
}
return internalSet(entity, paths, value, removeIfUndefined);
}
function isObject(obj) {
return typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === Object.prototype;
}
function createEmpty(source) {
return Array.isArray(source) ? [] : {};
}
const keys = typeof Reflect === 'undefined' ? Object.keys : Reflect.ownKeys;
// ================================ Merge ================================
/**
* Merge multiple objects. Support custom merge logic.
* @param sources object sources
* @param config.prepareArray Customize array prepare function.
* It will return empty [] by default.
* So when match array, it will auto be override with next array in sources.
*/
export function mergeWith(sources, config = {}) {
const {
prepareArray
} = config;
const finalPrepareArray = prepareArray || (() => []);
let clone = createEmpty(sources[0]);
sources.forEach(src => {
function internalMerge(path, parentLoopSet) {
const loopSet = new Set(parentLoopSet);
const value = get(src, path);
const isArr = Array.isArray(value);
if (isArr || isObject(value)) {
// Only add not loop obj
if (!loopSet.has(value)) {
loopSet.add(value);
const originValue = get(clone, path);
if (isArr) {
// Array will always be override
clone = set(clone, path, finalPrepareArray(originValue, value));
} else if (!originValue || typeof originValue !== 'object') {
// Init container if not exist
clone = set(clone, path, createEmpty(value));
}
keys(value).forEach(key => {
if (Object.getOwnPropertyDescriptor(value, key).enumerable) {
internalMerge([...path, key], loopSet);
}
});
}
} else {
clone = set(clone, path, value);
}
}
internalMerge([]);
});
return clone;
}
/**
* Merge multiple objects into a new single object.
* Arrays will be replaced by default.
*/
export function merge(...sources) {
return mergeWith(sources);
}
+32
View File
@@ -0,0 +1,32 @@
export type preMessageFn = (message: string, type: 'warning' | 'note') => string | null | undefined | number;
/**
* Pre warning enable you to parse content before console.error.
* Modify to null will prevent warning.
*/
export declare const preMessage: (fn: preMessageFn) => void;
/**
* Warning if condition not match.
* @param valid Condition
* @param message Warning message
* @example
* ```js
* warning(false, 'some error'); // print some error
* warning(true, 'some error'); // print nothing
* warning(1 === 2, 'some error'); // print some error
* ```
*/
export declare function warning(valid: boolean, message: string): void;
/** @see Similar to {@link warning} */
export declare function note(valid: boolean, message: string): void;
export declare function resetWarned(): void;
export declare function call(method: (valid: boolean, message: string) => void, valid: boolean, message: string): void;
/** @see Same as {@link warning}, but only warn once for the same message */
export declare function warningOnce(valid: boolean, message: string): void;
export declare namespace warningOnce {
var preMessage: (fn: preMessageFn) => void;
var resetWarned: typeof import("./warning").resetWarned;
var noteOnce: typeof import("./warning").noteOnce;
}
/** @see Same as {@link warning}, but only warn once for the same message */
export declare function noteOnce(valid: boolean, message: string): void;
export default warningOnce;
+64
View File
@@ -0,0 +1,64 @@
/* eslint-disable no-console */
let warned = {};
const preWarningFns = [];
/**
* Pre warning enable you to parse content before console.error.
* Modify to null will prevent warning.
*/
export const preMessage = fn => {
preWarningFns.push(fn);
};
/**
* Warning if condition not match.
* @param valid Condition
* @param message Warning message
* @example
* ```js
* warning(false, 'some error'); // print some error
* warning(true, 'some error'); // print nothing
* warning(1 === 2, 'some error'); // print some error
* ```
*/
export function warning(valid, message) {
if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) {
const finalMessage = preWarningFns.reduce((msg, preMessageFn) => preMessageFn(msg ?? '', 'warning'), message);
if (finalMessage) {
console.error(`Warning: ${finalMessage}`);
}
}
}
/** @see Similar to {@link warning} */
export function note(valid, message) {
if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) {
const finalMessage = preWarningFns.reduce((msg, preMessageFn) => preMessageFn(msg ?? '', 'note'), message);
if (finalMessage) {
console.warn(`Note: ${finalMessage}`);
}
}
}
export function resetWarned() {
warned = {};
}
export function call(method, valid, message) {
if (!valid && !warned[message]) {
method(false, message);
warned[message] = true;
}
}
/** @see Same as {@link warning}, but only warn once for the same message */
export function warningOnce(valid, message) {
call(warning, valid, message);
}
/** @see Same as {@link warning}, but only warn once for the same message */
export function noteOnce(valid, message) {
call(note, valid, message);
}
warningOnce.preMessage = preMessage;
warningOnce.resetWarned = resetWarned;
warningOnce.noteOnce = noteOnce;
export default warningOnce;
@@ -0,0 +1,5 @@
import React from 'react';
export interface Option {
keepEmpty?: boolean;
}
export default function toArray(children: React.ReactNode, option?: Option): React.ReactElement[];
@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = toArray;
var _isFragment = _interopRequireDefault(require("../React/isFragment"));
var _react = _interopRequireDefault(require("react"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function toArray(children, option = {}) {
let ret = [];
_react.default.Children.forEach(children, child => {
if ((child === undefined || child === null) && !option.keepEmpty) {
return;
}
if (Array.isArray(child)) {
ret = ret.concat(toArray(child));
} else if ((0, _isFragment.default)(child) && child.props) {
ret = ret.concat(toArray(child.props.children, option));
} else {
ret.push(child);
}
});
return ret;
}
@@ -0,0 +1 @@
export default function canUseDom(): boolean;
+9
View File
@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = canUseDom;
function canUseDom() {
return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
}
@@ -0,0 +1 @@
export default function contains(root: Node | null | undefined, n?: Node): boolean;
+26
View File
@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = contains;
function contains(root, n) {
if (!root) {
return false;
}
// Use native if support
if (root.contains) {
return root.contains(n);
}
// `document.contains` not support with IE11
let node = n;
while (node) {
if (node === root) {
return true;
}
node = node.parentNode;
}
return false;
}
@@ -0,0 +1,25 @@
export type ContainerType = Element | ShadowRoot;
export type Prepend = boolean | 'queue';
export type AppendType = 'prependQueue' | 'append' | 'prepend';
interface Options {
attachTo?: ContainerType;
csp?: {
nonce?: string;
};
prepend?: Prepend;
/**
* Config the `priority` of `prependQueue`. Default is `0`.
* It's useful if you need to insert style before other style.
*/
priority?: number;
mark?: string;
styles?: HTMLElement[];
}
export declare function injectCSS(css: string, option?: Options): HTMLStyleElement;
export declare function removeCSS(key: string, option?: Options): void;
/**
* manually clear container cache to avoid global cache in unit testes
*/
export declare function clearContainerCache(): void;
export declare function updateCSS(css: string, key: string, originOption?: Options): HTMLElement;
export {};
+156
View File
@@ -0,0 +1,156 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.clearContainerCache = clearContainerCache;
exports.injectCSS = injectCSS;
exports.removeCSS = removeCSS;
exports.updateCSS = updateCSS;
var _canUseDom = _interopRequireDefault(require("./canUseDom"));
var _contains = _interopRequireDefault(require("./contains"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const APPEND_ORDER = 'data-rc-order';
const APPEND_PRIORITY = 'data-rc-priority';
const MARK_KEY = `rc-util-key`;
const containerCache = new Map();
function getMark({
mark
} = {}) {
if (mark) {
return mark.startsWith('data-') ? mark : `data-${mark}`;
}
return MARK_KEY;
}
function getContainer(option) {
if (option.attachTo) {
return option.attachTo;
}
const head = document.querySelector('head');
return head || document.body;
}
function getOrder(prepend) {
if (prepend === 'queue') {
return 'prependQueue';
}
return prepend ? 'prepend' : 'append';
}
/**
* Find style which inject by rc-util
*/
function findStyles(container) {
return Array.from((containerCache.get(container) || container).children).filter(node => node.tagName === 'STYLE');
}
function injectCSS(css, option = {}) {
if (!(0, _canUseDom.default)()) {
return null;
}
const {
csp,
prepend,
priority = 0
} = option;
const mergedOrder = getOrder(prepend);
const isPrependQueue = mergedOrder === 'prependQueue';
const styleNode = document.createElement('style');
styleNode.setAttribute(APPEND_ORDER, mergedOrder);
if (isPrependQueue && priority) {
styleNode.setAttribute(APPEND_PRIORITY, `${priority}`);
}
if (csp?.nonce) {
styleNode.nonce = csp?.nonce;
}
styleNode.innerHTML = css;
const container = getContainer(option);
const {
firstChild
} = container;
if (prepend) {
// If is queue `prepend`, it will prepend first style and then append rest style
if (isPrependQueue) {
const existStyle = (option.styles || findStyles(container)).filter(node => {
// Ignore style which not injected by rc-util with prepend
if (!['prepend', 'prependQueue'].includes(node.getAttribute(APPEND_ORDER))) {
return false;
}
// Ignore style which priority less then new style
const nodePriority = Number(node.getAttribute(APPEND_PRIORITY) || 0);
return priority >= nodePriority;
});
if (existStyle.length) {
container.insertBefore(styleNode, existStyle[existStyle.length - 1].nextSibling);
return styleNode;
}
}
// Use `insertBefore` as `prepend`
container.insertBefore(styleNode, firstChild);
} else {
container.appendChild(styleNode);
}
return styleNode;
}
function findExistNode(key, option = {}) {
let {
styles
} = option;
styles ||= findStyles(getContainer(option));
return styles.find(node => node.getAttribute(getMark(option)) === key);
}
function removeCSS(key, option = {}) {
const existNode = findExistNode(key, option);
if (existNode) {
const container = getContainer(option);
container.removeChild(existNode);
}
}
/**
* qiankun will inject `appendChild` to insert into other
*/
function syncRealContainer(container, option) {
const cachedRealContainer = containerCache.get(container);
// Find real container when not cached or cached container removed
if (!cachedRealContainer || !(0, _contains.default)(document, cachedRealContainer)) {
const placeholderStyle = injectCSS('', option);
const {
parentNode
} = placeholderStyle;
containerCache.set(container, parentNode);
container.removeChild(placeholderStyle);
}
}
/**
* manually clear container cache to avoid global cache in unit testes
*/
function clearContainerCache() {
containerCache.clear();
}
function updateCSS(css, key, originOption = {}) {
const container = getContainer(originOption);
const styles = findStyles(container);
const option = {
...originOption,
styles
};
// Sync real parent
syncRealContainer(container, option);
const existNode = findExistNode(key, option);
if (existNode) {
if (option.csp?.nonce && existNode.nonce !== option.csp?.nonce) {
existNode.nonce = option.csp?.nonce;
}
if (existNode.innerHTML !== css) {
existNode.innerHTML = css;
}
return existNode;
}
const newNode = injectCSS(css, option);
newNode.setAttribute(getMark(option), key);
return newNode;
}
@@ -0,0 +1,14 @@
import React from 'react';
export declare function isDOM(node: any): node is HTMLElement | SVGElement;
/**
* Retrieves a DOM node via a ref, and does not invoke `findDOMNode`.
*/
export declare function getDOM(node: any): HTMLElement | SVGElement | null;
/**
* Return if a node is a DOM node. Else will return by `findDOMNode`
*/
export default function findDOMNode<T = Element | Text>(node: React.ReactInstance | HTMLElement | SVGElement | {
nativeElement: T;
} | {
current: T;
}): T | null;
+43
View File
@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = findDOMNode;
exports.getDOM = getDOM;
exports.isDOM = isDOM;
function isDOM(node) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element
// Since XULElement is also subclass of Element, we only need HTMLElement and SVGElement
return node instanceof HTMLElement || node instanceof SVGElement;
}
/**
* Retrieves a DOM node via a ref, and does not invoke `findDOMNode`.
*/
function getDOM(node) {
if (node && typeof node === 'object' && isDOM(node.nativeElement)) {
return node.nativeElement;
}
if (isDOM(node)) {
return node;
}
return null;
}
/**
* Return if a node is a DOM node. Else will return by `findDOMNode`
*/
function findDOMNode(node) {
const domNode = getDOM(node);
if (domNode) {
return domNode;
}
if (node && typeof node === 'object' && 'current' in node) {
const refDomNode = getDOM(node.current);
if (refDomNode) {
return refDomNode;
}
}
return null;
}
+21
View File
@@ -0,0 +1,21 @@
export declare function getFocusNodeList(node: HTMLElement, includePositive?: boolean): HTMLElement[];
export interface InputFocusOptions extends FocusOptions {
cursor?: 'start' | 'end' | 'all';
}
/**
* Focus element and set cursor position for input/textarea elements.
*/
export declare function triggerFocus(element?: HTMLElement, option?: InputFocusOptions): void;
/**
* Lock focus in the element.
* It will force back to the first focusable element when focus leaves the element.
* @param id - A stable ID for this lock instance
*/
export declare function lockFocus(element: HTMLElement, id: string): VoidFunction;
/**
* Lock focus within an element.
* When locked, focus will be restricted to focusable elements within the specified element.
* If multiple elements are locked, only the last locked element will be effective.
* @returns A function to mark an element as ignored, which will temporarily allow focus on that element even if it's outside the locked area.
*/
export declare function useLockFocus(lock: boolean, getElement: () => HTMLElement | null): [ignoreElement: (ele: HTMLElement) => void];
+238
View File
@@ -0,0 +1,238 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getFocusNodeList = getFocusNodeList;
exports.lockFocus = lockFocus;
exports.triggerFocus = triggerFocus;
exports.useLockFocus = useLockFocus;
var _react = require("react");
var _isVisible = _interopRequireDefault(require("./isVisible"));
var _useId = _interopRequireDefault(require("../hooks/useId"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function focusable(node, includePositive = false) {
if ((0, _isVisible.default)(node)) {
const nodeName = node.nodeName.toLowerCase();
const isFocusableElement =
// Focusable element
['input', 'select', 'textarea', 'button'].includes(nodeName) ||
// Editable element
node.isContentEditable ||
// Anchor with href element
nodeName === 'a' && !!node.getAttribute('href');
// Get tabIndex
const tabIndexAttr = node.getAttribute('tabindex');
const tabIndexNum = Number(tabIndexAttr);
// Parse as number if validate
let tabIndex = null;
if (tabIndexAttr && !Number.isNaN(tabIndexNum)) {
tabIndex = tabIndexNum;
} else if (isFocusableElement && tabIndex === null) {
tabIndex = 0;
}
// Block focusable if disabled
if (isFocusableElement && node.disabled) {
tabIndex = null;
}
return tabIndex !== null && (tabIndex >= 0 || includePositive && tabIndex < 0);
}
return false;
}
function getFocusNodeList(node, includePositive = false) {
const res = [...node.querySelectorAll('*')].filter(child => {
return focusable(child, includePositive);
});
if (focusable(node, includePositive)) {
res.unshift(node);
}
return res;
}
// Used for `rc-input` `rc-textarea` `rc-input-number`
/**
* Focus element and set cursor position for input/textarea elements.
*/
function triggerFocus(element, option) {
if (!element) return;
element.focus(option);
// Selection content
const {
cursor
} = option || {};
if (cursor && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
const len = element.value.length;
switch (cursor) {
case 'start':
element.setSelectionRange(0, 0);
break;
case 'end':
element.setSelectionRange(len, len);
break;
default:
element.setSelectionRange(0, len);
}
}
}
// ======================================================
// == Lock Focus ==
// ======================================================
let lastFocusElement = null;
let focusElements = [];
// Map stable ID to lock element
const idToElementMap = new Map();
// Map stable ID to ignored element
const ignoredElementMap = new Map();
function getLastElement() {
return focusElements[focusElements.length - 1];
}
function isIgnoredElement(element) {
const lastElement = getLastElement();
if (element && lastElement) {
// Find the ID that maps to the last element
let lockId;
for (const [id, ele] of idToElementMap.entries()) {
if (ele === lastElement) {
lockId = id;
break;
}
}
const ignoredEle = ignoredElementMap.get(lockId);
return !!ignoredEle && (ignoredEle === element || ignoredEle.contains(element));
}
return false;
}
function hasFocus(element) {
const {
activeElement
} = document;
return element === activeElement || element.contains(activeElement);
}
function syncFocus() {
const lastElement = getLastElement();
const {
activeElement
} = document;
// If current focus is on an ignored element, don't force it back
if (isIgnoredElement(activeElement)) {
return;
}
if (lastElement && !hasFocus(lastElement)) {
const focusableList = getFocusNodeList(lastElement);
const matchElement = focusableList.includes(lastFocusElement) ? lastFocusElement : focusableList[0];
matchElement?.focus({
preventScroll: true
});
} else {
lastFocusElement = activeElement;
}
}
function onWindowKeyDown(e) {
if (e.key === 'Tab') {
const {
activeElement
} = document;
const lastElement = getLastElement();
const focusableList = getFocusNodeList(lastElement);
const last = focusableList[focusableList.length - 1];
if (e.shiftKey && activeElement === focusableList[0]) {
// Tab backward on first focusable element
lastFocusElement = last;
} else if (!e.shiftKey && activeElement === last) {
// Tab forward on last focusable element
lastFocusElement = focusableList[0];
}
}
}
/**
* Lock focus in the element.
* It will force back to the first focusable element when focus leaves the element.
* @param id - A stable ID for this lock instance
*/
function lockFocus(element, id) {
if (element) {
// Store the mapping between ID and element
idToElementMap.set(id, element);
// Refresh focus elements
focusElements = focusElements.filter(ele => ele !== element);
focusElements.push(element);
// Just add event since it will de-duplicate
window.addEventListener('focusin', syncFocus);
window.addEventListener('keydown', onWindowKeyDown, true);
syncFocus();
}
// Always return unregister function
return () => {
lastFocusElement = null;
focusElements = focusElements.filter(ele => ele !== element);
idToElementMap.delete(id);
ignoredElementMap.delete(id);
if (focusElements.length === 0) {
window.removeEventListener('focusin', syncFocus);
window.removeEventListener('keydown', onWindowKeyDown, true);
}
};
}
/**
* Retry an effect until it reports ready.
* When `ready` is `false`, it will schedule one more effect cycle and call `func` again
* with the next `retryTimes`.
*/
function useRetryEffect(func, deps) {
/* eslint-disable react-hooks/exhaustive-deps */
const retryTimesRef = (0, _react.useRef)(0);
const [retryMark, setRetryMark] = (0, _react.useState)(0);
(0, _react.useEffect)(() => {
retryTimesRef.current = 0;
}, deps);
(0, _react.useEffect)(() => {
const [clearFn, ready] = func(retryTimesRef.current);
if (!ready) {
retryTimesRef.current += 1;
setRetryMark(count => count + 1);
}
return clearFn;
}, [...deps, retryMark]);
/* eslint-enable react-hooks/exhaustive-deps */
}
/**
* Lock focus within an element.
* When locked, focus will be restricted to focusable elements within the specified element.
* If multiple elements are locked, only the last locked element will be effective.
* @returns A function to mark an element as ignored, which will temporarily allow focus on that element even if it's outside the locked area.
*/
function useLockFocus(lock, getElement) {
const id = (0, _useId.default)();
const getElementRef = (0, _react.useRef)(getElement);
getElementRef.current = getElement;
const lockEffect = retryTimes => {
if (!lock) {
return [undefined, true];
}
const element = getElementRef.current();
if (element) {
return [lockFocus(element, id), true];
}
return [undefined, retryTimes >= 1];
};
useRetryEffect(lockEffect, [id, lock]);
const ignoreElement = ele => {
if (ele) {
// Set the ignored element using stable ID
ignoredElementMap.set(id, ele);
}
};
return [ignoreElement];
}
@@ -0,0 +1,2 @@
declare const _default: (element: Element) => boolean;
export default _default;
+36
View File
@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _default = element => {
if (!element) {
return false;
}
if (element instanceof Element) {
if (element.offsetParent) {
return true;
}
if (element.getBBox) {
const {
width,
height
} = element.getBBox();
if (width || height) {
return true;
}
}
if (element.getBoundingClientRect) {
const {
width,
height
} = element.getBoundingClientRect();
if (width || height) {
return true;
}
}
}
return false;
};
exports.default = _default;
@@ -0,0 +1,12 @@
export interface scrollLockOptions {
container: HTMLElement;
}
export default class ScrollLocker {
private lockTarget;
private options;
constructor(options?: scrollLockOptions);
getContainer: () => HTMLElement | undefined;
reLock: (options?: scrollLockOptions) => void;
lock: () => void;
unLock: () => void;
}
+117
View File
@@ -0,0 +1,117 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _getScrollBarSize = _interopRequireDefault(require("../getScrollBarSize"));
var _setStyle = _interopRequireDefault(require("../setStyle"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
let uuid = 0;
let locks = [];
const scrollingEffectClassName = 'ant-scrolling-effect';
const scrollingEffectClassNameReg = new RegExp(`${scrollingEffectClassName}`, 'g');
// https://github.com/ant-design/ant-design/issues/19340
// https://github.com/ant-design/ant-design/issues/19332
const cacheStyle = new Map();
class ScrollLocker {
lockTarget;
options;
constructor(options) {
// eslint-disable-next-line no-plusplus
this.lockTarget = uuid++;
this.options = options;
}
getContainer = () => {
return this.options?.container;
};
// if options change...
reLock = options => {
const findLock = locks.find(({
target
}) => target === this.lockTarget);
if (findLock) {
this.unLock();
}
this.options = options;
if (findLock) {
findLock.options = options;
this.lock();
}
};
lock = () => {
// If lockTarget exist return
if (locks.some(({
target
}) => target === this.lockTarget)) {
return;
}
// If same container effect, return
if (locks.some(({
options
}) => options?.container === this.options?.container)) {
locks = [...locks, {
target: this.lockTarget,
options: this.options
}];
return;
}
let scrollBarSize = 0;
const container = this.options?.container || document.body;
if (container === document.body && window.innerWidth - document.documentElement.clientWidth > 0 || container.scrollHeight > container.clientHeight) {
if (getComputedStyle(container).overflow !== 'hidden') {
scrollBarSize = (0, _getScrollBarSize.default)();
}
}
const containerClassName = container.className;
if (locks.filter(({
options
}) => options?.container === this.options?.container).length === 0) {
cacheStyle.set(container, (0, _setStyle.default)({
width: scrollBarSize !== 0 ? `calc(100% - ${scrollBarSize}px)` : undefined,
overflow: 'hidden',
overflowX: 'hidden',
overflowY: 'hidden'
}, {
element: container
}));
}
// https://github.com/ant-design/ant-design/issues/19729
if (!scrollingEffectClassNameReg.test(containerClassName)) {
const addClassName = `${containerClassName} ${scrollingEffectClassName}`;
container.className = addClassName.trim();
}
locks = [...locks, {
target: this.lockTarget,
options: this.options
}];
};
unLock = () => {
const findLock = locks.find(({
target
}) => target === this.lockTarget);
locks = locks.filter(({
target
}) => target !== this.lockTarget);
if (!findLock || locks.some(({
options
}) => options?.container === findLock.options?.container)) {
return;
}
// Remove Effect
const container = this.options?.container || document.body;
const containerClassName = container.className;
if (!scrollingEffectClassNameReg.test(containerClassName)) return;
(0, _setStyle.default)(cacheStyle.get(container), {
element: container
});
cacheStyle.delete(container);
container.className = container.className.replace(scrollingEffectClassNameReg, '').trim();
};
}
exports.default = ScrollLocker;

Some files were not shown because too many files have changed in this diff Show More