From 1ec7cd744ddbc5fe8c75d1a7b98e830b163ae722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Sun, 4 Jan 2026 16:39:07 +0800 Subject: [PATCH] feat: Drawer support focusable (#56463) * feat: add focusable support to drawer component * test: add test case * docs: add focusable prop documentation for drawer component * test: update test case --- .../__snapshots__/components.test.tsx.snap | 84 ----- components/drawer/__tests__/Drawer.test.tsx | 21 ++ .../__snapshots__/Drawer.test.tsx.snap | 168 ---------- .../__snapshots__/DrawerEvent.test.tsx.snap | 12 - .../__snapshots__/demo-extend.test.tsx.snap | 288 ------------------ components/drawer/index.en-US.md | 1 + components/drawer/index.tsx | 30 +- components/drawer/index.zh-CN.md | 1 + components/drawer/useFocusable.ts | 21 ++ package.json | 2 +- 10 files changed, 70 insertions(+), 558 deletions(-) create mode 100644 components/drawer/useFocusable.ts diff --git a/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap b/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap index 38090b3037..3f8e23c5d5 100644 --- a/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap +++ b/components/config-provider/__tests__/__snapshots__/components.test.tsx.snap @@ -13928,12 +13928,6 @@ exports[`ConfigProvider components Drawer configProvider 1`] = `
- - `; @@ -13999,12 +13987,6 @@ exports[`ConfigProvider components Drawer configProvider componentDisabled 1`] =
- - `; @@ -14070,12 +14046,6 @@ exports[`ConfigProvider components Drawer configProvider componentSize large 1`]
- - `; @@ -14141,12 +14105,6 @@ exports[`ConfigProvider components Drawer configProvider componentSize middle 1`
- - `; @@ -14212,12 +14164,6 @@ exports[`ConfigProvider components Drawer configProvider componentSize small 1`]
- - `; @@ -14283,12 +14223,6 @@ exports[`ConfigProvider components Drawer normal 1`] = `
- - `; @@ -14354,12 +14282,6 @@ exports[`ConfigProvider components Drawer prefixCls 1`] = `
- - `; diff --git a/components/drawer/__tests__/Drawer.test.tsx b/components/drawer/__tests__/Drawer.test.tsx index 229e78a4c3..e293de4270 100644 --- a/components/drawer/__tests__/Drawer.test.tsx +++ b/components/drawer/__tests__/Drawer.test.tsx @@ -507,6 +507,27 @@ describe('Drawer', () => { expect(container.querySelector('#test')).toBeTruthy(); }); + it('focusable default config should pass to classNames', () => { + const classNames = jest.fn(() => ({})); + + render( + + Here is content of Drawer + , + ); + + expect(classNames).toHaveBeenCalledWith( + expect.objectContaining({ + props: expect.objectContaining({ + focusable: { + trap: false, + focusTriggerAfterClose: true, + }, + }), + }), + ); + }); + describe('Drawer mask blur className', () => { const testCases: [ mask?: MaskType, diff --git a/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap b/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap index 14eaee45e3..b86885b75e 100644 --- a/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap +++ b/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap @@ -8,12 +8,6 @@ exports[`Drawer Drawer loading have a spinner 1`] = `
- - `; @@ -99,12 +87,6 @@ exports[`Drawer Drawer mask blur className should support closable placement wit
- - `; @@ -171,12 +147,6 @@ exports[`Drawer Drawer mask blur className should support closable placement wit
- - `; @@ -244,12 +208,6 @@ exports[`Drawer className is test_drawer 1`] = `
- - `; @@ -317,12 +269,6 @@ exports[`Drawer closable is false 1`] = `
- - `; @@ -361,12 +301,6 @@ exports[`Drawer getContainer return undefined 2`] = `
- -
`; @@ -435,12 +363,6 @@ exports[`Drawer have a footer 1`] = `
- - `; @@ -513,12 +429,6 @@ exports[`Drawer have a title 1`] = `
- - `; @@ -593,12 +497,6 @@ exports[`Drawer render correctly 1`] = `
- - `; @@ -666,12 +558,6 @@ exports[`Drawer render top drawer 1`] = `
- - `; @@ -743,12 +623,6 @@ exports[`Drawer style migrate match between styles and deprecated style prop 1`] class="ant-drawer-mask ant-drawer-mask-motion-enter ant-drawer-mask-motion-enter-start ant-drawer-mask-motion ant-drawer-mask-blur" style="font-size: 15px;" /> - -
`; @@ -830,12 +698,6 @@ exports[`Drawer style migrate match between styles and deprecated style prop 2`] class="ant-drawer-mask ant-drawer-mask-motion-enter ant-drawer-mask-motion-enter-start ant-drawer-mask-motion ant-drawer-mask-blur" style="font-size: 15px;" /> - -
`; @@ -916,12 +772,6 @@ exports[`Drawer style/drawerStyle/headerStyle/bodyStyle should work 1`] = `
- - `; @@ -992,12 +836,6 @@ exports[`Drawer support closeIcon 1`] = `
- - `; diff --git a/components/drawer/__tests__/__snapshots__/DrawerEvent.test.tsx.snap b/components/drawer/__tests__/__snapshots__/DrawerEvent.test.tsx.snap index 68a5cbfa0c..5be12d3225 100644 --- a/components/drawer/__tests__/__snapshots__/DrawerEvent.test.tsx.snap +++ b/components/drawer/__tests__/__snapshots__/DrawerEvent.test.tsx.snap @@ -8,12 +8,6 @@ exports[`Drawer render correctly 1`] = `
- - `; diff --git a/components/drawer/__tests__/__snapshots__/demo-extend.test.tsx.snap b/components/drawer/__tests__/__snapshots__/demo-extend.test.tsx.snap index 346165a39d..490e12d78e 100644 --- a/components/drawer/__tests__/__snapshots__/demo-extend.test.tsx.snap +++ b/components/drawer/__tests__/__snapshots__/demo-extend.test.tsx.snap @@ -17,12 +17,6 @@ Array [
- - , ] `; @@ -137,12 +125,6 @@ Array [
- - ,
- - , ] `; @@ -340,12 +304,6 @@ Array [
- - , ] `; @@ -506,12 +458,6 @@ exports[`renders components/drawer/demo/config-provider.tsx extend context corre
- -
`; @@ -712,12 +652,6 @@ Array [
- - , ] `; @@ -870,12 +798,6 @@ Array [
- - , ] `; @@ -2892,12 +2808,6 @@ Array [
- - , ] `; @@ -3014,12 +2918,6 @@ exports[`renders components/drawer/demo/mask.tsx extend context correctly 1`] =
- -
- -
- -
@@ -3314,12 +3182,6 @@ Array [
-
- , ] `; @@ -3436,12 +3280,6 @@ Array [ class="ant-drawer ant-drawer-right no-mask css-var-test-id ant-drawer-open ant-drawer-inline" tabindex="-1" > - - , ] `; @@ -3642,12 +3474,6 @@ Array [
- - , ] `; @@ -3723,12 +3543,6 @@ exports[`renders components/drawer/demo/render-in-current.tsx extend context cor
- -
`; @@ -3964,12 +3772,6 @@ Array [
- - , ] `; @@ -4174,12 +3970,6 @@ exports[`renders components/drawer/demo/scroll-debug.tsx extend context correctl
-
-
- -
`; @@ -4567,12 +4327,6 @@ Array [
- - , ] `; @@ -4710,12 +4458,6 @@ exports[`renders components/drawer/demo/style-class.tsx extend context correctly class="ant-drawer-mask ant-drawer-mask-blur" style="background-image: linear-gradient(to top, #18181b 0, rgba(21, 21, 22, 0.2) 100%);" /> - -
- -
`; @@ -5076,12 +4800,6 @@ Array [
- - , ] `; diff --git a/components/drawer/index.en-US.md b/components/drawer/index.en-US.md index 2b8e49c276..d9bd71ad9d 100644 --- a/components/drawer/index.en-US.md +++ b/components/drawer/index.en-US.md @@ -64,6 +64,7 @@ v5 uses `rootClassName` & `rootStyle` to configure the outermost element style, | extra | Extra actions area at corner | ReactNode | - | 4.17.0 | | footer | The footer for Drawer | ReactNode | - | | | forceRender | Pre-render Drawer component forcibly | boolean | false | | +| focusable | Configuration for focus management in the Drawer | `{ trap?: boolean, focusTriggerAfterClose?: boolean }` | - | 6.2.0 | | getContainer | mounted node and display window for Drawer | HTMLElement \| () => HTMLElement \| Selectors \| false | body | | | headerStyle | Style of the drawer header part | CSSProperties | - | | | ~~height~~ | Placement is `top` or `bottom`, height of the Drawer dialog, please use `size` instead | string \| number | 378 | | diff --git a/components/drawer/index.tsx b/components/drawer/index.tsx index 024980f4da..61ef407f0e 100644 --- a/components/drawer/index.tsx +++ b/components/drawer/index.tsx @@ -19,6 +19,8 @@ import { usePanelRef } from '../watermark/context'; import type { DrawerClassNamesType, DrawerPanelProps, DrawerStylesType } from './DrawerPanel'; import DrawerPanel from './DrawerPanel'; import useStyle from './style'; +import type { FocusableConfig, OmitFocusType } from './useFocusable'; +import useFocusable from './useFocusable'; const _SizeTypes = ['default', 'large'] as const; @@ -38,7 +40,13 @@ export interface DrawerResizableConfig { export interface DrawerProps extends Omit< RcDrawerProps, - 'maskStyle' | 'destroyOnClose' | 'mask' | 'resizable' | 'classNames' | 'styles' + | 'maskStyle' + | 'destroyOnClose' + | 'mask' + | 'resizable' + | 'classNames' + | 'styles' + | OmitFocusType >, Omit { size?: sizeType | number | string; @@ -52,6 +60,8 @@ export interface DrawerProps */ destroyOnHidden?: boolean; mask?: MaskType; + + focusable?: Omit; } const defaultPushState: PushState = { distance: 180 }; @@ -80,6 +90,9 @@ const Drawer: React.FC & { resizable, 'aria-labelledby': ariaLabelledby, + // Focus + focusable, + // Deprecated maskStyle, drawerStyle, @@ -190,14 +203,17 @@ const Drawer: React.FC & { const innerPanelRef = usePanelRef(); const mergedPanelRef = composeRef(panelRef, innerPanelRef) as React.Ref; - // ============================ zIndex ============================ + // =========================== zIndex =========================== const [zIndex, contextZIndex] = useZIndex('Drawer', rest.zIndex); + // ============================ Mask ============================ + const [mergedMask, maskBlurClassName] = useMergedMask(drawerMask, contextMask, prefixCls); + + // ========================== Focusable ========================= + const mergedFocusable = useFocusable(focusable, getContainer !== false && mergedMask); + // =========================== Render =========================== const { classNames, styles, rootStyle } = rest; - - const [mergedMask, maskBlurClassName] = useMergedMask(drawerMask, contextMask, prefixCls); - const mergedProps: DrawerProps = { ...props, zIndex, @@ -205,6 +221,7 @@ const Drawer: React.FC & { mask: mergedMask, defaultSize, push, + focusable: mergedFocusable, }; const [mergedClassNames, mergedStyles] = useMergeSemantic< @@ -263,6 +280,9 @@ const Drawer: React.FC & { {...(resizable ? { resizable } : {})} aria-labelledby={ariaLabelledby ?? ariaId} destroyOnHidden={destroyOnHidden ?? destroyOnClose} + // Focusable + focusTriggerAfterClose={mergedFocusable.focusTriggerAfterClose} + focusTrap={mergedFocusable.trap} > HTMLElement \| Selectors \| false | body | | | ~~height~~ | 高度,在 `placement` 为 `top` 或 `bottom` 时使用,请使用 `size` 替换 | string \| number | 378 | | | keyboard | 是否支持键盘 esc 关闭 | boolean | true | | diff --git a/components/drawer/useFocusable.ts b/components/drawer/useFocusable.ts new file mode 100644 index 0000000000..3bedbbf598 --- /dev/null +++ b/components/drawer/useFocusable.ts @@ -0,0 +1,21 @@ +import { useMemo } from 'react'; + +export type OmitFocusType = 'autoFocusButton' | 'focusTriggerAfterClose' | 'focusTrap'; + +export interface FocusableConfig { + autoFocusButton?: 'ok' | 'cancel' | false; + focusTriggerAfterClose?: boolean; + trap?: boolean; +} + +export default function useFocusable(focusable?: FocusableConfig, defaultTrap?: boolean) { + return useMemo(() => { + const ret = { + trap: defaultTrap ?? true, + focusTriggerAfterClose: true, + ...focusable, + }; + + return ret; + }, [focusable, defaultTrap]); +} diff --git a/package.json b/package.json index 2dd19ba05f..a4a755e545 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "@rc-component/collapse": "~1.1.2", "@rc-component/color-picker": "~3.0.3", "@rc-component/dialog": "~1.7.0", - "@rc-component/drawer": "~1.3.0", + "@rc-component/drawer": "~1.4.0", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.6.0", "@rc-component/image": "~1.6.0",