Skip to content

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

Open
sebastianiv21 wants to merge 17 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 17 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/29114763071
Commit: 94be603
Cypress dashboard.
Tags: @tag.All
Spec:


Fri, 10 Jul 2026 20:01:01 UTC

Communication

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

  • Yes
  • No

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added explicit tabOrder support for eligible widgets via a shared Accessibility property-pane section (clearable numeric input).
    • Introduced a tab-order overlay badge in the editor with an Alt+T hotkey toggle.
  • Bug Fixes
    • Improved keyboard navigation and reverse traversal to respect explicit/automatic tab order.
    • Prevented focus handling from getting stuck when no focusable target is available.
  • Tests
    • Expanded coverage for property-pane integration, tab-order rendering, overlay badge behavior, keyboard traversal, and numeric input interactions.

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

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

Changes

Explicit Widget Tab Order

Layer / File(s) Summary
Core sanitization and property control
app/client/src/utils/widgetTabOrder.*, app/client/src/components/propertyControls/*
Adds shared tab-order sanitization, a clearable numeric control that deletes the property on clear, and registry/test coverage.
Keyboard traversal ordering
app/client/src/utils/hooks/useWidgetFocus/tabbable.*, app/client/src/utils/hooks/useWidgetFocus/handleTab.ts
Reads explicit tab order from data-tab-order, prioritizes explicit ranks over automatic position order, and supports nested scopes, wrap-around, and Shift+Tab traversal.
Property pane injection
app/client/src/WidgetProvider/factory/*
Adds eligibility checks and an Accessibility section with the shared tab-order control, then wires it into widget property-pane generation.
DOM attribute rendering
app/client/src/components/designSystems/appsmith/*, app/client/src/layoutSystems/fixedlayout/common/PositionedComponentLayer.tsx
Forwards and sanitizes tabOrder, emitting data-tab-order only for valid explicit values.
Editor overlay
app/client/src/actions/editorActions.ts, app/client/src/ce/*, app/client/src/layoutSystems/common/tabOrderOverlay/*, app/client/src/pages/Editor/GlobalHotKeys/*
Adds Redux state, an Alt+T toggle, and a badge showing valid tab-order values while the overlay is enabled outside preview mode.

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
Loading

Suggested labels: Enhancement

Poem

A tab order charm now hums in code,
Through widgets, panes, and focus mode.
Clear values vanish, numbers stay,
And keyboard paths now know the way.
🐇✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The tab-order overlay badge, hotkey toggle, and Redux plumbing are extra UI features not required by #37947. Either remove these overlay-related changes or document them as part of the issue scope and implementation plan.
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #37947 by adding configurable tab order, wrap-around tabbing, and preserving existing behavior when unset.
Title check ✅ Passed The title accurately summarizes the main change: a shared tabOrder property for standard widgets and keyboard tab navigation.
Description check ✅ Passed The description matches the template well, with motivation, issue reference, automation, Cypress results, and communication section filled out.
✨ 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.

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

Copy link
Copy Markdown
Contributor

I've reviewed the code, The changes which I see is looking fine. I've two concerns:

  • I'm assuming that this property would be saved on git serialization, so if A discard happens, what is the automatic value being set?
  • Not sure if in case we would have missed something

Comment thread app/client/src/components/propertyControls/TabOrderControl.tsx Outdated
* @param currentWidget the widget we are tabbing from, when it is part of the scope
* @returns
*/
export function sortTabbableWidgets(

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.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true recreate=true

@github-actions

Copy link
Copy Markdown

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

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/client/src/components/propertyControls/ClearableNumericInputControl.test.tsx (1)

79-87: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add edge-case tests for null clear behavior and whitespace displayability.

Please add regression tests for:

  1. clearing when propertyValue is null should call deleteProperties, and
  2. canDisplayValueInUI(config, " ") should be false.

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

📥 Commits

Reviewing files that changed from the base of the PR and between db268e7 and 367dba6.

📒 Files selected for processing (5)
  • app/client/src/WidgetProvider/factory/helpers.tabOrder.test.ts
  • app/client/src/WidgetProvider/factory/helpers.ts
  • app/client/src/components/propertyControls/ClearableNumericInputControl.test.tsx
  • app/client/src/components/propertyControls/ClearableNumericInputControl.tsx
  • app/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

Comment thread app/client/src/components/propertyControls/ClearableNumericInputControl.tsx Outdated
@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/27977705904.
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 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>
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

This PR has not seen activitiy for a while. It will be closed in 7 days unless further activity is detected.

@github-actions github-actions Bot added the Stale label Jul 1, 2026
sebastianiv21 and others added 6 commits July 6, 2026 17:07
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>
@sebastianiv21

Copy link
Copy Markdown
Contributor Author

/build-deploy-preview skip-tests=true

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

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

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

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

@sebastianiv21 sebastianiv21 requested a review from ashit-rath July 7, 2026 03:24
@sebastianiv21

Copy link
Copy Markdown
Contributor Author

I've reviewed the code, The changes which I see is looking fine. I've two concerns:

  • I'm assuming that this property would be saved on git serialization, so if A discard happens, what is the automatic value being set?
  • Not sure if in case we would have missed something

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

@github-actions github-actions Bot removed the Stale label Jul 7, 2026
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>
@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/29064818655.
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

…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>

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d5138fc and 0f1eeeb.

📒 Files selected for processing (9)
  • app/client/src/WidgetProvider/factory/helpers.tabOrder.test.ts
  • app/client/src/WidgetProvider/factory/helpers.ts
  • app/client/src/components/designSystems/appsmith/PositionedContainer.test.tsx
  • app/client/src/layoutSystems/common/tabOrderOverlay/TabOrderBadge.test.tsx
  • app/client/src/utils/hooks/useWidgetFocus/handleTab.ts
  • 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
🚧 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

Comment thread app/client/src/utils/hooks/useWidgetFocus/tabbable.ts
@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/29107858739.
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

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

4 participants