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

资讯详情

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

Astryx 交互模态架构(Interaction Modality):键盘、指针与触控焦点一致性的共享状态设计

Astryx 交互模态架构(Interaction Modality):键盘、指针与触控焦点一致性的共享状态设计 Astryx 交互模态架构Interaction Modality键盘、指针与触控焦点一致性的共享状态设计【免费下载链接】astryxAn open source design system thats fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx导读本指南解读 Astryx 开源设计系统中决定焦点环何时可见的跨组件架构约定——交互模态interaction modality。它回答一个看似简单、实现却极其微妙的问题当用户用键盘、鼠标、触摸或手写笔来回切换操作时组件如何可靠地判断上一次输入是什么从而决定是否绘制键盘焦点指示器。读完本文你将掌握 Astryx 的共享模态状态模型、9 条交互不变量INV1–INV9、焦点可见性与焦点所有权的边界划分以及这些约定在源码interactionModality.ts、focusOutline.stylex.ts中的落地方式与验证标准。为什么需要共享模态:focus-visible不够用浏览器原生提供了:focus-visible选择器来决定焦点环的绘制但 Astryx 在 interactionModality.ts 的文档注释中明确指出一个关键事实:focus-visible并不等价于由键盘聚焦。按照 CSS Selectors 4 规范一个支持文本输入的指针聚焦元素也会匹配:focus-visible——这是故意的设计因为点击一个文本框时仍然需要显示输入将落在哪里。在 Chromium 中实测用鼠标点击一个裸input同样会匹配:focus-visible。因此一个只想在键盘聚焦时显示焦点环的组件需要:focus-visible无法提供的额外信息到底是哪种设备移动了焦点。Astryx 的方案不是用自定义状态替换浏览器的启发式判断而是把共享模态状态作为一个门控gate与:focus-visible并列使用——浏览器的启发式仍然决定其他一切包括focus({focusVisible: true})的语义。系统模型一个共享的最后一次输入状态状态转移规则Astryx 将最后一次输入模态记录为全局共享状态规则简单而精确输入事件模态结果未按住 Meta、Alt、Control 的按键keydownkeyboard指针按下pointerdown包括鼠标、触摸、手写笔pointer按住 Meta、Alt 或 Control 的按键不改变当前模态任何输入发生之前keyboard安全默认值两个值得注意的设计决策修饰键按下不改变模态。源码 interactionModality.ts实际路径见下中的onKeyDown处理器明确注释按住 Shift 并不是导航——如果用户在点击前按住 Shift这不应把一次点击变成键盘输入。测试用例也验证了Shift、Tab、ArrowRight按住 Shift 均计为键盘输入见 interactionModality.test.ts。初始状态是keyboard。在任何输入之前焦点要么是程序化设置的要么由浏览器恢复此时显示焦点环是安全的一侧。服务端渲染无document同样返回keyboard默认值见 interactionModality.ts。程序化焦点不产生新模态程序化聚焦.focus()不会创建新的模态。它的焦点指示器跟随最后一次输入模态指针输入后隐藏键盘输入后显示。组件结合浏览器:focus-visible行为与共享模态状态处理仅靠:focus-visible无法区分输入来源的情况。单例存储document 级别共享模态存储在document对象上通过Symbol.for(astryxdesign/core/interaction-modality/v1)键这意味着重复加载的 Astryx bundle 共享同一个状态值和同一对监听器。第一个挂载的消费者激活跟踪之后监听器在整个 document 生命周期内保持注册——即使在消费者卸载的空档期发生输入也不会丢失。模块导入本身没有 DOM 副作用服务端渲染保持键盘安全的默认值。测试 interactionModality.test.ts 专门验证了两份模块副本只注册一对监听器、且卸载后不注销的行为。核心概念分层可见性 vs. 所有权 vs. 悬停文档确立了三个容易混淆的职责边界模态Modality回答共享焦点指示器是否应可见。这是全局共享的、跨组件一致的决策。组件所有权Ownership回答哪个元素绘制焦点环。语义焦点所有者可以自己绘制、委托给一个包裹 wrapper 绘制或在可聚焦元素被有意隐藏时绘制在一个视觉代理visual proxy上。所有权不会改变模态状态。悬停能力与模态历史无关。悬停行为只对具备悬停能力的指针可用且始终是对键盘、点击和触摸路径的增强。触摸和手写笔的按压即使在某组件族中具有不同的手势行为也参与指针模态。此外禁用disabled、忙碌busy、只读read-only、有意隐藏、有意不可用的语义也与模态分离——这些状态由各自的组件、组件族或系统契约决定操作是否存在、可聚焦、可操作模态只控制合格焦点所有者上的焦点指示器。九条不变量INV1–INV9交互行为的边界文档定义了 9 条跨组件必须遵守的不变量这是整个架构的契约核心INV1 — 最后一次输入拥有程序化焦点可见性。程序化焦点保留当前模态指针输入后不显示共享焦点指示器键盘输入后显示。INV2 — 模态与焦点所有权分离。共享模态决定可见性每个组件族决定语义焦点所有者与代表其绘制的元素。INV3 — 一次焦点移动只有一个所有者指示器。wrapper 或视觉代理可以为语义所有者绘制但兄弟控件绘制自己的焦点替换内容不能静默移除所有者的指示器。INV4 — 每种模态都有可操作路径。键盘、指针、触摸/手写笔都能到达每个受支持交互。悬停永远不是唯一的发现或激活路径。INV5 — 既有状态语义优先。禁用控件不会因模态而变得可交互只读和忙碌控件保留其文档化的焦点与激活行为。INV6 — 模态保持内部性。组件从共享浏览器输入状态推导模态。公开焦点可见性或模态 prop 需要单独的公共 API 决策与architecture:public-component-api耦合见 public-component-api.md。INV7 — 必要操作保持可达。在组件提供必要操作受控上下文controlled context的交互点上每种受支持模态都有可感知、可操作的路径。布局、滚动、溢出、重排、响应式组合、替换内容或自定义渲染器接缝custom-renderer seam不得将所有路径与上下文分离。这不要求持续可见、粘性sticky或增强视觉突出度。INV8 — 更新颖的所有者接受的意图拥有当前结果。当前组件/组件族契约定义意图接受、待定呈现与同一交互结果的取代关系supersession。一旦被取代旧工作的完成不得改变当前结果的内容、选择、状态或播报、可见性、焦点或激活。无关的后台工作保持其既有所有者与生命周期。INV9 — 交互发现需要可观察的伤害。共享交互违规必须能在可复现的受支持状态与模态中造成具体用户伤害证据必须指明丢失或过期的结果并验证代表性正常/遗留路径不受补救措施影响。审美偏好、推测性不支持状态、该操作本可以更容易被发现这类主张不构成交互违规。其中 INV9 是关键的质量闸门它防止把视觉偏好伪装成交互可达性问题同时保留已由设计/主题/无障碍权威裁定的视觉修正走渲染像素证据和直接所有者的正常通道。允许的差异组件族保留自己的交互模型架构不强制统一外观而是允许组件族保留其既有的交互模型同时这些差异被视为所有权与行为选择而非替代模态定义操作actions可在可聚焦操作上绘制共享轮廓带边框的字段可在所有者的 wrapper 上绘制既定的字段焦点处理field-focus treatment隐藏的原生输入可由所有者代码在可见指示器上绘制焦点菜单可随鼠标悬停移动焦点使指针与键盘共享同一个高亮项空间控件spatial controls可在指针手势期间移动焦点而不显示键盘焦点指示器。但注意新的视觉表示仍需设计评审本记录并不授权新视觉。源码落地共享焦点环的实现机制1. 模态跟踪interactionModality.ts核心实现位于 interactionModality.ts导出三个 APIuseInteractionModalityTracking()挂载时激活 document 级跟踪getInteractionModality(): keyboard | pointer读取当前模态测试专用__resetInteractionModalityForTest()与__startInteractionModalityTrackingForTest()。监听器在 capture 阶段、passive 模式下注册到 document。核心的修饰键过滤逻辑onKeyDown: event { // Modifier-only presses are not navigation — holding Shift before a // click must not turn that click into keyboard. if (event.metaKey || event.altKey || event.ctrlKey) { return; } store.modality keyboard; },这解释了文档规则中带 Meta/Alt/Control 的按键不改变模态的精确实现也解释了 Shift 为何不被排除——Shift 不会像 Ctrl 那样触发组合快捷键按住 Shift 再按 Tab/方向键属于真实导航。2. 共享焦点环focusOutline.stylex.tsfocusOutline.stylex.ts 集中了所有核心与实验室组件core 和 lab的键盘焦点环解决了几十个组件各自写 2px accent 环导致偏移漂移、粗细不一的历史问题。关键机制所有值来自--focus-outline-*主题 token宽度、样式、颜色、偏移主题只需一次覆盖即可全局重设焦点环:focus-visible条件不可主题化——主题可以改环的外观但不能把环展示给指针用户提供focusVisible自身焦点、focusWithin后代焦点、focusWithinFirstChild仅首个子元素焦点避免多 tab stop 的 row 出现两个环、suppressed取消环并关掉 UA 默认环、publishFocusVisibleVars、focusWithinOrPublished等多种样式使用长手属性longhands而非outline简写简写会重置其覆盖的所有长手属性导致破坏性按钮的自定义红色环被静默抹掉长手属性允许变体只改颜色而继承宽度、样式与偏移提供FOCUS_OUTLINE_PARTS/FOCUS_OUTLINE_PARTS_NONE供必须命令式绘制焦点环的场景直接Object.assign(el.style, ...)。makeFocusOutlineProps的注释还记录了一个真实事故TreeList 曾因 base style 携带outline: none而出现可聚焦行无可见焦点这正是长手属性设计要防御的回归。3. 自适应表面返回焦点useFocusReturnVisibility.tsuseFocusReturnVisibility.ts 处理从自适应浮层返回焦点时的可见性策略当浮层如下拉菜单关闭并把焦点还给触发元素时如果关闭动作是指针发起的则抑制焦点环如果当前模态是键盘则恢复显示。它暴露prepareFocusReturn根据当前模态决定是否抑制、resetFocusReturn、onFocusReturnTargetFocus三个回调。以 DropdownMenu.tsx 为例关闭时调用prepareFocusReturn()打开时resetFocusReturn()并在触发按钮上应用isFocusRingSuppressed focusOutlineStyles.suppressed把指针关闭菜单后返回触发按钮不闪环落实到位。而键盘关闭路径DropdownMenu.tsx则判断getInteractionModality() keyboard时显式trigger?.focus()指针路径则显式 blur避免 Safari 在触摸选择后给触发元素画一个focus-visible环。4. 隐藏输入的视觉代理useIndicatorFocusRing.tsxuseIndicatorFocusRing.tsx 解决一个棘手的谁画环问题复选框/单选的原生input是opacity: 0的可见焦点环必须出现在旁边的主题化指示器indicator上。但指示器可能是主题提供的第三方代码把画环责任交给它会导致替换主题没有画环 → 控件无可见焦点违反 WCAG 2.4.7。因此由所有者在焦点时命令式地在指示器元素上绘制。两个承重细节均为实测而非假设用内联样式而非 classNameReact 在重渲染时会整体替换className注入的类会在控件状态变化如按下 Space时消失导致焦点环丢失而 React 按属性调和style此处设置的outline得以存活用:focus-visible而非:focus检查保留浏览器启发式包括focus({focusVisible: true})这是手写模态猜测会遗漏的。5. 触摸层触发useTouchTrigger.tsuseTouchTrigger.ts 展示了模态概念在 Tooltip/HoverCard 层上的延伸。悬停是触摸屏无法表达的触发器点击会合成mouseenter未处理的悬停层要么每次点击都打开并残留要么吞掉用户瞄准下层控件的点击。useTouchTrigger通过touchTrigger: auto | tap | none决定auto模式下若触发器本身是动作元素按钮、链接、表单控件由isActionTrigger依据 ARIA role 与标签判断则保持关闭否则点击打开、再次点击外部关闭。手写笔的按压也属于指针模态——笔在探测范围内会像鼠标一样悬停pointerenter无接触只有落地按压才被当作 tap。该 hook 同样调用useInteractionModalityTracking()并组合getInteractionModality()判断交互是否属于触摸isTouchInteraction。恢复性变更路由与变更耦合架构明确architecture:knowledge-contracts见 knowledge-contracts.md单独定义变更处置。INV7 与 INV8 可以说明某个变更所恢复的当前结果但不能授权额外的可观察公共变更。新 API、异常、默认值、交互模型、图标或控件尺寸、粘性行为、调用方所有布局、新视觉表示都归其直接当前所有者——除非证据证明该确切机制是恢复不变量所必需的且代表性未受影响路径保持不变。变更耦合规则Change Coupling划定了 PR 边界修改模态跟踪、焦点返回可见性、共享焦点样式或视觉代理所有权的须在同一 PR 内更新相关代表性测试移动组件的语义焦点所有者或绘制所有者的须验证键盘顺序、两种模态后的程序化焦点、单一指示器所有权以及适用的禁用/忙碌/只读行为增加指针依赖行为的须验证鼠标、触摸、手写笔如适用并包含非悬停路径移动、隐藏、裁剪、替换或异步更新必要操作的须验证受影响状态下每种受支持模态、关闭与取代如适用以及一个代表性未受影响状态改变焦点外观仍与设计约定与主题验证耦合通过公共 API 暴露模态仍与architecture:public-component-api耦合见 public-component-api.md。拥有代码与运行时归属模态架构的拥有边界清晰interactionModality.ts — 记录并暴露共享的最后输入模态focusOutline.stylex.ts 与焦点 token — 提供共享焦点指示器机制与主题接缝但不选择组件所有权useFocusReturnVisibility.ts — 焦点从自适应表面返回时应用最后输入可见性useIndicatorFocusRing.tsx — 让语义所有者保证在可替换的视觉指示器上绘制当前组件与组件族记录定义焦点目标、键盘与指针行为、必要操作可用性、接受意图、取代关系、待定生命周期与焦点绘制所有权architecture:react-component-runtime见 react-component-runtime.md拥有共享生命周期与资源机制本记录只拥有跨切面的模态路径与无过期交互结果需求。验证策略单元测试与真实浏览器证据不变量与证据映射文档提供了严格的验证矩阵每一条不变量对应明确的失败信号INV1/INV2键盘与指针输入后的程序化焦点用例——失败信号是程序化焦点发明了模态或指针历史绘制了键盘指示器INV3直接所有者、wrapper 所有者、可替换指示器、嵌套操作用例——失败信号是无指示器、双指示器或替换内容可以退出INV4键盘、鼠标、触摸、手写笔浏览器路径——失败信号是仅悬停行为、粘性触摸悬停、不可达操作INV5禁用、忙碌、只读的 DOM 与激活断言——失败信号是非活动控件响应、合格控件失去焦点、或状态随模态改变INV6公共 API 评审与代表性源码检查——失败信号是组件在无独立 API 决策时添加调用方可控模态INV7渲染浏览器的受影响状态检查几何、裁剪、滚动、命中测试、内置与自定义渲染器路径——失败信号是在某交互点上一种模态失去全部必要路径或证据用类相等性替代可达性/可感知性INV8所有者定义的意图顺序、待定、关闭、取消、禁用、卸载变更——失败信号是所有者定义取代后旧工作改变当前内容、选择、状态/播报、可见性、焦点或激活INV9具体伤害复现、代表性正常/遗留/无新 prop 路径、附带公共变更清单——失败信号是把不支持或审美偏好当作伤害、未受影响行为被改变、或 API/布局/尺寸/默认/粘性/异常变更逃逸直接权威。何时必须使用真实浏览器当行为依赖以下因素时必须提供真实浏览器证据:focus-visible、指针能力、触摸/手写笔合成、渲染焦点所有权、滚动、裁剪、命中测试或受支持呈现中必要控件是否仍可感知。单元测试只能证明事件路由、DOM 状态与确定性意图排序不能替代这些浏览器结果——这是文档反复强调的验证红线。单元层证据可参考 interactionModality.test.ts它覆盖了 Shift 键策略、无 document 的安全默认、单例监听器连续性组件层可参考文档列出的 CheckboxInput.test.tsx、DropdownMenu.test.tsx、Selector.test.tsx、Slider.test.tsx。基准验证样例文档将 PR #5648 定位为验证基准benchmark而非权威。其 ChatComposer 夹具覆盖键盘所有的编辑器焦点、指针抑制、指针按下时清除既有键盘指示器、内部动作焦点归动作自身所有以及键盘与指针输入后程序化编辑器焦点在行为被视为完成前的双重验证。真实用法可在 ChatComposer.tsx 中看到组合编辑器焦点通过isComposerEditor(event.target) getInteractionModality() keyboard计算键盘编辑器焦点状态将模态作为状态驱动的输入。决策历史与适用范围本记录是当前权威authority: current由cixzhang批准2026-09-10。最后输入规则于 2026-08-30 作为系统架构决策批准必要操作可达性、更新颖意图优先与证据/范围闸门于 2026-09-10 批准。没有独立规范改变本记录。它明确不定义焦点环外观、悬停绘制、按压处理或新组件 API——这些仍归属设计约定、主题 token 与所属组件族。对于正在构建或审查 Astryx 组件的开发者这份架构的核心行动准则可以浓缩为三句话让浏览器:focus-visible做它擅长的事用共享模态状态补上哪种设备发起的焦点这一缺失位把环是否可见全局模态与谁来画环组件所有权彻底分离任何声称修复交互问题的变更都必须先证明可复现的用户伤害并清单化所有附带公共行为变化。【免费下载链接】astryxAn open source design system thats fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表