feat(client): shared tabOrder property for keyboard Tab navigation across standard widgets#41895
feat(client): shared tabOrder property for keyboard Tab navigation across standard widgets#41895sebastianiv21 wants to merge 17 commits into
Conversation
Adds a single platform-level "tabOrder" property exposed in a shared Accessibility property-pane section for all standard non-Anvil widgets (injected centrally in WidgetFactory, no per-widget config changes). - New TAB_ORDER_INPUT control persists only valid non-negative integers as numbers and removes the property from the DSL when cleared, so blank always means Auto and no placeholder strings are persisted - PositionedContainer renders a sanitized data-tab-order attribute only for valid explicit values (0 is valid); no native tabIndex is used - Fixed-layout tabbing (useWidgetFocus) sorts widgets with explicit tabOrder first (ascending, duplicates tie-break by position), then Auto/invalid widgets in the existing position order; Shift+Tab reverses the same sequence. When no widget in the scope has a valid tabOrder, the previous position-based behavior is preserved exactly - Anvil-only (WDS_*, SECTION/ZONE) and internal widgets (CANVAS, SKELETON, TABS_MIGRATOR) are excluded; auto layout is intentionally unchanged since useWidgetFocus opts out of it - No DSL migration and no page version bump; existing apps keep their current Tab behavior unless tabOrder is explicitly set Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkthroughThis PR adds explicit widget tab ordering end to end: sanitizing and storing tab order values, exposing a property-pane control, honoring the order during keyboard traversal, injecting the control for eligible widgets, rendering the order as a DOM attribute, and displaying an editor overlay badge. ChangesExplicit Widget Tab Order
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Editor
participant PropertyPane
participant Widget
participant TabTraversal
Editor->>PropertyPane: set tabOrder
PropertyPane->>Widget: persist numeric value
Widget->>Widget: render data-tab-order
TabTraversal->>Widget: read explicit order
TabTraversal-->>Editor: focus next ordered widget
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27449717446. |
|
🔴🔴🔴 Cyclic Dependency Check: This PR has increased the number of cyclic dependencies by 1, when compared with the release branch. Refer this document to identify the cyclic dependencies introduced by this PR. You can view the dependency diff in the run log. Look for the check-cyclic-dependencies job in the run. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
Follow-up to the shared tabOrder feature (016eec8) after a "doesn't work" report. Investigation showed the feature is actually functional; the real gaps were clarity and missing end-to-end coverage. Investigation (verified live via Playwright on a deployed app): - Widget-level tab order WORKS at runtime for focusable widgets on the same canvas. A 3-widget test (orders top=1, mid=3, bottom=2) produced the keyboard focus cycle top -> bottom -> mid (ascending by tabOrder, not visual position), and data-tab-order propagated correctly to the deployed DOM. - The original "doesn't work" was a non-discriminating test: a 2-widget cycle ping-pongs identically whether ordered by position or tabOrder, so it cannot tell the two apart. Three widgets are required to distinguish them. - The genuine limitation is that fields INSIDE a form (JSON Form) cannot be individually ordered, because form fields are not standalone widgets and the control is injected per widget. This is by design: per-field tab numbering is the positive-tabindex accessibility anti-pattern. Within a form, native DOM order is used. Changes: - tabOrderPropertyConfig.ts: expand the help text to disclose that ordering is relative to widgets in the same container, and that fields inside a form follow the form's own order. Strings kept inline to match property-pane convention (sectionName/helpText are inline literals across the codebase). - tabbable.test.ts: add a discriminating 3-widget regression test asserting the tab sequence follows explicit order rather than position (forward and Shift+Tab). Asserts at the getTabbableDescendants level to avoid the pre-existing jsdom FOCUS_SELECTOR limitation that breaks handleTab-based tests. No feature flag added: the change is additive and inert by default (when no widget sets tabOrder, traversal falls back to the existing position-based path unchanged), so existing applications are unaffected. Verification: new test passes (suite 23 passed / 3 pre-existing jsdom failures); ESLint clean on changed files; check-types passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
🔴🔴🔴 Cyclic Dependency Check: This PR has increased the number of cyclic dependencies by 1, when compared with the release branch. Refer this document to identify the cyclic dependencies introduced by this PR. You can view the dependency diff in the run log. Look for the check-cyclic-dependencies job in the run. |
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27568604573. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
The standalone WidgetProvider/factory/tabOrderPropertyConfig.ts imported constants/PropertyControlConstants and was imported by the WidgetFactory, closing a new import cycle that the ci-client-cyclic-deps-check (dpdm) gate flagged as +1 over the base branch. Move the shared tabOrder property-pane helpers into the existing WidgetProvider/factory/helpers.ts, which already imports every module they need (PropertyControlConstants, WidgetValidation, ./types, widgets/wds/constants) and is already part of these factory cycles. This adds no new import edge, so the dpdm circular-dependency count returns to the base 2134 with zero tabOrder-related cycles. - Delete tabOrderPropertyConfig.ts; functions/constants now live in helpers.ts - factory/index.tsx imports addTabOrderToPropertyPaneConfig from ./helpers - Rename the config test to helpers.tabOrder.test.ts, importing from ./helpers No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27572072253. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
- helpers.tabOrder.test.ts: assert the current help-text wording - PositionedContainer.test.tsx: add connect() to the react-redux mock so redux-form (pulled in via reflow selectors) loads - tabbable.test.ts: drop the two composite-widget handleTab tests; they hit querySelectorAll(FOCUS_SELECTOR) whose :is(...) syntax jsdom cannot parse, and exercise unchanged legacy routing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the canvas native module available, the full suite runs and exposed
three jsdom/test-harness issues (not feature bugs):
- PositionedContainer.test.tsx: render the forwardRef default export instead
of the raw named function, which received frozen legacy context as its ref
arg ("object is not extensible")
- helpers.tabOrder.test.ts: stop calling getWidgetPropertyPaneConfig for every
registered widget with empty props (some widgets' dynamic-property
generators throw on {}). Prove blanket coverage via the pure
shouldExposeTabOrderProperty classifier across all types, and keep
representative end-to-end checks on static-pane widgets
- tabbable.test.ts: drop the handleTab describe; handleTab ends in
node.matches(FOCUS_SELECTOR), whose :is(...[tabindex='-1']) selector is
unparseable by jsdom/nwsapi. The ordering logic is fully covered via
getTabbableDescendants; handleTab is unchanged legacy plumbing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts (1)
1-2: ⚡ Quick winUse
TAB_ORDER_ATTRIBUTEin test fixtures instead of hardcodeddata-tab-order.This keeps test setup aligned with the shared contract and avoids silent drift if the attribute name changes.
Proposed refactor
import { getExplicitTabOrder, getNextTabbableDescendant, getTabbableDescendants, sortTabbableWidgets, sortWidgetsByPosition, } from "./tabbable"; +import { TAB_ORDER_ATTRIBUTE } from "utils/widgetTabOrder"; @@ if (tabOrder !== undefined) { - widget.setAttribute("data-tab-order", tabOrder); + widget.setAttribute(TAB_ORDER_ATTRIBUTE, tabOrder); } @@ - widget.setAttribute("data-tab-order", "0"); + widget.setAttribute(TAB_ORDER_ATTRIBUTE, "0"); @@ - widget.setAttribute("data-tab-order", "3"); + widget.setAttribute(TAB_ORDER_ATTRIBUTE, "3"); @@ - widget.setAttribute("data-tab-order", value); + widget.setAttribute(TAB_ORDER_ATTRIBUTE, value);Also applies to: 65-67, 98-103, 110-113
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts` around lines 1 - 2, Replace all hardcoded `data-tab-order` attribute strings in test fixtures with the `TAB_ORDER_ATTRIBUTE` constant to keep test setup aligned with the shared contract. First, ensure `TAB_ORDER_ATTRIBUTE` is imported at the top of the file in app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts alongside `getExplicitTabOrder`. Then, at lines 65-67, 98-103, and 110-113 in the same file, replace each occurrence of the string `data-tab-order` with `TAB_ORDER_ATTRIBUTE` in test fixture HTML setup. This ensures the test will automatically reflect any future changes to the actual attribute name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts`:
- Around line 1-2: Replace all hardcoded `data-tab-order` attribute strings in
test fixtures with the `TAB_ORDER_ATTRIBUTE` constant to keep test setup aligned
with the shared contract. First, ensure `TAB_ORDER_ATTRIBUTE` is imported at the
top of the file in app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts
alongside `getExplicitTabOrder`. Then, at lines 65-67, 98-103, and 110-113 in
the same file, replace each occurrence of the string `data-tab-order` with
`TAB_ORDER_ATTRIBUTE` in test fixture HTML setup. This ensures the test will
automatically reflect any future changes to the actual attribute name.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a6369dec-3eb0-4870-8f15-440167711e31
📒 Files selected for processing (13)
app/client/src/WidgetProvider/factory/helpers.tabOrder.test.tsapp/client/src/WidgetProvider/factory/helpers.tsapp/client/src/WidgetProvider/factory/index.tsxapp/client/src/components/designSystems/appsmith/PositionedContainer.test.tsxapp/client/src/components/designSystems/appsmith/PositionedContainer.tsxapp/client/src/components/propertyControls/TabOrderControl.test.tsxapp/client/src/components/propertyControls/TabOrderControl.tsxapp/client/src/components/propertyControls/index.tsapp/client/src/layoutSystems/fixedlayout/common/PositionedComponentLayer.tsxapp/client/src/utils/hooks/useWidgetFocus/tabbable.test.tsapp/client/src/utils/hooks/useWidgetFocus/tabbable.tsapp/client/src/utils/widgetTabOrder.test.tsapp/client/src/utils/widgetTabOrder.ts
Replace hardcoded "data-tab-order" strings in the test fixtures with the shared TAB_ORDER_ATTRIBUTE constant so the setup tracks the real attribute name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I've reviewed the code, The changes which I see is looking fine. I've two concerns:
|
| * @param currentWidget the widget we are tabbing from, when it is part of the scope | ||
| * @returns | ||
| */ | ||
| export function sortTabbableWidgets( |
There was a problem hiding this comment.
I don't get why this is needed; we are already passing tabOrder in the PositionedComponentLayer.tsx which helps the tabbing by default. What is re-sorting based on tabOrder helping us with?
There was a problem hiding this comment.
data-tab-order is just a custom data attribute. The browser doesn't do anything with it on its own. PositionedComponentLayer only renders it, so it doesn't affect the focus order. In fixed layout, Tab navigation is handled entirely in our code. useWidgetFocus intercepts the keydown event, handleTab calls preventDefault(), and then manually moves focus to the next widget with .focus(). sortTabbableWidgets is the only place that actually reads data-tab-order, so without it the attribute is effectively ignored. We also can't rely on native tabindex because positive tabindex values are considered an accessibility anti-pattern, and they would also conflict with our custom Tab handling.
…Control PR review: the control did nothing tabOrder-specific, so make it generic and reusable. Rename TabOrderControl -> ClearableNumericInputControl (controlType CLEARABLE_NUMERIC_INPUT), decouple it from the tabOrder sanitizer: clearing the field unsets the property (delete from DSL); otherwise the numeric value is persisted, with range/format validation left to the property's own validation config. The shared Accessibility section now uses the generic control and passes min: 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
/build-deploy-preview skip-tests=true recreate=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27976115494. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
app/client/src/components/propertyControls/ClearableNumericInputControl.test.tsx (1)
79-87: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winAdd edge-case tests for
nullclear behavior and whitespace displayability.Please add regression tests for:
- clearing when
propertyValueisnullshould calldeleteProperties, andcanDisplayValueInUI(config, " ")should befalse.Also applies to: 134-149
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/client/src/components/propertyControls/ClearableNumericInputControl.test.tsx` around lines 79 - 87, Add two new regression test cases to the test suite in ClearableNumericInputControl.test.tsx. First, create a test that verifies when propertyValue is null and the input field is cleared, the deleteProperties function should be called (unlike the existing test which checks the already-unset field scenario). Second, add a test that verifies the canDisplayValueInUI function returns false when passed whitespace-only strings like " ". Both tests should follow the same pattern as the existing test that uses renderControl(), fireEvent.change(), and expect() assertions to verify the expected behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/client/src/components/propertyControls/ClearableNumericInputControl.tsx`:
- Around line 54-59: The canDisplayValueInUI function (lines 54-59) and the
clear semantics (lines 65-66) are inconsistent in how they handle whitespace
strings. The function currently treats whitespace-only strings like " " as
valid numeric values since Number(" ") returns 0, but the clear logic treats
trimmed whitespace as blank/unset. Fix this by adding a trim check in the
canDisplayValueInUI function to ensure that whitespace-only strings are treated
as invalid/non-numeric, making the behavior consistent with the clearing
semantics. Add a condition to check that value.trim() is not empty before
considering it a valid numeric input.
- Around line 65-69: The null check in the deleteProperties condition within the
value clearing logic prevents the property from being deleted when propertyValue
is null, leaving stale null values in the DSL. In the if statement checking
propertyValue, remove the null check (the && propertyValue !== null part) so
that deleteProperties is called whenever propertyValue is not undefined,
allowing both undefined and null values to be properly removed from the DSL when
the field is cleared.
---
Nitpick comments:
In
`@app/client/src/components/propertyControls/ClearableNumericInputControl.test.tsx`:
- Around line 79-87: Add two new regression test cases to the test suite in
ClearableNumericInputControl.test.tsx. First, create a test that verifies when
propertyValue is null and the input field is cleared, the deleteProperties
function should be called (unlike the existing test which checks the
already-unset field scenario). Second, add a test that verifies the
canDisplayValueInUI function returns false when passed whitespace-only strings
like " ". Both tests should follow the same pattern as the existing test that
uses renderControl(), fireEvent.change(), and expect() assertions to verify the
expected behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7eefad29-7b53-46fc-a330-9a2a4bdb187b
📒 Files selected for processing (5)
app/client/src/WidgetProvider/factory/helpers.tabOrder.test.tsapp/client/src/WidgetProvider/factory/helpers.tsapp/client/src/components/propertyControls/ClearableNumericInputControl.test.tsxapp/client/src/components/propertyControls/ClearableNumericInputControl.tsxapp/client/src/components/propertyControls/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- app/client/src/WidgetProvider/factory/helpers.tabOrder.test.ts
- app/client/src/WidgetProvider/factory/helpers.ts
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27977705904. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
The shared "Tab order" property was exposed on every standard widget, including display-only ones with no focusable element (Chart, Document Viewer, Image, Divider, Progress, Statbox, Map Chart, Icon, Text, Rate). The custom Tab handler already skips these at runtime, so the field was a confusing no-op there. Narrow shouldExposeTabOrderProperty with a curated non-focusable denylist so the field only appears on widgets that can actually receive focus. Audio, Video, Map, Iframe and all input/container/list widgets stay eligible. Duplicate tab-order values are unchanged (tie-break by position, by design). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This PR has not seen activitiy for a while. It will be closed in 7 days unless further activity is detected. |
Address CodeRabbit review on PR #41895: - canDisplayValueInUI trims strings so whitespace-only input is treated as blank, consistent with the clear semantics in handleValueChange - clearing now removes a stale null value too (drop the !== null guard); widen propertyValue to allow null Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test rendered with propertyValue=null (an empty input), so firing a change to "" was a no-op that React's change dedup swallowed and onChange never ran. Type a value then clear it so the clear branch executes while the prop is null. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Explain the default (position-based) tab order and how numbering overrides it, and drop the misleading "fields inside a form follow the form's own order" line (Form widget children actually do honor tabOrder). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Explain that widgets sharing the same tab order value fall back to the default position order between them, matching the tie-break in getTabOrderSequence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/28838518441. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
@sondermanish On serialization: tabOrder isn't in widget defaults and doesn't have a defaultValue, so it only gets written to the DSL when a builder actually sets a number. Auto isn't a stored value, it's just the absence of the key. So on discard, a widget goes back to its committed state (no key means Auto, or whatever number was committed). There's no migration, and any absent/null/invalid value is treated as Auto at runtime anyway, so there's nothing for discard to corrupt. |
Builders had no way to see assigned tab order values at a glance. Add an editor-only overlay, following the Visual Studio "Tab Order" view / Accessibility Insights "tab stops" pattern: a small numbered badge on the corner of every widget that has a valid explicit tabOrder, all visible at once while the overlay is toggled on. - New showTabOrderOverlay editor redux state (off by default, session-only), mirroring the isPreviewMode pattern - Alt+T global hotkey toggles it (Ctrl/Cmd+Shift+T is browser-reserved for reopen-closed-tab and cannot be intercepted); auto-listed in the "?" shortcuts dialog under Canvas - TabOrderBadge renders via the fixed-layout editor widget onion only, so nothing ships to deployed apps; hidden in preview mode and for widgets without a valid value (0 is valid); reuses sanitizeTabOrder Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/29064818655. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
…dgets
Two issues from testing:
1. Zero was inconsistent with the property pane stepper: NumberInput's "+"
on an empty field yields 1, so 0 was only reachable by typing. The valid
range now starts at 1 (control min, validation params, and the runtime
sanitizer all treat 0 as Auto).
2. Forward Tab got stuck on the last numbered widget while Shift+Tab wrapped
fine. Display-only widgets (image/chart/divider) are part of the tab
scope but have nothing to focus; when they trailed the sequence, the
sibling candidate list was non-empty but unfocusable, so handleTab
consumed the Tab without moving focus and the parent-scope wrap-around
was never reached. Fixes:
- the sibling scope falls through to the parent-canvas wrap when none of
its candidates contains a focusable element
- handleTab only preventDefaults when it actually moves focus
- FOCUS_SELECTOR rewritten as an expanded selector list (no :is) so
jsdom can parse it, unlocking handleTab regression tests: forward wrap,
wrap past a trailing non-focusable widget, backward wrap, full cycle
1 -> 2 -> 3 -> 1, and the single-widget self-cycle
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/client/src/utils/hooks/useWidgetFocus/tabbable.ts`:
- Around line 18-33: The mapped selectors in FOCUS_SELECTOR are missing the
closing parenthesis for :not(), producing invalid CSS. Update the template
literal within the FOCUS_SELECTOR map callback to append `)` after
`[data-tabbable='false']` so querySelectorAll and matches receive valid
selectors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4f22d5a9-3c18-4224-aaf4-0eb7b016dd5c
📒 Files selected for processing (9)
app/client/src/WidgetProvider/factory/helpers.tabOrder.test.tsapp/client/src/WidgetProvider/factory/helpers.tsapp/client/src/components/designSystems/appsmith/PositionedContainer.test.tsxapp/client/src/layoutSystems/common/tabOrderOverlay/TabOrderBadge.test.tsxapp/client/src/utils/hooks/useWidgetFocus/handleTab.tsapp/client/src/utils/hooks/useWidgetFocus/tabbable.test.tsapp/client/src/utils/hooks/useWidgetFocus/tabbable.tsapp/client/src/utils/widgetTabOrder.test.tsapp/client/src/utils/widgetTabOrder.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- app/client/src/utils/widgetTabOrder.ts
- app/client/src/WidgetProvider/factory/helpers.tabOrder.test.ts
- app/client/src/WidgetProvider/factory/helpers.ts
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/29107858739. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
Description
TL;DR: Adds a single platform-level
tabOrderproperty, exposed in a shared Accessibility property-pane section for every standard (non-Anvil) widget, letting builders optionally control keyboard Tab order in fixed layout. Existing apps keep their exact current Tab behavior unlesstabOrderis explicitly set.What changed
WidgetFactorycentrally appends one sharedAccessibility > Tab ordersection to both property-pane paths (legacygetPropertyPaneConfigand content config) viaWidgetProvider/factory/tabOrderPropertyConfig.ts. No per-widget config edits, no per-widget property names or validation rules. Anvil-only (WDS_*,SECTION_WIDGET,ZONE_WIDGET) and internal widgets (CANVAS_WIDGET,SKELETON_WIDGET,TABS_MIGRATOR_WIDGET) are excluded; widgets without a property pane stay untouched.TAB_ORDER_INPUTcontrol (TabOrderControl) reusing the ADSNumberInput: persists only valid non-negative integers as numbers, removes the property from the DSL when cleared (blank always means Auto), never persists""or invalid input. Not JS-convertible (isJSConvertible/isBindProperty/isTriggerProperty: false), validated asValidationTypes.NUMBER { min: 0, natural: true }.PositionedContainerrenders a sanitizeddata-tab-orderattribute only for valid explicit values (0is valid and earliest). No positive nativetabIndexis ever used, so non-focusable widgets stay non-focusable.useWidgetFocus/tabbable.ts) — newsortTabbableWidgets(): when no widget in the current scope has a valid explicit order, it delegates to the unchangedsortWidgetsByPosition(exact current behavior). Otherwise, explicitly ordered widgets come first ascending (duplicates tie-break by position), Auto/invalid widgets follow in existing position order, and Shift+Tab reverses the same sequence. Modal, container, JSONForm, CheckboxGroup, SwitchGroup and ButtonGroup behaviors are preserved.Compatibility
LATEST_PAGE_VERSIONbump, no widget default pollution —tabOrderonly exists in a DSL after a builder sets it.useWidgetFocusopts out of auto layout, sotabOrderis currently enforced only by the fixed-layout custom tabbing (FlexComponentandlayoutSystems/anvil/*untouched).Security notes
sanitizeTabOrder).Tests
utils/widgetTabOrder.test.ts— sanitizer value rules (0 valid; null/blank/decimals/negatives/NaN/Infinity/non-numeric → Auto).utils/hooks/useWidgetFocus/tabbable.test.ts— all-Auto preserves position order; explicit overrides position; 0 before 1; explicit widget above/left can be next; duplicate tie-break; blank/invalid → Auto; Shift+Tab; nested container scope; modal scope; composite widgets keep native internal tabbing.WidgetProvider/factory/tabOrderPropertyConfig.test.ts— unit tests plus an integration test over all registered widgets proving standard non-Anvil widgets expose exactly one identical shared property and Anvil/internal widgets expose none.components/propertyControls/TabOrderControl.test.tsx— persistence semantics incl. regression that clearing returns to Auto without persisting an invalid value.components/designSystems/appsmith/PositionedContainer.test.tsx—data-tab-orderrenders only for valid explicit values (including0), never a nativetabindex.Fixes #37947
Automation
/ok-to-test tags="@tag.All"
🔍 Cypress test results
Tip
🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/29114763071
Commit: 94be603
Cypress dashboard.
Tags:
@tag.AllSpec:
Fri, 10 Jul 2026 20:01:01 UTC
Communication
Should the DevRel and Marketing teams inform users about this change?
🤖 Generated with Claude Code
Summary by CodeRabbit
tabOrdersupport for eligible widgets via a shared Accessibility property-pane section (clearable numeric input).