This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
@@ -0,0 +1 @@
export default function canUseDom(): boolean;
+3
View File
@@ -0,0 +1,3 @@
export default function canUseDom() {
return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
}
+1
View File
@@ -0,0 +1 @@
export default function contains(root: Node | null | undefined, n?: Node): boolean;
+20
View File
@@ -0,0 +1,20 @@
export default function contains(root, n) {
if (!root) {
return false;
}
// Use native if support
if (root.contains) {
return root.contains(n);
}
// `document.contains` not support with IE11
let node = n;
while (node) {
if (node === root) {
return true;
}
node = node.parentNode;
}
return false;
}
+25
View File
@@ -0,0 +1,25 @@
export type ContainerType = Element | ShadowRoot;
export type Prepend = boolean | 'queue';
export type AppendType = 'prependQueue' | 'append' | 'prepend';
interface Options {
attachTo?: ContainerType;
csp?: {
nonce?: string;
};
prepend?: Prepend;
/**
* Config the `priority` of `prependQueue`. Default is `0`.
* It's useful if you need to insert style before other style.
*/
priority?: number;
mark?: string;
styles?: HTMLElement[];
}
export declare function injectCSS(css: string, option?: Options): HTMLStyleElement;
export declare function removeCSS(key: string, option?: Options): void;
/**
* manually clear container cache to avoid global cache in unit testes
*/
export declare function clearContainerCache(): void;
export declare function updateCSS(css: string, key: string, originOption?: Options): HTMLElement;
export {};
+146
View File
@@ -0,0 +1,146 @@
import canUseDom from "./canUseDom";
import contains from "./contains";
const APPEND_ORDER = 'data-rc-order';
const APPEND_PRIORITY = 'data-rc-priority';
const MARK_KEY = `rc-util-key`;
const containerCache = new Map();
function getMark({
mark
} = {}) {
if (mark) {
return mark.startsWith('data-') ? mark : `data-${mark}`;
}
return MARK_KEY;
}
function getContainer(option) {
if (option.attachTo) {
return option.attachTo;
}
const head = document.querySelector('head');
return head || document.body;
}
function getOrder(prepend) {
if (prepend === 'queue') {
return 'prependQueue';
}
return prepend ? 'prepend' : 'append';
}
/**
* Find style which inject by rc-util
*/
function findStyles(container) {
return Array.from((containerCache.get(container) || container).children).filter(node => node.tagName === 'STYLE');
}
export function injectCSS(css, option = {}) {
if (!canUseDom()) {
return null;
}
const {
csp,
prepend,
priority = 0
} = option;
const mergedOrder = getOrder(prepend);
const isPrependQueue = mergedOrder === 'prependQueue';
const styleNode = document.createElement('style');
styleNode.setAttribute(APPEND_ORDER, mergedOrder);
if (isPrependQueue && priority) {
styleNode.setAttribute(APPEND_PRIORITY, `${priority}`);
}
if (csp?.nonce) {
styleNode.nonce = csp?.nonce;
}
styleNode.innerHTML = css;
const container = getContainer(option);
const {
firstChild
} = container;
if (prepend) {
// If is queue `prepend`, it will prepend first style and then append rest style
if (isPrependQueue) {
const existStyle = (option.styles || findStyles(container)).filter(node => {
// Ignore style which not injected by rc-util with prepend
if (!['prepend', 'prependQueue'].includes(node.getAttribute(APPEND_ORDER))) {
return false;
}
// Ignore style which priority less then new style
const nodePriority = Number(node.getAttribute(APPEND_PRIORITY) || 0);
return priority >= nodePriority;
});
if (existStyle.length) {
container.insertBefore(styleNode, existStyle[existStyle.length - 1].nextSibling);
return styleNode;
}
}
// Use `insertBefore` as `prepend`
container.insertBefore(styleNode, firstChild);
} else {
container.appendChild(styleNode);
}
return styleNode;
}
function findExistNode(key, option = {}) {
let {
styles
} = option;
styles ||= findStyles(getContainer(option));
return styles.find(node => node.getAttribute(getMark(option)) === key);
}
export function removeCSS(key, option = {}) {
const existNode = findExistNode(key, option);
if (existNode) {
const container = getContainer(option);
container.removeChild(existNode);
}
}
/**
* qiankun will inject `appendChild` to insert into other
*/
function syncRealContainer(container, option) {
const cachedRealContainer = containerCache.get(container);
// Find real container when not cached or cached container removed
if (!cachedRealContainer || !contains(document, cachedRealContainer)) {
const placeholderStyle = injectCSS('', option);
const {
parentNode
} = placeholderStyle;
containerCache.set(container, parentNode);
container.removeChild(placeholderStyle);
}
}
/**
* manually clear container cache to avoid global cache in unit testes
*/
export function clearContainerCache() {
containerCache.clear();
}
export function updateCSS(css, key, originOption = {}) {
const container = getContainer(originOption);
const styles = findStyles(container);
const option = {
...originOption,
styles
};
// Sync real parent
syncRealContainer(container, option);
const existNode = findExistNode(key, option);
if (existNode) {
if (option.csp?.nonce && existNode.nonce !== option.csp?.nonce) {
existNode.nonce = option.csp?.nonce;
}
if (existNode.innerHTML !== css) {
existNode.innerHTML = css;
}
return existNode;
}
const newNode = injectCSS(css, option);
newNode.setAttribute(getMark(option), key);
return newNode;
}
@@ -0,0 +1,14 @@
import React from 'react';
export declare function isDOM(node: any): node is HTMLElement | SVGElement;
/**
* Retrieves a DOM node via a ref, and does not invoke `findDOMNode`.
*/
export declare function getDOM(node: any): HTMLElement | SVGElement | null;
/**
* Return if a node is a DOM node. Else will return by `findDOMNode`
*/
export default function findDOMNode<T = Element | Text>(node: React.ReactInstance | HTMLElement | SVGElement | {
nativeElement: T;
} | {
current: T;
}): T | null;
+35
View File
@@ -0,0 +1,35 @@
export function isDOM(node) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element
// Since XULElement is also subclass of Element, we only need HTMLElement and SVGElement
return node instanceof HTMLElement || node instanceof SVGElement;
}
/**
* Retrieves a DOM node via a ref, and does not invoke `findDOMNode`.
*/
export function getDOM(node) {
if (node && typeof node === 'object' && isDOM(node.nativeElement)) {
return node.nativeElement;
}
if (isDOM(node)) {
return node;
}
return null;
}
/**
* Return if a node is a DOM node. Else will return by `findDOMNode`
*/
export default function findDOMNode(node) {
const domNode = getDOM(node);
if (domNode) {
return domNode;
}
if (node && typeof node === 'object' && 'current' in node) {
const refDomNode = getDOM(node.current);
if (refDomNode) {
return refDomNode;
}
}
return null;
}
+21
View File
@@ -0,0 +1,21 @@
export declare function getFocusNodeList(node: HTMLElement, includePositive?: boolean): HTMLElement[];
export interface InputFocusOptions extends FocusOptions {
cursor?: 'start' | 'end' | 'all';
}
/**
* Focus element and set cursor position for input/textarea elements.
*/
export declare function triggerFocus(element?: HTMLElement, option?: InputFocusOptions): void;
/**
* Lock focus in the element.
* It will force back to the first focusable element when focus leaves the element.
* @param id - A stable ID for this lock instance
*/
export declare function lockFocus(element: HTMLElement, id: string): VoidFunction;
/**
* Lock focus within an element.
* When locked, focus will be restricted to focusable elements within the specified element.
* If multiple elements are locked, only the last locked element will be effective.
* @returns A function to mark an element as ignored, which will temporarily allow focus on that element even if it's outside the locked area.
*/
export declare function useLockFocus(lock: boolean, getElement: () => HTMLElement | null): [ignoreElement: (ele: HTMLElement) => void];
+228
View File
@@ -0,0 +1,228 @@
import { useEffect, useRef, useState } from 'react';
import isVisible from "./isVisible";
import useId from "../hooks/useId";
function focusable(node, includePositive = false) {
if (isVisible(node)) {
const nodeName = node.nodeName.toLowerCase();
const isFocusableElement =
// Focusable element
['input', 'select', 'textarea', 'button'].includes(nodeName) ||
// Editable element
node.isContentEditable ||
// Anchor with href element
nodeName === 'a' && !!node.getAttribute('href');
// Get tabIndex
const tabIndexAttr = node.getAttribute('tabindex');
const tabIndexNum = Number(tabIndexAttr);
// Parse as number if validate
let tabIndex = null;
if (tabIndexAttr && !Number.isNaN(tabIndexNum)) {
tabIndex = tabIndexNum;
} else if (isFocusableElement && tabIndex === null) {
tabIndex = 0;
}
// Block focusable if disabled
if (isFocusableElement && node.disabled) {
tabIndex = null;
}
return tabIndex !== null && (tabIndex >= 0 || includePositive && tabIndex < 0);
}
return false;
}
export function getFocusNodeList(node, includePositive = false) {
const res = [...node.querySelectorAll('*')].filter(child => {
return focusable(child, includePositive);
});
if (focusable(node, includePositive)) {
res.unshift(node);
}
return res;
}
// Used for `rc-input` `rc-textarea` `rc-input-number`
/**
* Focus element and set cursor position for input/textarea elements.
*/
export function triggerFocus(element, option) {
if (!element) return;
element.focus(option);
// Selection content
const {
cursor
} = option || {};
if (cursor && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
const len = element.value.length;
switch (cursor) {
case 'start':
element.setSelectionRange(0, 0);
break;
case 'end':
element.setSelectionRange(len, len);
break;
default:
element.setSelectionRange(0, len);
}
}
}
// ======================================================
// == Lock Focus ==
// ======================================================
let lastFocusElement = null;
let focusElements = [];
// Map stable ID to lock element
const idToElementMap = new Map();
// Map stable ID to ignored element
const ignoredElementMap = new Map();
function getLastElement() {
return focusElements[focusElements.length - 1];
}
function isIgnoredElement(element) {
const lastElement = getLastElement();
if (element && lastElement) {
// Find the ID that maps to the last element
let lockId;
for (const [id, ele] of idToElementMap.entries()) {
if (ele === lastElement) {
lockId = id;
break;
}
}
const ignoredEle = ignoredElementMap.get(lockId);
return !!ignoredEle && (ignoredEle === element || ignoredEle.contains(element));
}
return false;
}
function hasFocus(element) {
const {
activeElement
} = document;
return element === activeElement || element.contains(activeElement);
}
function syncFocus() {
const lastElement = getLastElement();
const {
activeElement
} = document;
// If current focus is on an ignored element, don't force it back
if (isIgnoredElement(activeElement)) {
return;
}
if (lastElement && !hasFocus(lastElement)) {
const focusableList = getFocusNodeList(lastElement);
const matchElement = focusableList.includes(lastFocusElement) ? lastFocusElement : focusableList[0];
matchElement?.focus({
preventScroll: true
});
} else {
lastFocusElement = activeElement;
}
}
function onWindowKeyDown(e) {
if (e.key === 'Tab') {
const {
activeElement
} = document;
const lastElement = getLastElement();
const focusableList = getFocusNodeList(lastElement);
const last = focusableList[focusableList.length - 1];
if (e.shiftKey && activeElement === focusableList[0]) {
// Tab backward on first focusable element
lastFocusElement = last;
} else if (!e.shiftKey && activeElement === last) {
// Tab forward on last focusable element
lastFocusElement = focusableList[0];
}
}
}
/**
* Lock focus in the element.
* It will force back to the first focusable element when focus leaves the element.
* @param id - A stable ID for this lock instance
*/
export function lockFocus(element, id) {
if (element) {
// Store the mapping between ID and element
idToElementMap.set(id, element);
// Refresh focus elements
focusElements = focusElements.filter(ele => ele !== element);
focusElements.push(element);
// Just add event since it will de-duplicate
window.addEventListener('focusin', syncFocus);
window.addEventListener('keydown', onWindowKeyDown, true);
syncFocus();
}
// Always return unregister function
return () => {
lastFocusElement = null;
focusElements = focusElements.filter(ele => ele !== element);
idToElementMap.delete(id);
ignoredElementMap.delete(id);
if (focusElements.length === 0) {
window.removeEventListener('focusin', syncFocus);
window.removeEventListener('keydown', onWindowKeyDown, true);
}
};
}
/**
* Retry an effect until it reports ready.
* When `ready` is `false`, it will schedule one more effect cycle and call `func` again
* with the next `retryTimes`.
*/
function useRetryEffect(func, deps) {
/* eslint-disable react-hooks/exhaustive-deps */
const retryTimesRef = useRef(0);
const [retryMark, setRetryMark] = useState(0);
useEffect(() => {
retryTimesRef.current = 0;
}, deps);
useEffect(() => {
const [clearFn, ready] = func(retryTimesRef.current);
if (!ready) {
retryTimesRef.current += 1;
setRetryMark(count => count + 1);
}
return clearFn;
}, [...deps, retryMark]);
/* eslint-enable react-hooks/exhaustive-deps */
}
/**
* Lock focus within an element.
* When locked, focus will be restricted to focusable elements within the specified element.
* If multiple elements are locked, only the last locked element will be effective.
* @returns A function to mark an element as ignored, which will temporarily allow focus on that element even if it's outside the locked area.
*/
export function useLockFocus(lock, getElement) {
const id = useId();
const getElementRef = useRef(getElement);
getElementRef.current = getElement;
const lockEffect = retryTimes => {
if (!lock) {
return [undefined, true];
}
const element = getElementRef.current();
if (element) {
return [lockFocus(element, id), true];
}
return [undefined, retryTimes >= 1];
};
useRetryEffect(lockEffect, [id, lock]);
const ignoreElement = ele => {
if (ele) {
// Set the ignored element using stable ID
ignoredElementMap.set(id, ele);
}
};
return [ignoreElement];
}
@@ -0,0 +1,2 @@
declare const _default: (element: Element) => boolean;
export default _default;
+29
View File
@@ -0,0 +1,29 @@
export default (element => {
if (!element) {
return false;
}
if (element instanceof Element) {
if (element.offsetParent) {
return true;
}
if (element.getBBox) {
const {
width,
height
} = element.getBBox();
if (width || height) {
return true;
}
}
if (element.getBoundingClientRect) {
const {
width,
height
} = element.getBoundingClientRect();
if (width || height) {
return true;
}
}
}
return false;
});
@@ -0,0 +1,12 @@
export interface scrollLockOptions {
container: HTMLElement;
}
export default class ScrollLocker {
private lockTarget;
private options;
constructor(options?: scrollLockOptions);
getContainer: () => HTMLElement | undefined;
reLock: (options?: scrollLockOptions) => void;
lock: () => void;
unLock: () => void;
}
+109
View File
@@ -0,0 +1,109 @@
import getScrollBarSize from "../getScrollBarSize";
import setStyle from "../setStyle";
let uuid = 0;
let locks = [];
const scrollingEffectClassName = 'ant-scrolling-effect';
const scrollingEffectClassNameReg = new RegExp(`${scrollingEffectClassName}`, 'g');
// https://github.com/ant-design/ant-design/issues/19340
// https://github.com/ant-design/ant-design/issues/19332
const cacheStyle = new Map();
export default class ScrollLocker {
lockTarget;
options;
constructor(options) {
// eslint-disable-next-line no-plusplus
this.lockTarget = uuid++;
this.options = options;
}
getContainer = () => {
return this.options?.container;
};
// if options change...
reLock = options => {
const findLock = locks.find(({
target
}) => target === this.lockTarget);
if (findLock) {
this.unLock();
}
this.options = options;
if (findLock) {
findLock.options = options;
this.lock();
}
};
lock = () => {
// If lockTarget exist return
if (locks.some(({
target
}) => target === this.lockTarget)) {
return;
}
// If same container effect, return
if (locks.some(({
options
}) => options?.container === this.options?.container)) {
locks = [...locks, {
target: this.lockTarget,
options: this.options
}];
return;
}
let scrollBarSize = 0;
const container = this.options?.container || document.body;
if (container === document.body && window.innerWidth - document.documentElement.clientWidth > 0 || container.scrollHeight > container.clientHeight) {
if (getComputedStyle(container).overflow !== 'hidden') {
scrollBarSize = getScrollBarSize();
}
}
const containerClassName = container.className;
if (locks.filter(({
options
}) => options?.container === this.options?.container).length === 0) {
cacheStyle.set(container, setStyle({
width: scrollBarSize !== 0 ? `calc(100% - ${scrollBarSize}px)` : undefined,
overflow: 'hidden',
overflowX: 'hidden',
overflowY: 'hidden'
}, {
element: container
}));
}
// https://github.com/ant-design/ant-design/issues/19729
if (!scrollingEffectClassNameReg.test(containerClassName)) {
const addClassName = `${containerClassName} ${scrollingEffectClassName}`;
container.className = addClassName.trim();
}
locks = [...locks, {
target: this.lockTarget,
options: this.options
}];
};
unLock = () => {
const findLock = locks.find(({
target
}) => target === this.lockTarget);
locks = locks.filter(({
target
}) => target !== this.lockTarget);
if (!findLock || locks.some(({
options
}) => options?.container === findLock.options?.container)) {
return;
}
// Remove Effect
const container = this.options?.container || document.body;
const containerClassName = container.className;
if (!scrollingEffectClassNameReg.test(containerClassName)) return;
setStyle(cacheStyle.get(container), {
element: container
});
cacheStyle.delete(container);
container.className = container.className.replace(scrollingEffectClassNameReg, '').trim();
};
}
+8
View File
@@ -0,0 +1,8 @@
/**
* Check if is in shadowRoot
*/
export declare function inShadow(ele: Node): boolean;
/**
* Return shadowRoot if possible
*/
export declare function getShadowRoot(ele: Node): ShadowRoot;
+17
View File
@@ -0,0 +1,17 @@
function getRoot(ele) {
return ele?.getRootNode?.();
}
/**
* Check if is in shadowRoot
*/
export function inShadow(ele) {
return getRoot(ele) instanceof ShadowRoot;
}
/**
* Return shadowRoot if possible
*/
export function getShadowRoot(ele) {
return inShadow(ele) ? getRoot(ele) : null;
}
@@ -0,0 +1,2 @@
export declare function isStyleSupport(styleName: string | string[]): boolean;
export declare function isStyleSupport(styleName: string, styleValue: any): boolean;
+26
View File
@@ -0,0 +1,26 @@
import canUseDom from "./canUseDom";
const isStyleNameSupport = styleName => {
if (canUseDom() && window.document.documentElement) {
const styleNameList = Array.isArray(styleName) ? styleName : [styleName];
const {
documentElement
} = window.document;
return styleNameList.some(name => name in documentElement.style);
}
return false;
};
const isStyleValueSupport = (styleName, value) => {
if (!isStyleNameSupport(styleName)) {
return false;
}
const ele = document.createElement('div');
const origin = ele.style[styleName];
ele.style[styleName] = value;
return ele.style[styleName] !== origin;
};
export function isStyleSupport(styleName, styleValue) {
if (!Array.isArray(styleName) && styleValue !== undefined) {
return isStyleValueSupport(styleName, styleValue);
}
return isStyleNameSupport(styleName);
}