1
This commit is contained in:
+22
@@ -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
@@ -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
|
||||
|
||||
[](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)
|
||||
+5
@@ -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
@@ -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;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function canUseDom(): boolean;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function canUseDom() {
|
||||
return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function contains(root: Node | null | undefined, n?: Node): boolean;
|
||||
+20
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
+14
@@ -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
@@ -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
@@ -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
@@ -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];
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
declare const _default: (element: Element) => boolean;
|
||||
export default _default;
|
||||
+29
@@ -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;
|
||||
});
|
||||
+12
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare function isStyleSupport(styleName: string | string[]): boolean;
|
||||
export declare function isStyleSupport(styleName: string, styleValue: any): boolean;
|
||||
+26
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
declare function composeProps<T extends Record<string, any>>(originProps: T, patchProps: Partial<T>, isAll?: boolean): T;
|
||||
export default composeProps;
|
||||
+17
@@ -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;
|
||||
+7
@@ -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
@@ -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);
|
||||
}
|
||||
+8
@@ -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 {};
|
||||
+21
@@ -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];
|
||||
}
|
||||
+3
@@ -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
@@ -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;
|
||||
+2
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
+4
@@ -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;
|
||||
+30
@@ -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;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function useMemo<Value, Condition = any[]>(getValue: () => Value, condition: Condition, shouldUpdate: (prev: Condition, next: Condition) => boolean): Value;
|
||||
+9
@@ -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;
|
||||
}
|
||||
+13
@@ -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 {};
|
||||
+58
@@ -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];
|
||||
}
|
||||
+6
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+9
@@ -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;
|
||||
+20
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
declare const getIsMobile: () => boolean;
|
||||
export default getIsMobile;
|
||||
+9
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export default function omit<T extends object, K extends keyof T>(obj: T, fields: K[] | readonly K[]): Omit<T, K>;
|
||||
+9
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
declare const wrapperRaf: {
|
||||
(callback: () => void, times?: number): number;
|
||||
cancel(id: number): void;
|
||||
ids(): Map<number, number>;
|
||||
};
|
||||
export default wrapperRaf;
|
||||
+43
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export default function get(entity: any, path: (string | number | symbol)[] | readonly (string | number | symbol)[]): any;
|
||||
+10
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
export interface Option {
|
||||
keepEmpty?: boolean;
|
||||
}
|
||||
export default function toArray(children: React.ReactNode, option?: Option): React.ReactElement[];
|
||||
+25
@@ -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;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function canUseDom(): boolean;
|
||||
+9
@@ -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);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function contains(root: Node | null | undefined, n?: Node): boolean;
|
||||
+26
@@ -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;
|
||||
}
|
||||
+25
@@ -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
@@ -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;
|
||||
}
|
||||
+14
@@ -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
@@ -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
@@ -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
@@ -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];
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
declare const _default: (element: Element) => boolean;
|
||||
export default _default;
|
||||
+36
@@ -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;
|
||||
+12
@@ -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
@@ -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
Reference in New Issue
Block a user