Skip to content

feat(drawer): add DrawerPlugin #3381

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 16, 2025
Merged
45 changes: 34 additions & 11 deletions packages/components/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { forwardRef, useState, useEffect, useImperativeHandle, useRef, useMemo, isValidElement } from 'react';
import classnames from 'classnames';
import { isString , isObject , isFunction } from 'lodash-es';
import { isString, isObject, isFunction } from 'lodash-es';

import { CSSTransition } from 'react-transition-group';
import { CloseIcon as TdCloseIcon } from 'tdesign-icons-react';

import { useLocaleReceiver } from '../locale/LocalReceiver';
import { TdDrawerProps, DrawerEventSource } from './type';
import { TdDrawerProps, DrawerEventSource, DrawerInstance } from './type';
import { StyledProps } from '../common';
import Button, { ButtonProps } from '../button';
import useConfig from '../hooks/useConfig';
Expand All @@ -18,6 +18,7 @@ import useLockStyle from './hooks/useLockStyle';
import useDefaultProps from '../hooks/useDefaultProps';
import parseTNode from '../_util/parseTNode';
import useAttach from '../hooks/useAttach';
import useSetState from '../hooks/useSetState';

export const CloseTriggerType: { [key: string]: DrawerEventSource } = {
CLICK_OVERLAY: 'overlay',
Expand All @@ -26,16 +27,19 @@ export const CloseTriggerType: { [key: string]: DrawerEventSource } = {
KEYDOWN_ESC: 'esc',
};

export interface DrawerProps extends TdDrawerProps, StyledProps {}
export interface DrawerProps extends TdDrawerProps, StyledProps {
isPlugin?: boolean; // 是否以插件形式调用
}

const Drawer = forwardRef<HTMLDivElement, DrawerProps>((originalProps, ref) => {
const Drawer = forwardRef<DrawerInstance, DrawerProps>((originalProps, ref) => {
// 国际化文本初始化
const [local, t] = useLocaleReceiver('drawer');
const { CloseIcon } = useGlobalIcon({ CloseIcon: TdCloseIcon });
const confirmText = t(local.confirm);
const cancelText = t(local.cancel);

const props = useDefaultProps<DrawerProps>(originalProps, drawerDefaultProps);
const [state, setState] = useSetState<DrawerProps>({ isPlugin: false, ...props });
const {
className,
style,
Expand Down Expand Up @@ -67,7 +71,8 @@ const Drawer = forwardRef<HTMLDivElement, DrawerProps>((originalProps, ref) => {
destroyOnClose,
sizeDraggable,
forceRender,
} = props;
isPlugin,
} = state;

const size = propsSize ?? local.size;
const { classPrefix } = useConfig();
Expand All @@ -86,14 +91,32 @@ const Drawer = forwardRef<HTMLDivElement, DrawerProps>((originalProps, ref) => {
return dragSizeValue || sizeMap[size] || size;
}, [dragSizeValue, size]);

useLockStyle({ ...props, sizeValue, drawerWrapper: drawerWrapperRef.current });
useImperativeHandle(ref, () => containerRef.current);
useLockStyle({ ...state, sizeValue, drawerWrapper: drawerWrapperRef.current });
useImperativeHandle(ref, () => ({
show() {
setState({ visible: true });
},
hide() {
setState({ visible: false });
},
destroy() {
setState({ visible: false, destroyOnClose: true });
},
update(options) {
setState((prevState) => ({ ...prevState, ...options }));
},
}));

useEffect(() => {
if (!visible) return;
// 聚焦到 Drawer 最外层元素即 containerRef.current,KeyDown 事件才有效。
containerRef.current?.focus?.();
}, [visible]);
if (visible) {
// 聚焦到 Drawer 最外层元素即 containerRef.current,KeyDown 事件才有效。
containerRef.current?.focus?.();
}
// 非插件式调用 更新props
if (!isPlugin) {
setState((prevState) => ({ ...prevState, ...props }));
}
}, [visible, isPlugin, props, setState]);

function onMaskClick(e: React.MouseEvent<HTMLDivElement>) {
onOverlayClick?.({ e });
Expand Down
59 changes: 59 additions & 0 deletions packages/components/drawer/_example/plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { DrawerPlugin, Button, Space } from 'tdesign-react';

const buttonStyle = { marginRight: 16 };

export default function PluginModalExample() {
const showDrawer = () => {
const myDrawer = DrawerPlugin({
header: 'Drawer-Plugin',
body: 'Hi, darling! Do you want to be my lover?',
onConfirm: ({ e }) => {
console.log('confirm clicked', e);
myDrawer.hide();
},
onClose: ({ e, trigger }) => {
console.log('e: ', e);
console.log('trigger: ', trigger);
myDrawer.hide();
},
onCloseBtnClick: ({ e }) => {
console.log('close btn: ', e);
},
});
};
const onDrawerPlugin = () => {
const Drawer = DrawerPlugin({
header: 'Drawer-Confirm-Plugin',
body: 'I am a drawer!',
confirmBtn: 'hello',
cancelBtn: 'bye',
size: 'large',
className: 't-class-drawer--first',
onConfirm: ({ e }) => {
console.log('confirm button has been clicked!');
console.log('e: ', e);
Drawer.hide();
},
onClose: ({ e, trigger }) => {
console.log('e: ', e);
console.log('trigger: ', trigger);
Drawer.hide();
},
});
};
return (
<Space direction="vertical">
<p>函数调用方式一:DrawerPlugin(options)</p>
<p>函数调用方式二:drawer(options)</p>
<div>
<Button theme="primary" onClick={showDrawer} style={buttonStyle}>
DrawerPlugin
</Button>
<Button theme="primary" onClick={onDrawerPlugin} style={buttonStyle}>
drawer
</Button>
</div>
</Space>
);
}
19 changes: 19 additions & 0 deletions packages/components/drawer/drawer.en-US.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
:: BASE_DOC ::

### Function invocations

- `DrawerPlugin(options)`, or
- `drawer(options)`

<br />

A component instance: `DrawerInstance = DrawerPlugin(options)`.

- Destroying a drawer: `DrawerInstance.destroy()`

- Hiding a drawer: `DrawerInstance.hide()`

- Showing a drawer: `DrawerInstance.show()`

- Updating a drawer: `DrawerInstance.update()`

{{ plugin }}

## API
### Drawer Props

Expand Down
19 changes: 19 additions & 0 deletions packages/components/drawer/drawer.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
:: BASE_DOC ::

### 函数式调用

- 函数调用方式一: `DrawerPlugin(options)`
- 函数调用方式二: `drawer(options)`

<br />

组件实例:`DrawerInstance = DrawerPlugin(options)`

- 销毁抽屉:`DrawerInstance.destroy()`

- 隐藏抽屉:`DrawerInstance.hide()`

- 显示抽屉:`DrawerInstance.show()`

- 更新抽屉:`DrawerInstance.update()`

{{ plugin }}

## API
### Drawer Props

Expand Down
5 changes: 5 additions & 0 deletions packages/components/drawer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import _Drawer from './Drawer';
import { DrawerPlugin as _DrawerPlugin } from './plugin';

import './style/index.js';

export type { DrawerProps } from './Drawer';
export * from './type';

export const Drawer = _Drawer;

export const drawer = _DrawerPlugin;
export const DrawerPlugin = _DrawerPlugin;

export default Drawer;
55 changes: 55 additions & 0 deletions packages/components/drawer/plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import { render } from '../_util/react-render';
import DrawerComponent, { DrawerProps } from './Drawer';

import { getAttach } from '../_util/dom';
import { DrawerOptions, DrawerMethod, DrawerInstance } from './type';
import log from '../../common/js/log';

const createDrawer: DrawerMethod = (props: DrawerOptions): DrawerInstance => {
const drawerRef = React.createRef<DrawerInstance>();
const options = { ...props };

// 默认先不出现。如有更好的方法,请代替↓
const { visible = false } = options;

const fragment = document.createDocumentFragment();
render(<DrawerComponent {...(options as DrawerProps)} visible={visible} ref={drawerRef} isPlugin />, fragment);

const container = getAttach(options.attach);
if (container) {
// 抽屉加载出来了再设置出现,才有出现动画。如有更好的方法,请代替↓
requestAnimationFrame(() => {
drawerRef.current?.show();
});
container.appendChild(fragment);
} else {
log.error('Drawer', 'attach is not exist');
}

const drawerNode: DrawerInstance = {
show: () => {
requestAnimationFrame(() => {
drawerRef.current?.show();
});
},
hide: () => {
requestAnimationFrame(() => {
drawerRef.current?.destroy();
});
},
update: (updateOptions: DrawerOptions) => {
requestAnimationFrame(() => {
drawerRef.current?.update(updateOptions);
});
},
destroy: () => {
requestAnimationFrame(() => {
drawerRef.current?.destroy();
});
},
};
return drawerNode;
};

export const DrawerPlugin = createDrawer;
2 changes: 1 addition & 1 deletion packages/components/drawer/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,4 @@ export interface DrawerCloseContext {
e: MouseEvent<HTMLDivElement | HTMLButtonElement> | KeyboardEvent<HTMLDivElement>;
}

export type DrawerMethod = (options?: DrawerOptions) => void;
export type DrawerMethod = (options?: DrawerOptions) => DrawerInstance;
54 changes: 54 additions & 0 deletions test/snap/__snapshots__/csr.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -39999,6 +39999,58 @@ exports[`csr snapshot test > csr test packages/components/drawer/_example/placem
</div>
`;

exports[`csr snapshot test > csr test packages/components/drawer/_example/plugin.tsx 1`] = `
<div>
<div
class="t-space t-space-vertical"
style="gap: 16px;"
>
<div
class="t-space-item"
>
<p>
函数调用方式一:DrawerPlugin(options)
</p>
</div>
<div
class="t-space-item"
>
<p>
函数调用方式二:drawer(options)
</p>
</div>
<div
class="t-space-item"
>
<div>
<button
class="t-button t-button--theme-primary t-button--variant-base"
style="margin-right: 16px;"
type="button"
>
<span
class="t-button__text"
>
DrawerPlugin
</span>
</button>
<button
class="t-button t-button--theme-primary t-button--variant-base"
style="margin-right: 16px;"
type="button"
>
<span
class="t-button__text"
>
drawer
</span>
</button>
</div>
</div>
</div>
</div>
`;

exports[`csr snapshot test > csr test packages/components/drawer/_example/popup.tsx 1`] = `
<div>
<div
Expand Down Expand Up @@ -136950,6 +137002,8 @@ exports[`ssr snapshot test > ssr test packages/components/drawer/_example/operat

exports[`ssr snapshot test > ssr test packages/components/drawer/_example/placement.tsx 1`] = `"<div style="gap:16px" class="t-space t-space-horizontal"><div class="t-space-item"><div class="t-radio-group t-size-m t-radio-group__outline"><label tabindex="0" class="t-radio-button"><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;left&#x27;" value="left"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">左侧</span></label><label tabindex="0" class="t-radio-button t-is-checked" checked=""><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;right&#x27;" checked="" value="right"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">右侧</span></label><label tabindex="0" class="t-radio-button"><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;top&#x27;" value="top"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">上方</span></label><label tabindex="0" class="t-radio-button"><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;bottom&#x27;" value="bottom"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">下方</span></label></div></div><div class="t-space-item"><div><button type="button" class="t-button t-button--theme-primary t-button--variant-base"><span class="t-button__text">打开抽屉</span></button></div></div><div class="t-space-item"></div></div>"`;

exports[`ssr snapshot test > ssr test packages/components/drawer/_example/plugin.tsx 1`] = `"<div style="gap:16px" class="t-space t-space-vertical"><div class="t-space-item"><p>函数调用方式一:DrawerPlugin(options)</p></div><div class="t-space-item"><p>函数调用方式二:drawer(options)</p></div><div class="t-space-item"><div><button style="margin-right:16px" type="button" class="t-button t-button--theme-primary t-button--variant-base"><span class="t-button__text">DrawerPlugin</span></button><button style="margin-right:16px" type="button" class="t-button t-button--theme-primary t-button--variant-base"><span class="t-button__text">drawer</span></button></div></div></div>"`;

exports[`ssr snapshot test > ssr test packages/components/drawer/_example/popup.tsx 1`] = `"<div style="gap:16px" class="t-space t-space-horizontal"><div class="t-space-item"><div><span>抽屉弹出模式:</span><div class="t-radio-group t-size-m t-radio-group__outline"><label tabindex="0" class="t-radio-button t-is-checked" checked=""><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;push&#x27;" checked="" value="push"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">push</span></label><label tabindex="0" class="t-radio-button"><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;overlay&#x27;" value="overlay"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">overlay</span></label></div></div></div><div class="t-space-item"><div><button type="button" class="t-button t-button--theme-primary t-button--variant-base"><span class="t-button__text">打开抽屉</span></button></div></div><div class="t-space-item"></div></div>"`;

exports[`ssr snapshot test > ssr test packages/components/drawer/_example/size.tsx 1`] = `"<div style="gap:16px" class="t-space t-space-horizontal"><div class="t-space-item"><div class="t-radio-group t-size-m t-radio-group__outline"><label tabindex="0" class="t-radio-button t-is-checked" checked=""><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;small&#x27;" checked="" value="small"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">small(300px)</span></label><label tabindex="0" class="t-radio-button"><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;medium&#x27;" value="medium"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">medium(500px)</span></label><label tabindex="0" class="t-radio-button"><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;large&#x27;" value="large"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">large(760px)</span></label><label tabindex="0" class="t-radio-button"><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="200" value="200"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">200</span></label><label tabindex="0" class="t-radio-button"><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;400px&#x27;" value="400px"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">400px</span></label><label tabindex="0" class="t-radio-button"><input type="radio" class="t-radio-button__former" tabindex="-1" data-value="&#x27;50%&#x27;" value="50%"/><span class="t-radio-button__input"></span><span class="t-radio-button__label">50%</span></label></div></div><div class="t-space-item"><div><button type="button" class="t-button t-button--theme-primary t-button--variant-base"><span class="t-button__text">打开抽屉</span></button></div></div><div class="t-space-item"></div></div>"`;
Expand Down
Loading