Skip to content

feat(client): shared tabOrder property for keyboard Tab navigation across standard widgets#41895

Open
sebastianiv21 wants to merge 6 commits into
releasefrom
claude/hopeful-khayyam-72b20e
Open

feat(client): shared tabOrder property for keyboard Tab navigation across standard widgets#41895
sebastianiv21 wants to merge 6 commits into
releasefrom
claude/hopeful-khayyam-72b20e

Conversation

@sebastianiv21

@sebastianiv21 sebastianiv21 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Description

TL;DR: Adds a single platform-level tabOrder property, 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 unless tabOrder is explicitly set.

What changed

  • Shared property pane plumbingWidgetFactory centrally appends one shared Accessibility > Tab order section to both property-pane paths (legacy getPropertyPaneConfig and content config) via WidgetProvider/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.
  • New TAB_ORDER_INPUT control (TabOrderControl) reusing the ADS NumberInput: 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 as ValidationTypes.NUMBER { min: 0, natural: true }.
  • RuntimePositionedContainer renders a sanitized data-tab-order attribute only for valid explicit values (0 is valid and earliest). No positive native tabIndex is ever used, so non-focusable widgets stay non-focusable.
  • Fixed-layout tabbing (useWidgetFocus/tabbable.ts) — new sortTabbableWidgets(): when no widget in the current scope has a valid explicit order, it delegates to the unchanged sortWidgetsByPosition (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

  • No DSL migration, no LATEST_PAGE_VERSION bump, no widget default pollution — tabOrder only exists in a DSL after a builder sets it.
  • Auto layout is intentionally unchanged: useWidgetFocus opts out of auto layout, so tabOrder is currently enforced only by the fixed-layout custom tabbing (FlexComponent and layoutSystems/anvil/* untouched).

Security notes

  • No JS/eval support for the field; numeric-only non-negative integer validation at the control, and re-sanitization at every runtime read (sanitizeTabOrder).
  • Only a sanitized integer is reflected into the DOM attribute; no user-controlled selectors or HTML; invalid values are ignored and treated as Auto.

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.tsxdata-tab-order renders only for valid explicit values (including 0), never a native tabindex.

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/27584120150
Commit: db268e7
Cypress dashboard.
Tags: @tag.All
Spec:


Tue, 16 Jun 2026 00:59:28 UTC

Communication

Should the DevRel and Marketing teams inform users about this change?

  • Yes
  • No

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a shared Tab order property pane section and a dedicated Tab Order input control for eligible widgets.
    • Widgets now support explicit tab ordering using non-negative integer values; cleared/invalid inputs revert to automatic behavior.
    • Keyboard navigation (Tab and Shift+Tab) now follows explicit tab order within the relevant scope.
  • Test Coverage

    • Added comprehensive automated tests covering tab order sanitization, property-pane behavior, and keyboard traversal ordering.

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>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds explicit tab order support for widgets. A new sanitizeTabOrder utility validates non-negative integer tab orders. PositionedContainer emits a data-tab-order attribute when a valid order is set. The tabbable navigation logic gains sortTabbableWidgets and getExplicitTabOrder to sort by explicit order first. A TabOrderControl property panel component and a shared "Accessibility" section are injected into eligible widget property pane configs, with Anvil/internal widgets excluded.

Changes

Explicit Widget Tab Order

Layer / File(s) Summary
Core sanitizeTabOrder utility
app/client/src/utils/widgetTabOrder.ts, app/client/src/utils/widgetTabOrder.test.ts
New module exports TAB_ORDER_ATTRIBUTE and sanitizeTabOrder, which returns a non-negative integer for valid explicit orders and undefined for all invalid/auto values. Test suite covers the full valid/invalid matrix.
Tabbable sort logic with explicit order
app/client/src/utils/hooks/useWidgetFocus/tabbable.ts, app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts
Adds getExplicitTabOrder, sortTabbableWidgets, and getTabOrderSequence; all four sortWidgetsByPosition call-sites are replaced. Test suite grows from one sanity test to comprehensive sibling, canvas, modal, nested-container, and Shift+Tab regression coverage.
TabOrderControl component and registration
app/client/src/components/propertyControls/TabOrderControl.tsx, app/client/src/components/propertyControls/TabOrderControl.test.tsx, app/client/src/components/propertyControls/index.ts
New TabOrderControl renders a NumberInput, sanitizes/deletes on change, and gates canDisplayValueInUI on a valid sanitized value. Registered in PropertyControls and PropertyControlPropsType. Tests cover all input/delete/clamp/placeholder paths.
Property pane tabOrder section injection
app/client/src/WidgetProvider/factory/helpers.ts, app/client/src/WidgetProvider/factory/index.tsx, app/client/src/WidgetProvider/factory/helpers.tabOrder.test.ts
helpers.ts adds eligibility check, fresh-section factory, and addTabOrderToPropertyPaneConfig; index.tsx wraps both property pane config paths before enhancement. Integration tests validate inclusion/exclusion per widget type and full factory output.
PositionedContainer data-tab-order rendering
app/client/src/components/designSystems/appsmith/PositionedContainer.tsx, app/client/src/components/designSystems/appsmith/PositionedContainer.test.tsx, app/client/src/layoutSystems/fixedlayout/common/PositionedComponentLayer.tsx
PositionedContainer gains tabOrder prop, sanitizes it, and emits data-tab-order only for valid values. PositionedComponentLayer forwards the prop. Tests assert correct attribute presence/absence and confirm no native tabindex is emitted.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TabOrderControl
  participant sanitizeTabOrder
  participant DSL as Widget DSL
  participant PositionedContainer
  participant sortTabbableWidgets

  rect rgba(59, 130, 246, 0.5)
    note over User, DSL: Property Panel: setting explicit tab order
    User->>TabOrderControl: types "3"
    TabOrderControl->>sanitizeTabOrder: "3"
    sanitizeTabOrder-->>TabOrderControl: 3
    TabOrderControl->>DSL: onPropertyChange("tabOrder", 3)
  end

  rect rgba(16, 185, 129, 0.5)
    note over DSL, PositionedContainer: Render: emitting data-tab-order
    DSL-->>PositionedContainer: tabOrder = 3
    PositionedContainer->>sanitizeTabOrder: 3
    sanitizeTabOrder-->>PositionedContainer: 3
    PositionedContainer-->>User: data-tab-order="3" on DOM element
  end

  rect rgba(245, 158, 11, 0.5)
    note over User, sortTabbableWidgets: Keyboard Tab navigation
    User->>sortTabbableWidgets: Tab keypress, tabbable widgets
    sortTabbableWidgets->>sanitizeTabOrder: reads data-tab-order per widget
    sanitizeTabOrder-->>sortTabbableWidgets: explicit order or undefined
    sortTabbableWidgets-->>User: next focused widget (explicit order first)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🎹 Tab tab tab, no longer lost in space,
Each widget now knows its designated place.
sanitizeTabOrder guards the gate,
Explicit integers sorted, never late.
From property pane to DOM it flows,
Forward or Shift+Tab — the sequence knows! 🔢

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature: a shared tabOrder property for keyboard Tab navigation in standard widgets.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering all required template sections including objectives, changes, compatibility notes, security considerations, and testing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/hopeful-khayyam-72b20e

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sebastianiv21

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@sebastianiv21 sebastianiv21 added the ok-to-test Required label for CI label Jun 12, 2026
@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27449717446.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41895.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

🔴🔴🔴 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.

@github-actions

Copy link
Copy Markdown

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>
@github-actions

Copy link
Copy Markdown

🔴🔴🔴 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.

@sebastianiv21

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27568604573.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41895.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

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>
@sebastianiv21

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

Copy link
Copy Markdown

Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27572072253.
Workflow: On demand build Docker image and deploy preview.
skip-tests: true.
env: ``.
PR: 41895.
recreate: .
base-image-tag: .

@github-actions

Copy link
Copy Markdown

Deploy-Preview-URL: https://ce-41895.dp.appsmith.com

sebastianiv21 and others added 2 commits June 15, 2026 16:50
- 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>
@sebastianiv21 sebastianiv21 marked this pull request as ready for review June 15, 2026 23:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts (1)

1-2: ⚡ Quick win

Use TAB_ORDER_ATTRIBUTE in test fixtures instead of hardcoded data-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

📥 Commits

Reviewing files that changed from the base of the PR and between 057cc61 and 794a460.

📒 Files selected for processing (13)
  • app/client/src/WidgetProvider/factory/helpers.tabOrder.test.ts
  • app/client/src/WidgetProvider/factory/helpers.ts
  • app/client/src/WidgetProvider/factory/index.tsx
  • app/client/src/components/designSystems/appsmith/PositionedContainer.test.tsx
  • app/client/src/components/designSystems/appsmith/PositionedContainer.tsx
  • app/client/src/components/propertyControls/TabOrderControl.test.tsx
  • app/client/src/components/propertyControls/TabOrderControl.tsx
  • app/client/src/components/propertyControls/index.ts
  • app/client/src/layoutSystems/fixedlayout/common/PositionedComponentLayer.tsx
  • app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts
  • app/client/src/utils/hooks/useWidgetFocus/tabbable.ts
  • app/client/src/utils/widgetTabOrder.test.ts
  • app/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>
@github-actions github-actions Bot added the Task A simple Todo label Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ok-to-test Required label for CI Task A simple Todo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configure the order of the inputs that are focused when the “Tab” key is pressed

2 participants