尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

BlockNote 焦点管理机制解析:谁拿焦点、为什么拿、三种皮肤如何落实同一套契约

BlockNote 焦点管理机制解析:谁拿焦点、为什么拿、三种皮肤如何落实同一套契约 前端富文本UI组件AI 应用【免费下载链接】BlockNoteA React Rich Text Editor thats block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.项目地址https://gitcode.com/gh_mirrors/bl/BlockNote点击查看免费下载本指南以 BlockNote React 包内部分析文档 focus-management.md 为骨架结合核心与三种 UI 皮肤Ariakit / Mantine / shadcn的源码实现与端到端测试系统讲解 BlockNote 编辑器在桌面端与移动端如何管理焦点。读完你将理解编辑器保持焦点、只有autoFocus输入可以抢焦点这条规则背后的移动端原因掌握preventFocusOnTap与preventFocusOnOpen两个关键机制的触发条件与实现细节并能复述出 BlockNote 对 popover、菜单、选择器、工具栏按钮四类界面的焦点契约。核心规则编辑器保持焦点只有主动请求焦点的输入才能拿走它BlockNote 的焦点管理有一条总规则用户在编辑器周围的 UI 中操作时编辑器始终保持焦点只有自己请求焦点的输入autoFocus才能把焦点拿走。这条规则首先是为移动端服务的。在手机上焦点一旦离开可编辑元素屏幕软键盘就会立刻关闭而移动端格式化工具栏是锚定在软键盘上方的见 MobileFormattingToolbarController.tsx。任何一次游离的焦点移动都会同时关闭键盘、收起工具栏以及工具栏里正打开着的面板。桌面端则宽松得多鼠标点击会把焦点移向按钮按钮在应用完样式后把焦点交还给编辑器例如editor.focus()所以用户点击加粗后可以继续打字。文档明确列出的例外只有三类添加评论评论编辑器获得焦点、文件预览、表格单元格合并。这条桌面宽松、移动端严格的差异正是后面两套不同信号isTouchDevice()与 UI 模式并存的原因。各类界面取焦点的时机一张总览表原文档给出了一张覆盖桌面端与移动端工具栏的行为矩阵这里完整保留并逐项展开界面类型桌面端移动端工具栏Popover链接表单、文件面板、表情选择仅autoFocus的输入相同Menu颜色、拖拽手柄、表格打开时菜单获得焦点库的默认行为不获得焦点Select块类型选择打开时列表获得焦点库的默认行为不获得焦点工具栏按钮、Select 触发器点击时按钮获得焦点多数随后交还点击时完全不获得焦点下面分别讲解每一类界面在这张表背后对应的代码契约。Popover任何设备上都不从 UI 库获得焦点Popover 中的内容由 BlockNote 自己掌控焦点URL 输入框或标题输入框通过autoFocus主动请求焦点——这在手机上也没问题因为焦点落在输入框内时键盘保持打开状态。而没有这类输入框的 popover例如文件面板则完全不获得焦点。从源码看autoFocus正是 BlockNote 内部各 popover 表单采用的统一手段链接工具栏的编辑表单EditLinkMenuItems.tsx 中 URL 输入autoFocus{true}文件标题输入FileCaptionButton.tsx 与 FileRenameButton.tsx 均autoFocus{true}评论浮动编辑器对应添加评论例外FloatingComposer.tsx 的Editor组件autoFocus{true}。三种皮肤Ariakit、Mantine、shadcn在此处的行为完全一致因为 popover 本身不参与焦点接管焦点决策全部落在autoFocus上。Menu 与 Select保留库的打开即聚焦唯独移动端工具栏除外打开菜单/列表时把焦点移入其中是无障碍的默认行为键盘用户依赖它用方向键导航菜单项。因此桌面端保留这一行为。但在移动端工具栏内部这会导致键盘与整个工具栏一起关闭所以在这里被关闭。关闭动作由preventFocusOnOpen属性驱动它同时作用于Menu.Root与ToolbarSelect两类组件。该属性的语义在 ComponentsContext.tsx 中有明确注释置为true时 UI 库不得在界面打开时把焦点移入但其中通过autoFocus主动请求焦点的输入框仍然获得焦点对应链接表单在移动端仍可聚焦的场景。在具体组件上preventFocusOnOpen的取值统一由 UI 模式决定。例如块类型选择器 BlockTypeSelect.tsxpreventFocusOnOpen{uiMode mobile}颜色菜单 ColorStyleButton.tsx同样preventFocusOnOpen{uiMode mobile}。与此同时菜单项的 hover 聚焦Ariakit 的focusOnHover也会以同样的方式移动焦点因此一并关闭Ariakit 皮肤中菜单项与选项的focusOnHover{!preventFocusOnOpen}见 Menu.tsx 与 ToolbarSelect.tsx。此外点击菜单项或选项本身在任何设备上都不会让该项获得焦点这与工具栏按钮的规则一致统一由preventFocusOnTap保证onMouseDown{preventFocusOnTap}。工具栏按钮点击tap从不拿焦点一次点击是指针手势。通过取消浏览器在mousedown时的默认聚焦行为可以保证编辑器保持聚焦同时 click 事件照常触发——这就是preventFocusOnTap做的事。桌面端鼠标点击保留浏览器默认行为焦点移到按钮而 Safari 需要特殊处理它在mousedown时本身不会聚焦按钮所以代码里显式调用focus()让 Safari 与其他浏览器行为一致。完整的实现位于 mouseDownFocus.tsexport function preventFocusOnTap(event: MouseEventHTMLElement) { if (isTouchDevice()) { event.preventDefault(); return; } if (isSafari()) { event.currentTarget.focus(); } }值得注意的工程细节源码注释中说明mousedown是移动焦点的兼容事件如果改为取消pointerdown会抑制 iOS WebKit 上由浏览器合成的 click 事件导致打开 popover 的按钮永远无法切换开关。另外当 UI 库向触发器注入了自己的onMouseDown例如 Base UI 的菜单触发器靠它打开菜单时必须先展开库的 props再无条件转发到库的 handler——取消默认行为只取消焦点移动不影响事件本身。为什么是两套不同的信号原文档的核心洞察是无副作用的覆盖用宽信号有代价的覆盖用精确信号。工具栏按钮由isTouchDevice()决定。为一次点击取消焦点几乎不付出代价键盘用户照样可以用 Tab 聚焦按钮也没有任何逻辑依赖按钮已聚焦。所以守卫越宽越好——宽是有意的手机上的每一个工具栏包括链接工具栏而不仅是设置 UI 模式的移动格式化工具栏都可能有点击操作。isTouchDevice的实现在 browser.ts它综合navigator.maxTouchPoints 0与matchMedia((pointer: coarse))并且只在浏览器环境首次调用时惰性求值并缓存SSR 期间不会冻结一个假的false。菜单与选择器由 UI 模式决定。取消打开即聚焦对键盘用户是有代价的因此只应在焦点移动会破坏东西的场景应用。若用isTouchDevice()判断就太宽了平板连接了实体键盘时isTouchDevice()返回true但界面上显示的是桌面工具栏——此时若取消菜单聚焦键盘用户会平白失去方向键导航能力。UI 模式的来源是 UIModeContext.ts默认值是desktop。判断逻辑在 FormattingToolbarController.tsx只有isTouchDevice() keyboardOpen虚拟键盘打开时才渲染移动端控制器——因为手机和平板也可能外接键盘鼠标。虚拟键盘的判定由 useVirtualKeyboard.ts 完成它比较缩放不变的布局等效视口高度与历史最大值高度下降超过 150px 视为键盘打开低于真实键盘的约 250px高于地址栏显隐的约 60-100px并对横竖屏切换重置基线。移动端控制器在 MobileFormattingToolbarController.tsx 中通过UIModeContext.Provider valuemobile把模式注入子树并整体 portal 到document.body层级规避 iOS 滚动容器的层叠上下文把固定定位工具栏画到页脚后面同时用useEditorFocus({ includeEditorUI: true })检查用户仍在与这个编辑器交互避免多编辑器同页时键盘打开就同时弹出所有工具栏。三种皮肤如何落实同一套契约三套皮肤对preventFocusOnTap与preventFocusOnOpen的落实路径不同但对外行为一致Ariakit菜单用autoFocusOnShow{preventFocusOnOpen ? () false : true}与focusOnHover{!preventFocusOnOpen}Menu.tsx工具栏按钮与选项在onMouseDown中调用preventFocusOnTapToolbarButton.tsx。Mantine用trapFocus{preventFocusOnOpen ? false : undefined}关闭聚焦陷阱Menu.tsx按钮同样挂载onMouseDown{preventFocusOnTap}ToolbarButton.tsx。shadcn底层 Base UI 的 Menu 与 Select 没有关闭初始聚焦的开关因此封装了一个专用工具 preventFocusOnOpen.ts通过preventFocusOnOpenProps(enabled)注入等价于不聚焦的 propsMenu.tsx。端到端测试 skinFocus.test.tsx 同时覆盖三套皮肤mantine / ariakit / shadcn验证移动端工具栏的两类承诺点击工具栏按钮不能把焦点移出编辑器否则软键盘关闭从它打开的表面块类型选择器、颜色菜单也不能把焦点移走preventFocusOnOpen除非是主动请求焦点的输入链接表单。测试还专门用trackFocusLeavingEditor区分焦点真的离开与同一任务内交还一次瞬间的 blur 会让键盘闪烁而 shadcn 适配器在任务内交还焦点则不算离开。可能的后续演进指针永不聚焦工具栏按钮原文档在结尾记录了一个未采纳的设计方向把 tap 守卫同样应用到鼠标点击上让指针在任何设备上都不聚焦工具栏按钮——这样设备检测和 Safari 特殊分支都可以删除。理由大多数工具栏按钮在点击后立即把焦点交还编辑器桌面工具栏也不依赖按钮聚焦。代价则是Ariakit 工具栏的 roving tabindex 不再跟随鼠标点击点击后按 Tab 会从编辑器而不是刚点过的按钮继续。在 mouseDownFocus.ts 的注释中同样标注了possible follow-up表明这是一个有意保留的开放问题而非已实现的特性。小结BlockNote 的焦点管理可以概括为一条规则与两套信号规则是编辑器保持焦点、只有autoFocus输入能抢走它信号是无代价的覆盖用isTouchDevice()工具栏按钮、有代价的覆盖用 UI 模式菜单与选择器的preventFocusOnOpen。移动端软键盘的存续是这一切的锚点桌面端无障碍方向键导航、roving tabindex则是另一端的约束。理解这层设计对自定义格式化工具栏、接入新 UI 皮肤或排查手机上点击按钮键盘消失类问题时都能直接定位到正确的决策点。赞分享前端富文本UI组件AI 应用【免费下载链接】BlockNoteA React Rich Text Editor thats block-based (Notion style) and extensible. Built on top of Prosemirror and Tiptap.项目地址https://gitcode.com/gh_mirrors/bl/BlockNote点击查看免费下载相关推荐WinUI TeachingTip 焦点行为深度解析F6 键盘导航与 Light-Dismiss 焦点管理机制WinUI TeachingTip 焦点行为深度解析F6 键盘导航与 Light Dismiss 焦点管理机制 本篇文章基于 microsoft ui xam前端UI组件桌面应用BlockNote无障碍焦点管理键盘导航优化BlockNote无障碍焦点管理键盘导航优化 概述 BlockNote作为一款基于Prosemirror和Tiptap构建的Notion风格块级可扩展文本前端富文本UI组件AI 应用如何快速上手Teku5步搭建以太坊信标节点如何快速上手Teku5步搭建以太坊信标节点 想要参与以太坊2.0网络并成为网络验证者吗Teku作为一款开源的以太坊共识客户端为您提供了完整的信标节点和验证上一篇Qwen3.6-35B-A3B-Escha-W2高级特性结构化输出与工具调用功能的使用技巧下一篇ens-contracts高级功能NameWrapper如何实现域名所有权与权限管理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表