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
+39
View File
@@ -0,0 +1,39 @@
import type { PortalProps } from '@rc-component/portal';
import * as React from 'react';
import type { DrawerPanelAccessibility, DrawerPanelEvents } from './DrawerPanel';
import type { DrawerPopupProps } from './DrawerPopup';
import type { DrawerClassNames, DrawerStyles } from './inter';
export type Placement = 'left' | 'top' | 'right' | 'bottom';
export interface DrawerProps extends Omit<DrawerPopupProps, 'prefixCls' | 'inline' | 'scrollLocker'>, DrawerPanelEvents, DrawerPanelAccessibility {
prefixCls?: string;
open?: boolean;
onClose?: (e: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => void;
destroyOnHidden?: boolean;
getContainer?: PortalProps['getContainer'];
panelRef?: React.Ref<HTMLDivElement>;
classNames?: DrawerClassNames;
styles?: DrawerStyles;
/**
* @deprecated Use `size` instead. Will be removed in next major version.
*/
width?: number | string;
/**
* @deprecated Use `size` instead. Will be removed in next major version.
*/
height?: number | string;
/** Size of the drawer (width for left/right placement, height for top/bottom placement) */
size?: number | string;
/** Maximum size of the drawer */
maxSize?: number;
/** Default size for uncontrolled resizable drawer */
defaultSize?: number | string;
/** Resizable configuration - boolean to enable/disable or object with optional callbacks */
resizable?: boolean | {
onResize?: (size: number) => void;
onResizeStart?: () => void;
onResizeEnd?: () => void;
};
focusTriggerAfterClose?: boolean;
}
declare const Drawer: React.FC<DrawerProps>;
export default Drawer;
+130
View File
@@ -0,0 +1,130 @@
import Portal from '@rc-component/portal';
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import * as React from 'react';
import { RefContext } from "./context";
import DrawerPopup from "./DrawerPopup";
import { warnCheck } from "./util";
const Drawer = props => {
const {
open = false,
prefixCls = 'rc-drawer',
placement = 'right',
autoFocus = true,
keyboard = true,
width,
height,
size,
maxSize,
mask = true,
maskClosable = true,
getContainer,
forceRender,
afterOpenChange,
destroyOnHidden,
onMouseEnter,
onMouseOver,
onMouseLeave,
onClick,
onKeyDown,
onKeyUp,
onClose,
resizable,
defaultSize,
focusTriggerAfterClose,
// Refs
panelRef
} = props;
const [animatedVisible, setAnimatedVisible] = React.useState(false);
// ============================= Warn =============================
if (process.env.NODE_ENV !== 'production') {
warnCheck(props);
}
// ============================= Open =============================
const [mounted, setMounted] = React.useState(false);
useLayoutEffect(() => {
setMounted(true);
}, []);
const mergedOpen = mounted ? open : false;
// ============================ Focus =============================
const popupRef = React.useRef(null);
const lastActiveRef = React.useRef(null);
useLayoutEffect(() => {
if (mergedOpen) {
lastActiveRef.current = document.activeElement;
}
}, [mergedOpen]);
// ============================= Open =============================
const internalAfterOpenChange = nextVisible => {
setAnimatedVisible(nextVisible);
afterOpenChange?.(nextVisible);
if (!nextVisible && focusTriggerAfterClose !== false && lastActiveRef.current && !popupRef.current?.contains(lastActiveRef.current)) {
lastActiveRef.current?.focus({
preventScroll: true
});
}
};
// =========================== Context ============================
const refContext = React.useMemo(() => ({
panel: panelRef
}), [panelRef]);
// ============================ Render ============================
if (!forceRender && !animatedVisible && !mergedOpen && destroyOnHidden) {
return null;
}
const eventHandlers = {
onMouseEnter,
onMouseOver,
onMouseLeave,
onClick,
onKeyDown,
onKeyUp
};
const drawerPopupProps = {
...props,
open: mergedOpen,
prefixCls,
placement,
autoFocus,
keyboard,
width,
height,
size,
maxSize,
defaultSize,
mask,
maskClosable,
inline: getContainer === false,
afterOpenChange: internalAfterOpenChange,
ref: popupRef,
resizable,
...eventHandlers
};
const onEsc = ({
top,
event
}) => {
if (top && keyboard) {
event.stopPropagation();
onClose?.(event);
}
};
return /*#__PURE__*/React.createElement(RefContext.Provider, {
value: refContext
}, /*#__PURE__*/React.createElement(Portal, {
open: mergedOpen || forceRender || animatedVisible,
autoDestroy: false,
getContainer: getContainer,
autoLock: mask && (mergedOpen || animatedVisible),
onEsc: onEsc
}, /*#__PURE__*/React.createElement(DrawerPopup, drawerPopupProps)));
};
if (process.env.NODE_ENV !== 'production') {
Drawer.displayName = 'Drawer';
}
export default Drawer;
+24
View File
@@ -0,0 +1,24 @@
import * as React from 'react';
export interface DrawerPanelRef {
focus: VoidFunction;
}
export interface DrawerPanelEvents {
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onMouseOver?: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
onClick?: React.MouseEventHandler<HTMLDivElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
onKeyUp?: React.KeyboardEventHandler<HTMLDivElement>;
onFocus?: React.FocusEventHandler<HTMLDivElement>;
}
export type DrawerPanelAccessibility = Pick<React.DialogHTMLAttributes<HTMLDivElement>, keyof React.AriaAttributes>;
export interface DrawerPanelProps extends DrawerPanelEvents, DrawerPanelAccessibility {
prefixCls: string;
className?: string;
id?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
containerRef?: React.Ref<HTMLDivElement>;
}
declare const DrawerPanel: React.FC<Readonly<DrawerPanelProps>>;
export default DrawerPanel;
+34
View File
@@ -0,0 +1,34 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import * as React from 'react';
import { RefContext } from "./context";
import pickAttrs from "@rc-component/util/es/pickAttrs";
import { useComposeRef } from "@rc-component/util/es/ref";
const DrawerPanel = props => {
const {
prefixCls,
className,
containerRef,
...restProps
} = props;
const {
panel: panelRef
} = React.useContext(RefContext);
const mergedRef = useComposeRef(panelRef, containerRef);
// =============================== Render ===============================
return /*#__PURE__*/React.createElement("div", _extends({
className: clsx(`${prefixCls}-section`, className),
role: "dialog",
ref: mergedRef
}, pickAttrs(props, {
aria: true
}), {
"aria-modal": "true"
}, restProps));
};
if (process.env.NODE_ENV !== 'production') {
DrawerPanel.displayName = 'DrawerPanel';
}
export default DrawerPanel;
+52
View File
@@ -0,0 +1,52 @@
import type { CSSMotionProps } from '@rc-component/motion';
import * as React from 'react';
import type { DrawerPanelAccessibility, DrawerPanelEvents } from './DrawerPanel';
import type { DrawerClassNames, DrawerStyles } from './inter';
export type Placement = 'left' | 'right' | 'top' | 'bottom';
export interface PushConfig {
distance?: number | string;
}
export interface DrawerPopupProps extends DrawerPanelEvents, DrawerPanelAccessibility {
prefixCls: string;
open?: boolean;
inline?: boolean;
push?: boolean | PushConfig;
forceRender?: boolean;
keyboard?: boolean;
autoFocus?: boolean;
focusTrap?: boolean;
rootClassName?: string;
rootStyle?: React.CSSProperties;
zIndex?: number;
placement?: Placement;
id?: string;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
width?: number | string;
height?: number | string;
/** Size of the drawer (width for left/right placement, height for top/bottom placement) */
size?: number | string;
/** Maximum size of the drawer */
maxSize?: number;
mask?: boolean;
maskClosable?: boolean;
maskClassName?: string;
maskStyle?: React.CSSProperties;
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
maskMotion?: CSSMotionProps;
afterOpenChange?: (open: boolean) => void;
onClose?: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => void;
classNames?: DrawerClassNames;
styles?: DrawerStyles;
drawerRender?: (node: React.ReactNode) => React.ReactNode;
/** Default size for uncontrolled resizable drawer */
defaultSize?: number | string;
resizable?: boolean | {
onResize?: (size: number) => void;
onResizeStart?: () => void;
onResizeEnd?: () => void;
};
}
declare const RefDrawerPopup: React.ForwardRefExoticComponent<DrawerPopupProps & React.RefAttributes<HTMLDivElement>>;
export default RefDrawerPopup;
+265
View File
@@ -0,0 +1,265 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import CSSMotion from '@rc-component/motion';
import pickAttrs from "@rc-component/util/es/pickAttrs";
import * as React from 'react';
import DrawerContext from "./context";
import DrawerPanel from "./DrawerPanel";
import useDrag from "./hooks/useDrag";
import { parseWidthHeight } from "./util";
import { useEvent } from '@rc-component/util';
import useFocusable from "./hooks/useFocusable";
const DrawerPopup = (props, ref) => {
const {
prefixCls,
open,
placement,
inline,
push,
forceRender,
// Focus
autoFocus,
focusTrap,
// classNames
classNames: drawerClassNames,
// Root
rootClassName,
rootStyle,
zIndex,
// Drawer
className,
id,
style,
motion,
width,
height,
size,
maxSize,
children,
// Mask
mask,
maskClosable,
maskMotion,
maskClassName,
maskStyle,
// Events
afterOpenChange,
onClose,
onMouseEnter,
onMouseOver,
onMouseLeave,
onClick,
onKeyDown,
onKeyUp,
styles,
drawerRender,
resizable,
defaultSize
} = props;
// ================================ Refs ================================
const panelRef = React.useRef(null);
React.useImperativeHandle(ref, () => panelRef.current);
// ========================= Focusable ==========================
const ignoreElement = useFocusable(() => panelRef.current, open, autoFocus, focusTrap, mask);
// ============================ Push ============================
const [pushed, setPushed] = React.useState(false);
const parentContext = React.useContext(DrawerContext);
// Merge push distance
let pushConfig;
if (typeof push === 'boolean') {
pushConfig = push ? {} : {
distance: 0
};
} else {
pushConfig = push || {};
}
const pushDistance = pushConfig?.distance ?? parentContext?.pushDistance ?? 180;
const mergedContext = React.useMemo(() => ({
pushDistance,
push: () => {
setPushed(true);
},
pull: () => {
setPushed(false);
}
}), [pushDistance]);
// ========================= ScrollLock =========================
// Tell parent to push
React.useEffect(() => {
if (open) {
parentContext?.push?.();
} else {
parentContext?.pull?.();
}
}, [open]);
// Clean up
React.useEffect(() => () => {
parentContext?.pull?.();
}, []);
// ============================ Mask ============================
const maskNode = /*#__PURE__*/React.createElement(CSSMotion, _extends({
key: "mask"
}, maskMotion, {
visible: mask && open
}), ({
className: motionMaskClassName,
style: motionMaskStyle
}, maskRef) => /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-mask`, motionMaskClassName, drawerClassNames?.mask, maskClassName),
style: {
...motionMaskStyle,
...maskStyle,
...styles?.mask
},
onClick: maskClosable && open ? onClose : undefined,
ref: maskRef
}));
// =========================== Panel ============================
const motionProps = typeof motion === 'function' ? motion(placement) : motion;
// ============================ Size ============================
const [currentSize, setCurrentSize] = React.useState();
const isHorizontal = placement === 'left' || placement === 'right';
// Aggregate size logic with backward compatibility using useMemo
const mergedSize = React.useMemo(() => {
const legacySize = isHorizontal ? width : height;
const nextMergedSize = size ?? legacySize ?? currentSize ?? defaultSize ?? (isHorizontal ? 378 : undefined);
return parseWidthHeight(nextMergedSize);
}, [size, width, height, defaultSize, isHorizontal, currentSize]);
// >>> Style
const wrapperStyle = React.useMemo(() => {
const nextWrapperStyle = {};
if (pushed && pushDistance) {
switch (placement) {
case 'top':
nextWrapperStyle.transform = `translateY(${pushDistance}px)`;
break;
case 'bottom':
nextWrapperStyle.transform = `translateY(${-pushDistance}px)`;
break;
case 'left':
nextWrapperStyle.transform = `translateX(${pushDistance}px)`;
break;
default:
nextWrapperStyle.transform = `translateX(${-pushDistance}px)`;
break;
}
}
if (isHorizontal) {
nextWrapperStyle.width = parseWidthHeight(mergedSize);
} else {
nextWrapperStyle.height = parseWidthHeight(mergedSize);
}
return nextWrapperStyle;
}, [pushed, pushDistance, placement, isHorizontal, mergedSize]);
// =========================== Resize ===========================
const wrapperRef = React.useRef(null);
const isResizable = !!resizable;
const resizeConfig = typeof resizable === 'object' && resizable || {};
const onInternalResize = useEvent(size => {
setCurrentSize(size);
resizeConfig.onResize?.(size);
});
const {
dragElementProps,
isDragging
} = useDrag({
prefixCls: `${prefixCls}-resizable`,
direction: placement,
className: drawerClassNames?.dragger,
style: styles?.dragger,
maxSize,
containerRef: wrapperRef,
currentSize: mergedSize,
onResize: onInternalResize,
onResizeStart: resizeConfig.onResizeStart,
onResizeEnd: resizeConfig.onResizeEnd
});
// =========================== Events ===========================
const eventHandlers = {
onMouseEnter,
onMouseOver,
onMouseLeave,
onClick,
onKeyDown,
onKeyUp,
onFocus: e => {
ignoreElement(e.target);
}
};
// =========================== Render ==========================
// >>>>> Panel
const panelNode = /*#__PURE__*/React.createElement(CSSMotion, _extends({
key: "panel"
}, motionProps, {
visible: open,
forceRender: forceRender,
onVisibleChanged: afterOpenChange,
removeOnLeave: false,
leavedClassName: `${prefixCls}-content-wrapper-hidden`
}), ({
className: motionClassName,
style: motionStyle
}, motionRef) => {
const content = /*#__PURE__*/React.createElement(DrawerPanel, _extends({
id: id,
containerRef: motionRef,
prefixCls: prefixCls,
className: clsx(className, drawerClassNames?.section),
style: {
...style,
...styles?.section
}
}, pickAttrs(props, {
aria: true
}), eventHandlers), children);
return /*#__PURE__*/React.createElement("div", _extends({
ref: wrapperRef,
className: clsx(`${prefixCls}-content-wrapper`, isDragging && `${prefixCls}-content-wrapper-dragging`, drawerClassNames?.wrapper, !isDragging && motionClassName),
style: {
...motionStyle,
...wrapperStyle,
...styles?.wrapper
}
}, pickAttrs(props, {
data: true
})), isResizable && /*#__PURE__*/React.createElement("div", dragElementProps), drawerRender ? drawerRender(content) : content);
});
// >>>>> Container
const containerStyle = {
...rootStyle
};
if (zIndex) {
containerStyle.zIndex = zIndex;
}
return /*#__PURE__*/React.createElement(DrawerContext.Provider, {
value: mergedContext
}, /*#__PURE__*/React.createElement("div", {
className: clsx(prefixCls, `${prefixCls}-${placement}`, rootClassName, {
[`${prefixCls}-open`]: open,
[`${prefixCls}-inline`]: inline
}),
style: containerStyle,
tabIndex: -1,
ref: panelRef
}, maskNode, panelNode));
};
const RefDrawerPopup = /*#__PURE__*/React.forwardRef(DrawerPopup);
if (process.env.NODE_ENV !== 'production') {
RefDrawerPopup.displayName = 'DrawerPopup';
}
export default RefDrawerPopup;
+12
View File
@@ -0,0 +1,12 @@
import * as React from 'react';
export interface DrawerContextProps {
pushDistance?: number | string;
push: VoidFunction;
pull: VoidFunction;
}
declare const DrawerContext: React.Context<DrawerContextProps>;
export interface RefContextProps {
panel?: React.Ref<HTMLDivElement>;
}
export declare const RefContext: React.Context<RefContextProps>;
export default DrawerContext;
+4
View File
@@ -0,0 +1,4 @@
import * as React from 'react';
const DrawerContext = /*#__PURE__*/React.createContext(null);
export const RefContext = /*#__PURE__*/React.createContext({});
export default DrawerContext;
@@ -0,0 +1,23 @@
import * as React from 'react';
import type { Placement } from '../Drawer';
export interface UseDragOptions {
prefixCls: string;
direction: Placement;
className?: string;
style?: React.CSSProperties;
maxSize?: number;
containerRef?: React.RefObject<HTMLElement>;
currentSize?: number | string;
onResize?: (size: number) => void;
onResizeEnd?: (size: number) => void;
onResizeStart?: (size: number) => void;
}
export interface UseDragReturn {
dragElementProps: {
className: string;
style: React.CSSProperties;
onMouseDown: (e: React.MouseEvent) => void;
};
isDragging: boolean;
}
export default function useDrag(options: UseDragOptions): UseDragReturn;
+98
View File
@@ -0,0 +1,98 @@
import * as React from 'react';
import { clsx } from 'clsx';
import { useEvent } from '@rc-component/util';
export default function useDrag(options) {
const {
prefixCls,
direction,
className,
style,
maxSize,
containerRef,
currentSize,
onResize,
onResizeEnd,
onResizeStart
} = options;
const [isDragging, setIsDragging] = React.useState(false);
const [startPos, setStartPos] = React.useState(0);
const [startSize, setStartSize] = React.useState(0);
const isHorizontal = direction === 'left' || direction === 'right';
const handleMouseDown = useEvent(e => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
if (isHorizontal) {
setStartPos(e.clientX);
} else {
setStartPos(e.clientY);
}
// Use provided currentSize, or fallback to container size
let startSize;
if (typeof currentSize === 'number') {
startSize = currentSize;
} else if (containerRef?.current) {
const rect = containerRef.current.getBoundingClientRect();
startSize = isHorizontal ? rect.width : rect.height;
}
setStartSize(startSize);
onResizeStart?.(startSize);
});
const handleMouseMove = useEvent(e => {
if (!isDragging) return;
const currentPos = isHorizontal ? e.clientX : e.clientY;
let delta = currentPos - startPos;
// Adjust delta direction based on placement
if (direction === 'right' || direction === 'bottom') {
delta = -delta;
}
let newSize = startSize + delta;
// Apply min/max size limits
if (newSize < 0) {
newSize = 0;
}
// Only apply maxSize if it's a valid positive number
if (maxSize && newSize > maxSize) {
newSize = maxSize;
}
onResize?.(newSize);
});
const handleMouseUp = React.useCallback(() => {
if (isDragging) {
setIsDragging(false);
// Get the final size after resize
if (containerRef?.current) {
const rect = containerRef.current.getBoundingClientRect();
const finalSize = isHorizontal ? rect.width : rect.height;
onResizeEnd?.(finalSize);
}
}
}, [isDragging, containerRef, onResizeEnd, isHorizontal]);
React.useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
}, [isDragging, handleMouseMove, handleMouseUp]);
const dragElementClassName = clsx(`${prefixCls}-dragger`, `${prefixCls}-dragger-${direction}`, {
[`${prefixCls}-dragger-dragging`]: isDragging,
[`${prefixCls}-dragger-horizontal`]: isHorizontal,
[`${prefixCls}-dragger-vertical`]: !isHorizontal
}, className);
return {
dragElementProps: {
className: dragElementClassName,
style,
onMouseDown: handleMouseDown
},
isDragging
};
}
@@ -0,0 +1 @@
export default function useFocusable(getContainer: () => HTMLElement, open: boolean, autoFocus?: boolean, focusTrap?: boolean, mask?: boolean): (ele: HTMLElement) => void;
@@ -0,0 +1,18 @@
import React from 'react';
import { useLockFocus } from "@rc-component/util/es/Dom/focus";
export default function useFocusable(getContainer, open, autoFocus, focusTrap, mask) {
const mergedFocusTrap = focusTrap ?? mask !== false;
// Focus lock
const [ignoreElement] = useLockFocus(open && mergedFocusTrap, getContainer);
// Auto Focus
React.useEffect(() => {
if (open && autoFocus === true) {
getContainer()?.focus({
preventScroll: true
});
}
}, [open]);
return ignoreElement;
}
+4
View File
@@ -0,0 +1,4 @@
import Drawer from './Drawer';
import type { DrawerProps, Placement } from './Drawer';
export type { DrawerProps, Placement };
export default Drawer;
+3
View File
@@ -0,0 +1,3 @@
// export this package's api
import Drawer from "./Drawer";
export default Drawer;
+13
View File
@@ -0,0 +1,13 @@
/// <reference types="react" />
export interface DrawerClassNames {
mask?: string;
wrapper?: string;
section?: string;
dragger?: string;
}
export interface DrawerStyles {
mask?: React.CSSProperties;
wrapper?: React.CSSProperties;
section?: React.CSSProperties;
dragger?: React.CSSProperties;
}
+1
View File
@@ -0,0 +1 @@
export {};
+3
View File
@@ -0,0 +1,3 @@
import type { DrawerProps } from './Drawer';
export declare function parseWidthHeight(value?: number | string): string | number;
export declare function warnCheck(props: DrawerProps): void;
+19
View File
@@ -0,0 +1,19 @@
import warning from "@rc-component/util/es/warning";
import canUseDom from "@rc-component/util/es/Dom/canUseDom";
export function parseWidthHeight(value) {
if (typeof value === 'string') {
const num = Number(value.replace(/px$/i, ''));
const floatNum = parseFloat(value);
if (floatNum === num) {
warning(false, 'Invalid value type of `width` or `height` which should be number type instead.');
}
if (!Number.isNaN(num)) {
return num;
}
}
return value;
}
export function warnCheck(props) {
warning(!('wrapperClassName' in props), `'wrapperClassName' is removed. Please use 'rootClassName' instead.`);
warning(canUseDom() || !props.open, `Drawer with 'open' in SSR is not work since no place to createPortal. Please move to 'useEffect' instead.`);
}