Tamagui v2 焦点模型重构:用 `tabIndex` 取代 React Native `focusable` 属性的完整迁移方案
Tamagui v2 焦点模型重构用tabIndex取代 React Nativefocusable属性的完整迁移方案【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui本文基于 plans/focusable-remove.md 这一内部技术方案文档完整讲解 Tamagui v2 将 React Nativefocusable属性迁移到 Web 标准tabIndex的设计思路、语义映射、逐文件改造清单与验证策略并结合当前仓库源码给出实现层证据与进行中的迁移状态帮助维护者与贡献者理解并安全完成本次重构。方案背景与目标在 Tamagui 中组件同时面向 Web 与 React Native 两个渲染目标。React Native 提供了focusable布尔属性用于控制组件是否可获得焦点而 Web 平台的标准做法是使用tabIndex属性tabIndex{0}表示进入 Tab 键顺序、tabIndex{-1}表示可编程聚焦但不在 Tab 顺序中。方案文档明确了本次重构的目标Remove the React Nativefocusableprop from Tamagui v2 in favor of the web-standardtabIndex. This simplifies the API and aligns with web conventions.即在 Tamagui v2 中移除 React Native 风格的focusableprop统一改用 Web 标准的tabIndex。这样做的好处在于API 统一开发者只需记住一套焦点语义tabIndex无需再区分平台差异与 Web 惯例对齐tabIndex是 Web 平台的事实标准能更精确地表达是否在 Tab 导航顺序中语义更丰富focusable只有布尔语义而tabIndex的整数值能表达更多 Tab 顺序细节。需要特别说明的是这不是彻底去掉焦点能力而是把焦点控制的入口从 RN 专有 prop 换成跨平台统一的tabIndex。Native 端仍需要focusableRN 渲染层只认focusable因此由 Tamagui 核心在底层完成tabIndex → focusable的转换对上层 API 使用者透明。核心语义映射方案文档给出了三组最常用的迁移映射关系这是整个重构的换算表原写法focusable新写法tabIndexfocusable{true}tabIndex{0}focusable{false}tabIndex{-1}focusable{!disabled}tabIndex{disabled ? -1 : 0}记忆要点tabIndex{0}等价于可聚焦且进入 Tab 顺序原focusable{true}tabIndex{-1}等价于不可通过 Tab 聚焦原focusable{false}元素仍可通过脚本调用.focus()编程式聚焦对于禁用态的组件disabled为真时对应-1否则为0。底层实现剖析focusable当前在两条链路上的流转在动手修改前理解focusable在 Web 与 Native 两条渲染链路中的流转路径至关重要。以下分析均基于当前仓库源码。Web 链路forwardedProps→createDOMProps→ DOMtabIndexWeb 端focusable目前经过两层处理属性转发声明code/core/react-native-web-internals/src/modules/forwardedProps/index.tsx 的accessibilityProps对象中声明了focusable: true第 68 行这意味着该 prop 会被透传到后续处理。DOM 属性换算createDOMProps/index.tsx 将focusable换算为实际的 DOMtabIndex字符串第 362-393 行focusable false时设置tabIndex -1对a、button、input、select、textarea等原生可聚焦元素focusable false或accessibilityDisabled true时设置tabIndex -1对role为button、checkbox、link、radio、textbox、switch的角色元素focusable ! false时设置tabIndex 0其余元素仅在focusable true时设置tabIndex 0。从源码结构看createDOMProps已经内置了一套元素类型 ARIA role focusable组合推断tabIndex的规则。方案文档指出一旦从forwardedProps中移除focusable这个 prop 根本不会到达createDOMProps因此该文件的 focusable 分支第 362-393 行可以一并清理或保留保留也不会再被触发文档原话为Or just leave it - once removed from forwardedProps it wont reach here。Native 链路createComponent注入 →createOptimizedView.native.tsx换算Native 端目前存在两处focusable处理code/core/web/src/createComponent.tsx 第 1531 行在TAMAGUI_TARGET native分支中向事件对象注入focusable: viewProps.focusable ?? true——即默认所有 Tamagui 视图都是可聚焦的。方案要求移除该行以及focusable作为输入 prop 的接受。code/core/core/src/createOptimizedView.native.tsx 第 117-119 行这是本次重构在 Native 端的关键桥梁它负责把 Web 语义的tabIndex反向换算为 RN 的focusableconst f tabIndex ! undefined ? !tabIndex : focusable if (f ! null) { viewProps.focusable f }即tabIndex为0时focusable true为-1时focusable false!0 true、!(-1) false。方案文档明确要求保留这一转换KeeptabIndex→focusableconversion for native (RN still uses focusable)只需移除对focusable作为输入 prop 的接受第 40 行的解构。需要特别澄清的是该文件中tabIndex是透传 换算的语义——从 webAlignment.native.test.tsx 第 170-177 行的测试可以看到tabIndex会原样保留在viewProps.tabIndex中tabIndex is converted to focusable in createOptimizedView.native.tsx同时被用于推导focusable。类型层TamaguiComponentEventscode/core/web/src/interfaces/TamaguiComponentEvents.tsx 第 8 行目前定义了focusable?: any。方案要求将其从类型中移除确保 TypeScript 层面不再暴露该 prop让误用focusable的代码在编译期即报错这是强制 API 收敛的第一道防线。分文件改造清单方案文档将改动分为核心层移除类型与转发、UI 组件层focusable → tabIndex与测试层三大部分并明确划定了范围边界。以下清单完整继承原方案并补充当前仓库的源码状态对照。一、核心层移除类型与转发文件修改动作TamaguiComponentEvents.tsx第 8 行从类型中移除focusable?: anyforwardedProps/index.tsx第 68 行从accessibilityProps中移除focusable: truecreateDOMProps/index.tsx移除 focusable 处理第 362-393 行或保留——一旦从forwardedProps移除后它不会再被触达createComponent.tsx第 1531 行移除focusable: viewProps.focusable ?? truecreateOptimizedView.native.tsx第 40、117-119 行保留tabIndex → focusable的 Native 转换仅移除对focusable作为输入 prop 的接受其中createOptimizedView.native.tsx是唯一既要改又不改的文件它的换算逻辑是 Native 焦点能力的命脉一旦误删RN 端所有组件将失去可聚焦性必须谨慎处理。二、UI 组件层focusable→tabIndex方案文档列出了 9 个 UI 组件文件逐一对照如下文件原写法新写法code/ui/button/src/Button.tsxfocusable: true第 84 行、focusable: undefined第 295 行tabIndex: 0删除第 295 行code/ui/toggle-group/src/ToggleGroup.tsx第 83 行focusable{!disabled}tabIndex{disabled ? -1 : 0}code/ui/toast/src/ToastImpl.tsx第 34 行focusable: truetabIndex: 0code/ui/toast/src/ToastViewport.tsx第 293 行focusable{context.toastCount 0}tabIndex{context.toastCount 0 ? 0 : -1}code/ui/radio-headless/src/useRadioGroup.tsx第 235 行focusable: !isDisabledtabIndex: isDisabled ? -1 : 0code/ui/create-menu/src/createBaseMenu.tsx第 1039 行focusable{!disabled}tabIndex{disabled ? -1 : 0}code/ui/tabs/src/createTabs.tsx第 142 行focusable{!disabled}tabIndex{disabled ? -1 : 0}code/ui/input/src/shared.tsx第 20 行focusable: truetabIndex: 0code/ui/roving-focus/src/RovingFocusGroup.tsx第 172、237 行第 172 行focusable{focusable}tabIndex{focusable ? 0 : -1}第 237 行按需更新ItemData类型这些改动大多是在styled默认样式、组件调用RovingFocusGroup.Item或事件处理器注入处的机械替换。其中RovingFocusGroup值得额外注意它是 ToggleGroup、Tabs、Menu 等复合组件的罗盘焦点基础设施其ItemData类型第 236 行{ id: string; focusable: boolean; active: boolean }中focusable字段是内部集合登记用的布尔值并非透传给 DOM 的 prop是否需要改名取决于是否要彻底清除该单词的歧义。三、测试层文件修改动作createDOMProps/tests/index-test.tsx更新或移除 focusable 相关测试core-test/webAlignment.web.test.tsx第 213-220 行更新测试期望core-test/webAlignment.native.test.tsx第 188-194 行更新测试期望compiler/static-tests/tests/webAlignment.web.test.tsx第 221 行更新测试测试层覆盖两个维度运行时行为getSplitStyles / createDOMProps 的输出与编译产物static 编译器抽取出的 JS 中不得再出现 focusable 相关换算。四、明确超出范围方案文档特别声明code/packages/react-native-web-lite- This is a separate RN-web compatibility layer, keep focusable there即 code/packages/react-native-web-lite 是独立的 RN-web 兼容层是 Tamagui 复刻的 react-native-web必须保留focusable原样支持不做任何迁移。这是本次重构的范围红线避免误伤兼容层。从源码看迁移的进行中状态方案文档是一份待执行的改造计划而当前仓库代码呈现的正是改造进行到中途的真实状态——这非常有助于读者理解哪些是计划、哪些已落地已完成迁移的组件源码中已使用tabIndexButton.tsx 第 47 行已在styled默认样式中写入tabIndex: 0同时保留了第 345 行渲染处的tabIndex{0}ToastImpl.tsx 第 43 行已是tabIndex: 0input/src/shared.tsx 第 15-21 行已采用平台分支写法Web 端tabIndex: 0 as const、Native 端保留focusable: true恰好印证了方案Web 用 tabIndex、Native 用 focusable 内部转换的分层思路。尚未迁移的组件源码中仍使用focusableToggleGroup.tsx 第 63 行仍是focusable{!disabled}ToastViewport.tsx 第 293 行仍是focusable{context.toastCount 0}useRadioGroup.tsx 第 235 行仍是focusable: !isDisabledcreateBaseMenu.tsx 第 1143 行仍是focusable{!disabled}createTabs.tsx 第 139 行仍是focusable{!disabled}。核心层测试已先行落地值得注意的是webAlignment.web.test.tsx 第 212-221 行已经存在focusable is NOT converted (use tabIndex instead)的断言——验证focusable: true不再产生任何tabIndex输出webAlignment.native.test.tsx 第 188-194 行同样断言了 native 侧不再转换static-tests/tests/webAlignment.web.test.tsx 第 216-234 行的extractForWeb测试则验证编译产物中focusable不再被换算为tabIndex。这意味着核心层的移除 focusable 转换在 Web 侧实际上已经生效测试即证据而 UI 组件层仍有一批存量使用点待清理。这也印证了方案文档中Main risk is missing a usage somewhere的风险判断——迁移最危险的不是某个文件改错而是漏改某一处使用点。测试与验证策略方案文档给出了三步验证流程在受影响包中运行测试yarn test在对应包目录下。重点覆盖上文的四个测试文件确认 webAlignment 系列测试与 createDOMProps 测试全部通过。检查 kitchen-sink 的焦点行为kitchen-sink 是 Tamagui 的组件演练场见 code/kitchen-sink应手动验证按钮、Tab、Toggle、菜单、Toast 等组件的键盘 Tab 导航行为是否与迁移前一致。验证 Web/Native 双端语义Web 端确认tabIndex{0}的元素可被 Tab 聚焦、tabIndex{-1}的元素被移出 Tab 顺序Native 端确认tabIndex能正确映射为focusable由 createOptimizedView.native.tsx 第 117-119 行保证。此外createDOMProps/tests/index-test.tsx 中现有测试覆盖了不同元素类型 role focusable 组合 → tabIndex的既有行为迁移后这些测试要么删除、要么改为直接断言传入的tabIndex原样透传。难度评估与主要风险方案文档将该任务评为Medium中等难度并给出了风险画像Medium- Straightforward changes but spread across ~15 files. Main risk is missing a usage somewhere.改动本身直白核心是机械替换语义映射唯一不存在多选一的设计分歧风险在于覆盖面改动分散在约 15 个文件最大的风险是遗漏某个focusable使用点尤其藏在styled默认样式、variants或data属性中的写法Native 端是暗雷createOptimizedView.native.tsx的tabIndex → focusable换算必须保留误删会导致 RN 端焦点完全失效范围纪律react-native-web-lite兼容层明确不在范围内切勿顺手统一掉。实操建议迁移时可先全局搜索focusable关键字覆盖code/core、code/ui两棵子树对照上文清单逐项核对再运行双端测试与 kitchen-sink 手动验证即可将遗漏风险降至最低。小结本方案通过一次语义归一重构让 Tamagui v2 的焦点 API 全面对齐 Web 标准上层统一使用tabIndexNative 端由 createOptimizedView.native.tsx 在底层完成到focusable的换算react-native-web-lite兼容层保持原样。当前仓库中核心层测试已先行落地、部分组件已完成迁移剩余工作集中在 UI 组件层的存量替换与测试收尾——对照 plans/focusable-remove.md 的清单逐项执行即可安全完成这次跨约 15 个文件的焦点模型重构。【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考