Ant Design Drawer 遮罩(mask)详解:从 blur / dimmed / none 三种效果到 mask 属性的底层实现
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
在 Ant Design 的 Drawer 抽屉组件中,遮罩(mask)是覆盖在页面其余内容上的半透明层,用来隔离背景交互并引导用户聚焦于抽屉内容。本篇文章以官方 demo 文档 components/drawer/demo/mask.md(中文标题为"遮罩效果",英文标题为 "mask effect")为核心,完整讲解其配套示例 mask.tsx 所演示的 blur(模糊)、dimmed(变暗)、none(无遮罩)三种遮罩形态,并结合源码剖析mask属性的对象化配置、点击关闭语义、ConfigProvider 级联配置与底层样式实现。阅读完本文后,你将能精准控制 Drawer 遮罩的开关、模糊与点击行为,并理解这些配置背后在 Drawer.tsx 与 useMergedMask.ts 中的真实生效链路。
mask 属性:从布尔值到对象配置的能力跃迁
要理解"遮罩效果"这个 demo,首先需要认识 Drawer 的mask属性。根据 index.en-US.md 中的 API 表格,其类型定义为:
| 属性 | 说明 | 类型 | 默认值 | 版本 |
|---|---|---|---|---|
| mask | 遮罩效果 | boolean \| { enabled?: boolean, blur?: boolean, closable?: boolean } | true | mask.closable: 6.3.0 |
也就是说,mask在传统布尔值基础上,支持传入对象来细分控制三个维度:
enabled:是否渲染遮罩,等效于原来的布尔开关;blur:是否在遮罩之上叠加背景模糊效果;closable:点击遮罩区域(抽屉外部区域)是否关闭抽屉。
从源码看,这一对象化配置由 useMergedMask.ts 中的MaskConfig类型承载,并被 Drawer.tsx 中重定义的DrawerProps['mask']引用为MaskType:
export interface MaskConfig { enabled?: boolean; blur?: boolean; closable?: boolean; } export type MaskType = MaskConfig | boolean;示例 demo 正是用对象/布尔两种写法演示了三种遮罩档位:{ blur: true }(blur 模糊遮罩)、true(dimmed 变暗遮罩)、false(none 无遮罩)。
精读官方 demo:一个页面演示三种遮罩形态
mask.md 的中英文描述虽然只有"遮罩效果 / mask effect"寥寥几字,但真正的技术内容都沉淀在配套的 mask.tsx 中。它定义了一个联合类型与配置表,将三种遮罩模式映射到对应的mask取值:
import React, { useState } from 'react'; import { Button, Drawer, Space } from 'antd'; type MaskType = 'blur' | 'dimmed' | 'none'; type DrawerConfig = { type: MaskType; mask: boolean | { blur: boolean }; title: string; }; const drawerList: DrawerConfig[] = [ { type: 'blur', mask: { blur: true }, title: 'blur' }, { type: 'dimmed', mask: true, title: 'Dimmed mask' }, { type: 'none', mask: false, title: 'No mask' }, ];在渲染层,demo 用useState记录当前打开的遮罩类型,open仅在open === item.type时为true,因此三个按钮各自对应一个 Drawer 实例,点击按钮时只会唤起匹配类型的那一个:
const App: React.FC = () => { const [open, setOpen] = useState<false | MaskType>(false); const showDrawer = (type: MaskType) => { setOpen(type); }; const onClose = () => { setOpen(false); }; return ( <Space wrap> {drawerList.map((item) => ( <React.Fragment key={item.type}> <Button onClick={() => { showDrawer(item.type); }} > {item.title} </Button> <Drawer title={item.title} placement="right" mask={item.mask} onClose={onClose} open={open === item.type} > <p>Some contents...</p> <p>Some contents...</p> <p>Some contents...</p> </Drawer> </React.Fragment> ))} </Space> ); }; export default App;三种取值的直观差异可以概括为:
mask={{ blur: true }}:遮罩仍然渲染并变暗,同时页面背景会再叠加一层 4px 的模糊(详见下文样式解析),适合需要彻底弱化背景的专注类场景;mask: true(默认值):经典半透明遮罩,背景被colorBgMask色值压暗,但仍可看清轮廓,是最常用的"dimmed 变暗"形态;mask: false:完全不渲染遮罩层,抽屉悬浮于页面上方,背景可继续交互。
注意open同一时刻只可能等于一种类型,showDrawer再次点击同一个按钮不会导致状态抖动,而点击遮罩或关闭按钮时统一走onClose把open置回false。
mask={false}:无遮罩场景与其调试示例
无遮罩也是官方正式支持的使用形态。文档 index.en-US.md 中以 debug 形式收录了另一个示例No mask,配套 demo 见 no-mask.tsx:
<Drawer title="Drawer without mask" placement="right" mask={false} onClose={onClose} open={open} > <p>Some contents...</p> <p>Some contents...</p> <p>Some contents...</p> </Drawer>mask={false}会被 useMergedMask.ts 中的normalizeMaskConfig归一化为{ enabled: false },进而让合并结果中enabled !== false的判断失败,Drawer 底层不再挂载遮罩 DOM。该 demo 中还顺带演示了styles.mask的语义化样式写法(如width、background、borderRadius、boxShadow、overflow)。这里有一个实现层面的注意点:遮罩 DOM 是否存在取决于enabled,只有当遮罩实际渲染时styles.mask/classNames.mask才会真正作用于遮罩元素(相关语义结构说明见文档中的 Semantic DOM 一节)。因此,如果希望页面背景下压暗的同时保留一个特殊形状的遮罩,应开启遮罩并对styles.mask做定制,而不是与mask={false}组合使用。
需要补充的是,把mask设为false并不等于抽屉变得无法关闭:用户仍可通过右上角关闭按钮、ESC 键(keyboard属性控制)等途径关闭,只是"点击遮罩关闭"这一交互天然失效,因为遮罩根本不存在。
点击遮罩关闭:closable 与已废弃的 maskClosable
传统上 Drawer 通过maskClosable控制"点击遮罩关闭",该属性在 Drawer.tsx 中被标注为@deprecated,官方建议迁移到mask.closable。二者在useMergedMask中完成了兼容性合并:
export const useMergedMask = ( mask?: MaskType, contextMask?: MaskType, prefixCls?: string, maskClosable?: boolean, ) => { return useMemo(() => { const maskConfig = normalizeMaskConfig(mask, maskClosable); const contextMaskConfig = normalizeMaskConfig(contextMask); const mergedConfig: MaskConfig = { blur: false, ...contextMaskConfig, ...maskConfig, closable: maskConfig.closable ?? maskClosable ?? contextMaskConfig.closable ?? true, }; const className = mergedConfig.blur ? `${prefixCls}-mask-blur` : undefined; return [mergedConfig.enabled !== false, { mask: className }, !!mergedConfig.closable]; }, [mask, contextMask, prefixCls, maskClosable]); };从这段源码可以读出四条关键信息:
- 默认值:
closable的最终取值按mask.closable→maskClosable→ 上下文 →true的优先级链取第一个非空值,因此默认情况下点击遮罩即可关闭抽屉; - 归一化逻辑:布尔
mask会被自动转换成{ enabled },保证后续统一以对象处理; - blur 类名派生:当
blur: true时,返回给上层的类名是${prefixCls}-mask-blur,也就是实际渲染出的ant-drawer-mask-blur; - 返回值三元组:最终返回
[是否启用遮罩, 遮罩类名对象, 是否可点击遮罩关闭],由 Drawer.tsx 解构后分别透传给 rc-drawer 的mask与maskClosable。
相关行为在测试中有直接覆盖,例如 DrawerEvent.test.tsx 中maskClosable相关用例验证了 "点击遮罩不触发 onClose" 与 "对象化配置与 ConfigProvider 全局配置的优先级关系",可作为mask.closable替换maskClosable的回归保障。
遮罩的模糊效果如何实现:backdrop-filter 与 motion 动画
当blur: true时,遮罩层会额外获得模糊能力。这一效果并非通过改变遮罩本身的透明度实现,而是基于 CSSbackdrop-filter。在 Drawer 的样式文件 components/drawer/style/index.ts 中可以找到遮罩的基础样式:
[`${componentCls}-mask`]: { position: 'absolute', inset: 0, zIndex: zIndexPopup, background: colorBgMask, pointerEvents: 'auto', [`&${componentCls}-mask-blur`]: { backdropFilter: 'blur(4px)', }, },细节解读如下:
- 遮罩背景色来自设计令牌
colorBgMask,zIndex取自 Drawer 的组件令牌zIndexPopup,因此模糊/变暗效果与整体弹层层级体系一致; - 叠加的
.ant-drawer-mask-blur类把backdrop-filter设为blur(4px),实现"毛玻璃"式的背景模糊。backdrop-filter需要浏览器支持,且对父级层叠上下文有一定要求,这在低版本浏览器中可能表现为无模糊但遮罩仍在,属正常的渐进增强行为; - 遮罩的
pointerEvents: 'auto'保证了点击遮罩可以被捕获并触发closable关闭逻辑。
此外,遮罩与面板的入场/离场动画在 components/drawer/style/motion.ts 中定义,而动画名则由 Drawer.tsx 通过getTransitionName(prefixCls, 'mask-motion')动态生成,使"淡入淡出遮罩 + 滑入滑出面板"共用同一套MOTION_CONFIG(motionAppear/motionEnter/motionLeave均开启,motionDeadline: 500)。测试快照中可见实际生成的类名,例如ant-drawer-mask ant-drawer-mask-blur,参见 demo-extend.test.tsx.snap 中的渲染结果。
ConfigProvider 全局配置:遮罩的上下文级联
mask不仅能写在单个 Drawer 上,还可以通过 ConfigProvider 的组件级配置(components={{ drawer: { mask: ... } } })对整棵组件树生效。该能力在 index.en-US.md 的 API 表中对应 "Global Config" 一列:mask全局配置自 6.0.0 起支持,mask.closable自 6.3.0 起支持。
在 Drawer.tsx 中,组件通过useComponentConfig('drawer')取出上下文中的mask: contextMask,随后在 useMergedMask.ts 中与组件自身的mask做浅合并:
const mergedConfig: MaskConfig = { blur: false, ...contextMaskConfig, ...maskConfig, closable: maskConfig.closable ?? maskClosable ?? contextMaskConfig.closable ?? true, };展开顺序...contextMaskConfig在前、...maskConfig在后,意味着单个 Drawer 上显式传入的mask字段会覆盖 ConfigProvider 的全局配置,而全局配置又能兜底所有未显式声明该字段的 Drawer。DrawerEvent.test.tsx中mask.closable与 ConfigProvider 配置互相覆盖的用例,正是对这一优先级关系的回归验证。这一机制非常适合"整站统一关闭遮罩"或"全局默认开启背景模糊"这类批量治理诉求。
mask 与其他能力的联动:焦点管理、嵌套与样式令牌
从 Drawer.tsx 的实现可以观察到一个容易被忽略的联动:焦点陷阱是否启用取决于遮罩状态:
const mergedFocusable = useFocusable( { ...contextFocusable, ...focusable }, getContainer !== false && mergedMask, );即当 Drawer 通过getContainer渲染在 body 下且遮罩启用时,默认会开启焦点管理(focusTrap),把 Tab 焦点约束在抽屉内部;而当mask={false}时,焦点陷阱的默认前提被破坏,需要显式通过focusable配置({ trap?: boolean, focusTriggerAfterClose?: boolean },自 6.2.0 起支持)来接管。这意味着"去掉遮罩"不只是视觉层面的取舍,还牵动键盘可达性与无障碍体验,在需要背景内容保留可交互性的无遮罩场景下要格外留意焦点是否合理流转。
此外,Drawer 的嵌套场景(push属性,默认{ distance: 180 })会让被压入的抽屉整体(含其遮罩区域)随面板一起平移,多个抽屉的遮罩按zIndexPopup令牌与渲染顺序正确堆叠;这些能力与 mask 效果组合后仍能正常工作。涉及遮罩的样式令牌主要是全局设计令牌colorBgMask(遮罩底色)与组件令牌zIndexPopup(弹层层级),开发者可通过主题 token 覆盖实现自定义的遮罩观感,例如更深的压暗色或更高的层级。
小结与验证路径
围绕 mask.md 的"遮罩效果"主题,本文要点可归纳为:
mask支持boolean与{ enabled, blur, closable }对象两种形态,对应 demo 中的 none / dimmed / blur 三种遮罩形态;mask={false}移除遮罩层,需配合键盘与焦点方案保障可访问性;- 点击遮罩关闭由
mask.closable控制,maskClosable已废弃; - 模糊遮罩通过
ant-drawer-mask-blur+backdrop-filter: blur(4px)实现,遮罩动画与颜色分别由 motion.ts 与colorBgMask令牌决定; - 全局配置可通过 ConfigProvider 注入,组件级显式配置优先。
若想进一步核对以上结论,可以按图索骥:阅读 demo 源码 mask.tsx 与 no-mask.tsx 观察使用姿势;查看 useMergedMask.ts 理解归一化与优先级合并;在 style/index.ts 中验证 blur 的 CSS 实现;最后借助 Drawer.test.tsx 中针对ant-drawer-mask-blur类名的断言(校验开启blur后类名出现、关闭后消失)来确认行为与预期一致。掌握这些细节后,无论是要做沉浸式专注的模糊遮罩、轻量悬浮的无遮罩抽屉,还是全局统一遮罩策略,都能在 Ant Design Drawer 上从容落地。
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考