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
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-present react-component
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.
+54
View File
@@ -0,0 +1,54 @@
# rc-portal
React 18 supported Portal Component.
[![NPM version][npm-image]][npm-url] [![dumi](https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square)](https://github.com/umijs/dumi) [![build status][github-actions-image]][github-actions-url] [![Codecov][codecov-image]][codecov-url] [![npm download][download-image]][download-url]
[npm-image]: http://img.shields.io/npm/v/rc-portal.svg?style=flat-square
[npm-url]: http://npmjs.org/package/rc-portal
[github-actions-image]: https://github.com/react-component/portal/workflows/CI/badge.svg
[github-actions-url]: https://github.com/react-component/portal/actions
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/portal/master.svg?style=flat-square
[codecov-url]: https://app.codecov.io/gh/react-component/portal
[download-image]: https://img.shields.io/npm/dm/rc-portal.svg?style=flat-square
[download-url]: https://npmjs.org/package/rc-portal
## Development
```bash
npm install
npm start
open http://localhost:8000
```
## Feature
- React life cycle support portal component
## Install
[![rc-portal](https://nodei.co/npm/rc-portal.png)](https://npmjs.org/package/rc-portal)
## Usage
```js | pure
import Portal from '@rc-component/portal';
const Demo = () => {
return <Portal open>Hello World</Portal>;
};
export default Demo;
```
## 🔥 API
We use typescript to create the Type definition. You can view directly in IDE. But you can still check the type definition [here](https://github.com/react-component/portal/blob/master/src/interface.ts).
### Portal
| Prop | Description | Type | Default |
| ------------ | ---------------------------------- | ------------------------ | ------------- |
| getContainer | Customize portal container element | Element \| () => Element | document.body |
| open | Show the portal | boolean | false |
| autoLock | Lock screen scroll when open | boolean | false |
+4
View File
@@ -0,0 +1,4 @@
import * as React from 'react';
export type QueueCreate = (appendFunc: VoidFunction) => void;
declare const OrderContext: React.Context<QueueCreate>;
export default OrderContext;
+3
View File
@@ -0,0 +1,3 @@
import * as React from 'react';
const OrderContext = /*#__PURE__*/React.createContext(null);
export default OrderContext;
+23
View File
@@ -0,0 +1,23 @@
import * as React from 'react';
export type ContainerType = Element | DocumentFragment;
export type GetContainer = string | ContainerType | (() => ContainerType) | false;
export type EscCallback = ({ top, event, }: {
top: boolean;
event: KeyboardEvent;
}) => void;
export interface PortalProps {
/** Customize container element. Default will create a div in document.body when `open` */
getContainer?: GetContainer;
children?: React.ReactNode;
/** Show the portal children */
open?: boolean;
/** Remove `children` when `open` is `false`. Set `false` will not handle remove process */
autoDestroy?: boolean;
/** Lock screen scroll when open */
autoLock?: boolean;
onEsc?: EscCallback;
/** @private debug name. Do not use in prod */
debug?: string;
}
declare const Portal: React.ForwardRefExoticComponent<PortalProps & React.RefAttributes<any>>;
export default Portal;
+100
View File
@@ -0,0 +1,100 @@
import canUseDom from "@rc-component/util/es/Dom/canUseDom";
import { getNodeRef, supportRef, useComposeRef } from "@rc-component/util/es/ref";
import warning from "@rc-component/util/es/warning";
import * as React from 'react';
import { createPortal } from 'react-dom';
import OrderContext from "./Context";
import { inlineMock } from "./mock";
import useDom from "./useDom";
import useScrollLocker from "./useScrollLocker";
import useEscKeyDown from "./useEscKeyDown";
const getPortalContainer = getContainer => {
if (getContainer === false) {
return false;
}
if (!canUseDom() || !getContainer) {
return null;
}
if (typeof getContainer === 'string') {
return document.querySelector(getContainer);
}
if (typeof getContainer === 'function') {
return getContainer();
}
return getContainer;
};
const Portal = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
open,
autoLock,
getContainer,
debug,
autoDestroy = true,
children,
onEsc
} = props;
const [shouldRender, setShouldRender] = React.useState(open);
const mergedRender = shouldRender || open;
// ========================= Warning =========================
if (process.env.NODE_ENV !== 'production') {
warning(canUseDom() || !open, `Portal only work in client side. Please call 'useEffect' to show Portal instead default render in SSR.`);
}
// ====================== Should Render ======================
React.useEffect(() => {
if (autoDestroy || open) {
setShouldRender(open);
}
}, [open, autoDestroy]);
// ======================== Container ========================
const [innerContainer, setInnerContainer] = React.useState(() => getPortalContainer(getContainer));
React.useEffect(() => {
const customizeContainer = getPortalContainer(getContainer);
// Tell component that we check this in effect which is safe to be `null`
setInnerContainer(() =>
// React do the state update even the value is the same,
// Use function call to force React to compare update
customizeContainer ?? null);
});
const [defaultContainer, queueCreate] = useDom(mergedRender && !innerContainer, debug);
const mergedContainer = innerContainer ?? defaultContainer;
// ========================= Locker ==========================
useScrollLocker(autoLock && open && canUseDom() && (mergedContainer === defaultContainer || mergedContainer === document.body));
// ========================= Esc Keydown ==========================
useEscKeyDown(open, onEsc);
// =========================== Ref ===========================
let childRef = null;
if (children && supportRef(children) && ref) {
childRef = getNodeRef(children);
}
const mergedRef = useComposeRef(childRef, ref);
// ========================= Render ==========================
// Do not render when nothing need render
// When innerContainer is `undefined`, it may not ready since user use ref in the same render
if (!mergedRender || !canUseDom() || innerContainer === undefined) {
return null;
}
// Render inline
const renderInline = mergedContainer === false || inlineMock();
let reffedChildren = children;
if (ref) {
reffedChildren = /*#__PURE__*/React.cloneElement(children, {
ref: mergedRef
});
}
return /*#__PURE__*/React.createElement(OrderContext.Provider, {
value: queueCreate
}, renderInline ? reffedChildren : /*#__PURE__*/createPortal(reffedChildren, mergedContainer));
});
if (process.env.NODE_ENV !== 'production') {
Portal.displayName = 'Portal';
}
export default Portal;
+6
View File
@@ -0,0 +1,6 @@
import Portal from './Portal';
import type { PortalProps } from './Portal';
import { inlineMock } from './mock';
export type { PortalProps };
export { inlineMock };
export default Portal;
+4
View File
@@ -0,0 +1,4 @@
import Portal from "./Portal";
import { inlineMock } from "./mock";
export { inlineMock };
export default Portal;
+2
View File
@@ -0,0 +1,2 @@
export declare let inline: boolean;
export declare function inlineMock(nextInline?: boolean): boolean;
+7
View File
@@ -0,0 +1,7 @@
export let inline = false;
export function inlineMock(nextInline) {
if (typeof nextInline === 'boolean') {
inline = nextInline;
}
return inline;
}
+6
View File
@@ -0,0 +1,6 @@
import type { QueueCreate } from './Context';
/**
* Will add `div` to document. Nest call will keep order
* @param render Render DOM in document
*/
export default function useDom(render: boolean, debug?: string): [HTMLDivElement, QueueCreate];
+64
View File
@@ -0,0 +1,64 @@
import * as React from 'react';
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import canUseDom from "@rc-component/util/es/Dom/canUseDom";
import OrderContext from "./Context";
const EMPTY_LIST = [];
/**
* Will add `div` to document. Nest call will keep order
* @param render Render DOM in document
*/
export default function useDom(render, debug) {
const [ele] = React.useState(() => {
if (!canUseDom()) {
return null;
}
const defaultEle = document.createElement('div');
if (process.env.NODE_ENV !== 'production' && debug) {
defaultEle.setAttribute('data-debug', debug);
}
return defaultEle;
});
// ========================== Order ==========================
const appendedRef = React.useRef(false);
const queueCreate = React.useContext(OrderContext);
const [queue, setQueue] = React.useState(EMPTY_LIST);
const mergedQueueCreate = queueCreate || (appendedRef.current ? undefined : appendFn => {
setQueue(origin => {
const newQueue = [appendFn, ...origin];
return newQueue;
});
});
// =========================== DOM ===========================
function append() {
if (!ele.parentElement) {
document.body.appendChild(ele);
}
appendedRef.current = true;
}
function cleanup() {
ele.parentElement?.removeChild(ele);
appendedRef.current = false;
}
useLayoutEffect(() => {
if (render) {
if (queueCreate) {
queueCreate(append);
} else {
append();
}
} else {
cleanup();
}
return cleanup;
}, [render]);
useLayoutEffect(() => {
if (queue.length) {
queue.forEach(appendFn => appendFn());
setQueue(EMPTY_LIST);
}
}, [queue]);
return [ele, mergedQueueCreate];
}
@@ -0,0 +1,9 @@
import { type EscCallback } from './Portal';
export declare const _test: () => {
stack: {
id: string;
onEsc?: EscCallback;
}[];
reset: () => void;
};
export default function useEscKeyDown(open: boolean, onEsc?: EscCallback): void;
+79
View File
@@ -0,0 +1,79 @@
import { useEvent } from '@rc-component/util';
import useId from "@rc-component/util/es/hooks/useId";
import { useEffect, useMemo } from 'react';
let stack = [];
const IME_LOCK_DURATION = 200;
let lastCompositionEndTime = 0;
// Export for testing
export const _test = process.env.NODE_ENV === 'test' ? () => ({
stack,
reset: () => {
// Not reset stack to ensure effect will clean up correctly
lastCompositionEndTime = 0;
}
}) : null;
// Global event handlers
const onGlobalKeyDown = event => {
if (event.key === 'Escape' && !event.isComposing) {
const now = Date.now();
if (now - lastCompositionEndTime < IME_LOCK_DURATION) {
return;
}
const len = stack.length;
for (let i = len - 1; i >= 0; i -= 1) {
stack[i].onEsc({
top: i === len - 1,
event
});
}
}
};
const onGlobalCompositionEnd = () => {
lastCompositionEndTime = Date.now();
};
function attachGlobalEventListeners() {
window.addEventListener('keydown', onGlobalKeyDown);
window.addEventListener('compositionend', onGlobalCompositionEnd);
}
function detachGlobalEventListeners() {
if (stack.length === 0) {
window.removeEventListener('keydown', onGlobalKeyDown);
window.removeEventListener('compositionend', onGlobalCompositionEnd);
}
}
export default function useEscKeyDown(open, onEsc) {
const id = useId();
const onEventEsc = useEvent(onEsc);
const ensure = () => {
if (!stack.find(item => item.id === id)) {
stack.push({
id,
onEsc: onEventEsc
});
}
};
const clear = () => {
stack = stack.filter(item => item.id !== id);
};
useMemo(() => {
if (open) {
ensure();
} else if (!open) {
clear();
}
}, [open]);
useEffect(() => {
if (open) {
ensure();
// Attach global event listeners
attachGlobalEventListeners();
return () => {
clear();
// Remove global event listeners if instances is empty
detachGlobalEventListeners();
};
}
}, [open]);
}
@@ -0,0 +1 @@
export default function useScrollLocker(lock?: boolean): void;
@@ -0,0 +1,30 @@
import * as React from 'react';
import { updateCSS, removeCSS } from "@rc-component/util/es/Dom/dynamicCSS";
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import { getTargetScrollBarSize } from "@rc-component/util/es/getScrollBarSize";
import { isBodyOverflowing } from "./util";
const UNIQUE_ID = `rc-util-locker-${Date.now()}`;
let uuid = 0;
export default function useScrollLocker(lock) {
const mergedLock = !!lock;
const [id] = React.useState(() => {
uuid += 1;
return `${UNIQUE_ID}_${uuid}`;
});
useLayoutEffect(() => {
if (mergedLock) {
const scrollbarSize = getTargetScrollBarSize(document.body).width;
const isOverflow = isBodyOverflowing();
updateCSS(`
html body {
overflow-y: hidden;
${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''}
}`, id);
} else {
removeCSS(id);
}
return () => {
removeCSS(id);
};
}, [mergedLock, id]);
}
+4
View File
@@ -0,0 +1,4 @@
/**
* Test usage export. Do not use in your production
*/
export declare function isBodyOverflowing(): boolean;
+6
View File
@@ -0,0 +1,6 @@
/**
* Test usage export. Do not use in your production
*/
export function isBodyOverflowing() {
return document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) && window.innerWidth > document.body.offsetWidth;
}
+4
View File
@@ -0,0 +1,4 @@
import * as React from 'react';
export type QueueCreate = (appendFunc: VoidFunction) => void;
declare const OrderContext: React.Context<QueueCreate>;
export default OrderContext;
+11
View File
@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const OrderContext = /*#__PURE__*/React.createContext(null);
var _default = exports.default = OrderContext;
+23
View File
@@ -0,0 +1,23 @@
import * as React from 'react';
export type ContainerType = Element | DocumentFragment;
export type GetContainer = string | ContainerType | (() => ContainerType) | false;
export type EscCallback = ({ top, event, }: {
top: boolean;
event: KeyboardEvent;
}) => void;
export interface PortalProps {
/** Customize container element. Default will create a div in document.body when `open` */
getContainer?: GetContainer;
children?: React.ReactNode;
/** Show the portal children */
open?: boolean;
/** Remove `children` when `open` is `false`. Set `false` will not handle remove process */
autoDestroy?: boolean;
/** Lock screen scroll when open */
autoLock?: boolean;
onEsc?: EscCallback;
/** @private debug name. Do not use in prod */
debug?: string;
}
declare const Portal: React.ForwardRefExoticComponent<PortalProps & React.RefAttributes<any>>;
export default Portal;
+109
View File
@@ -0,0 +1,109 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _canUseDom = _interopRequireDefault(require("@rc-component/util/lib/Dom/canUseDom"));
var _ref = require("@rc-component/util/lib/ref");
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
var React = _interopRequireWildcard(require("react"));
var _reactDom = require("react-dom");
var _Context = _interopRequireDefault(require("./Context"));
var _mock = require("./mock");
var _useDom = _interopRequireDefault(require("./useDom"));
var _useScrollLocker = _interopRequireDefault(require("./useScrollLocker"));
var _useEscKeyDown = _interopRequireDefault(require("./useEscKeyDown"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const getPortalContainer = getContainer => {
if (getContainer === false) {
return false;
}
if (!(0, _canUseDom.default)() || !getContainer) {
return null;
}
if (typeof getContainer === 'string') {
return document.querySelector(getContainer);
}
if (typeof getContainer === 'function') {
return getContainer();
}
return getContainer;
};
const Portal = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
open,
autoLock,
getContainer,
debug,
autoDestroy = true,
children,
onEsc
} = props;
const [shouldRender, setShouldRender] = React.useState(open);
const mergedRender = shouldRender || open;
// ========================= Warning =========================
if (process.env.NODE_ENV !== 'production') {
(0, _warning.default)((0, _canUseDom.default)() || !open, `Portal only work in client side. Please call 'useEffect' to show Portal instead default render in SSR.`);
}
// ====================== Should Render ======================
React.useEffect(() => {
if (autoDestroy || open) {
setShouldRender(open);
}
}, [open, autoDestroy]);
// ======================== Container ========================
const [innerContainer, setInnerContainer] = React.useState(() => getPortalContainer(getContainer));
React.useEffect(() => {
const customizeContainer = getPortalContainer(getContainer);
// Tell component that we check this in effect which is safe to be `null`
setInnerContainer(() =>
// React do the state update even the value is the same,
// Use function call to force React to compare update
customizeContainer ?? null);
});
const [defaultContainer, queueCreate] = (0, _useDom.default)(mergedRender && !innerContainer, debug);
const mergedContainer = innerContainer ?? defaultContainer;
// ========================= Locker ==========================
(0, _useScrollLocker.default)(autoLock && open && (0, _canUseDom.default)() && (mergedContainer === defaultContainer || mergedContainer === document.body));
// ========================= Esc Keydown ==========================
(0, _useEscKeyDown.default)(open, onEsc);
// =========================== Ref ===========================
let childRef = null;
if (children && (0, _ref.supportRef)(children) && ref) {
childRef = (0, _ref.getNodeRef)(children);
}
const mergedRef = (0, _ref.useComposeRef)(childRef, ref);
// ========================= Render ==========================
// Do not render when nothing need render
// When innerContainer is `undefined`, it may not ready since user use ref in the same render
if (!mergedRender || !(0, _canUseDom.default)() || innerContainer === undefined) {
return null;
}
// Render inline
const renderInline = mergedContainer === false || (0, _mock.inlineMock)();
let reffedChildren = children;
if (ref) {
reffedChildren = /*#__PURE__*/React.cloneElement(children, {
ref: mergedRef
});
}
return /*#__PURE__*/React.createElement(_Context.default.Provider, {
value: queueCreate
}, renderInline ? reffedChildren : /*#__PURE__*/(0, _reactDom.createPortal)(reffedChildren, mergedContainer));
});
if (process.env.NODE_ENV !== 'production') {
Portal.displayName = 'Portal';
}
var _default = exports.default = Portal;
+6
View File
@@ -0,0 +1,6 @@
import Portal from './Portal';
import type { PortalProps } from './Portal';
import { inlineMock } from './mock';
export type { PortalProps };
export { inlineMock };
export default Portal;
+16
View File
@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
Object.defineProperty(exports, "inlineMock", {
enumerable: true,
get: function () {
return _mock.inlineMock;
}
});
var _Portal = _interopRequireDefault(require("./Portal"));
var _mock = require("./mock");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var _default = exports.default = _Portal.default;
+2
View File
@@ -0,0 +1,2 @@
export declare let inline: boolean;
export declare function inlineMock(nextInline?: boolean): boolean;
+14
View File
@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.inline = void 0;
exports.inlineMock = inlineMock;
let inline = exports.inline = false;
function inlineMock(nextInline) {
if (typeof nextInline === 'boolean') {
exports.inline = inline = nextInline;
}
return inline;
}
+6
View File
@@ -0,0 +1,6 @@
import type { QueueCreate } from './Context';
/**
* Will add `div` to document. Nest call will keep order
* @param render Render DOM in document
*/
export default function useDom(render: boolean, debug?: string): [HTMLDivElement, QueueCreate];
+73
View File
@@ -0,0 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useDom;
var React = _interopRequireWildcard(require("react"));
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _canUseDom = _interopRequireDefault(require("@rc-component/util/lib/Dom/canUseDom"));
var _Context = _interopRequireDefault(require("./Context"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const EMPTY_LIST = [];
/**
* Will add `div` to document. Nest call will keep order
* @param render Render DOM in document
*/
function useDom(render, debug) {
const [ele] = React.useState(() => {
if (!(0, _canUseDom.default)()) {
return null;
}
const defaultEle = document.createElement('div');
if (process.env.NODE_ENV !== 'production' && debug) {
defaultEle.setAttribute('data-debug', debug);
}
return defaultEle;
});
// ========================== Order ==========================
const appendedRef = React.useRef(false);
const queueCreate = React.useContext(_Context.default);
const [queue, setQueue] = React.useState(EMPTY_LIST);
const mergedQueueCreate = queueCreate || (appendedRef.current ? undefined : appendFn => {
setQueue(origin => {
const newQueue = [appendFn, ...origin];
return newQueue;
});
});
// =========================== DOM ===========================
function append() {
if (!ele.parentElement) {
document.body.appendChild(ele);
}
appendedRef.current = true;
}
function cleanup() {
ele.parentElement?.removeChild(ele);
appendedRef.current = false;
}
(0, _useLayoutEffect.default)(() => {
if (render) {
if (queueCreate) {
queueCreate(append);
} else {
append();
}
} else {
cleanup();
}
return cleanup;
}, [render]);
(0, _useLayoutEffect.default)(() => {
if (queue.length) {
queue.forEach(appendFn => appendFn());
setQueue(EMPTY_LIST);
}
}, [queue]);
return [ele, mergedQueueCreate];
}
@@ -0,0 +1,9 @@
import { type EscCallback } from './Portal';
export declare const _test: () => {
stack: {
id: string;
onEsc?: EscCallback;
}[];
reset: () => void;
};
export default function useEscKeyDown(open: boolean, onEsc?: EscCallback): void;
+87
View File
@@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports._test = void 0;
exports.default = useEscKeyDown;
var _util = require("@rc-component/util");
var _useId = _interopRequireDefault(require("@rc-component/util/lib/hooks/useId"));
var _react = require("react");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
let stack = [];
const IME_LOCK_DURATION = 200;
let lastCompositionEndTime = 0;
// Export for testing
const _test = exports._test = process.env.NODE_ENV === 'test' ? () => ({
stack,
reset: () => {
// Not reset stack to ensure effect will clean up correctly
lastCompositionEndTime = 0;
}
}) : null;
// Global event handlers
const onGlobalKeyDown = event => {
if (event.key === 'Escape' && !event.isComposing) {
const now = Date.now();
if (now - lastCompositionEndTime < IME_LOCK_DURATION) {
return;
}
const len = stack.length;
for (let i = len - 1; i >= 0; i -= 1) {
stack[i].onEsc({
top: i === len - 1,
event
});
}
}
};
const onGlobalCompositionEnd = () => {
lastCompositionEndTime = Date.now();
};
function attachGlobalEventListeners() {
window.addEventListener('keydown', onGlobalKeyDown);
window.addEventListener('compositionend', onGlobalCompositionEnd);
}
function detachGlobalEventListeners() {
if (stack.length === 0) {
window.removeEventListener('keydown', onGlobalKeyDown);
window.removeEventListener('compositionend', onGlobalCompositionEnd);
}
}
function useEscKeyDown(open, onEsc) {
const id = (0, _useId.default)();
const onEventEsc = (0, _util.useEvent)(onEsc);
const ensure = () => {
if (!stack.find(item => item.id === id)) {
stack.push({
id,
onEsc: onEventEsc
});
}
};
const clear = () => {
stack = stack.filter(item => item.id !== id);
};
(0, _react.useMemo)(() => {
if (open) {
ensure();
} else if (!open) {
clear();
}
}, [open]);
(0, _react.useEffect)(() => {
if (open) {
ensure();
// Attach global event listeners
attachGlobalEventListeners();
return () => {
clear();
// Remove global event listeners if instances is empty
detachGlobalEventListeners();
};
}
}, [open]);
}
@@ -0,0 +1 @@
export default function useScrollLocker(lock?: boolean): void;
@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useScrollLocker;
var React = _interopRequireWildcard(require("react"));
var _dynamicCSS = require("@rc-component/util/lib/Dom/dynamicCSS");
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _getScrollBarSize = require("@rc-component/util/lib/getScrollBarSize");
var _util = require("./util");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const UNIQUE_ID = `rc-util-locker-${Date.now()}`;
let uuid = 0;
function useScrollLocker(lock) {
const mergedLock = !!lock;
const [id] = React.useState(() => {
uuid += 1;
return `${UNIQUE_ID}_${uuid}`;
});
(0, _useLayoutEffect.default)(() => {
if (mergedLock) {
const scrollbarSize = (0, _getScrollBarSize.getTargetScrollBarSize)(document.body).width;
const isOverflow = (0, _util.isBodyOverflowing)();
(0, _dynamicCSS.updateCSS)(`
html body {
overflow-y: hidden;
${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''}
}`, id);
} else {
(0, _dynamicCSS.removeCSS)(id);
}
return () => {
(0, _dynamicCSS.removeCSS)(id);
};
}, [mergedLock, id]);
}
+4
View File
@@ -0,0 +1,4 @@
/**
* Test usage export. Do not use in your production
*/
export declare function isBodyOverflowing(): boolean;
+12
View File
@@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isBodyOverflowing = isBodyOverflowing;
/**
* Test usage export. Do not use in your production
*/
function isBodyOverflowing() {
return document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) && window.innerWidth > document.body.offsetWidth;
}
+78
View File
@@ -0,0 +1,78 @@
{
"name": "@rc-component/portal",
"version": "2.2.0",
"description": "React Portal Component",
"keywords": [
"react",
"react-component",
"react-portal"
],
"homepage": "https://github.com/react-component/portal",
"bugs": {
"url": "https://github.com/react-component/portal/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/react-component/portal.git"
},
"license": "MIT",
"author": "smith3816@gmail.com",
"main": "./lib/index",
"module": "./es/index",
"typings": "es/index.d.ts",
"files": [
"lib",
"es",
"dist"
],
"scripts": {
"compile": "father build",
"deploy": "npm run docs:build && npm run docs:deploy",
"docs:build": "dumi build",
"docs:deploy": "gh-pages -d docs-dist",
"lint": "eslint src/ --ext .tsx,.ts",
"lint:tsc": "tsc -p tsconfig.json --noEmit",
"now-build": "npm run docs:build",
"prepare": "dumi setup",
"prepublishOnly": "npm run compile && rc-np",
"prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"",
"start": "dumi dev",
"test": "npm run test:server && npm run test:coverage",
"test:client": "rc-test --testPathIgnorePatterns=ssr.test.tsx --testPathIgnorePatterns=ssr.test.tsx",
"test:coverage": "npm run test:client -- --coverage",
"test:server": "rc-test --env=node -- tests/ssr.test.tsx",
"watch": "father dev"
},
"dependencies": {
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"devDependencies": {
"@rc-component/father-plugin": "^2.0.2",
"@rc-component/np": "^1.0.3",
"@testing-library/jest-dom": "^6.9.0",
"@testing-library/react": "^13.0.0",
"@types/jest": "^26.0.20",
"@types/node": "^20.9.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@umijs/fabric": "^4.0.0",
"dumi": "^2.0.0",
"eslint": "^8.0.0",
"father": "^4.0.0",
"gh-pages": "^6.1.1",
"glob": "^10.0.0",
"prettier": "^2.1.2",
"rc-test": "^7.1.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.0.0"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"engines": {
"node": ">=12.x"
}
}