Skip to content

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

Open
sebastianiv21 wants to merge 8 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 8 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/28053399535
Commit: ab06f5f
Cypress dashboard.
Tags: @tag.All
Spec:


Tue, 23 Jun 2026 21:28:56 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 an explicit Tab Order property under Accessibility to configure keyboard navigation for eligible widgets.
    • Introduced Clearable Numeric Input to support numeric inputs that can be cleared to unset their values.
    • Keyboard traversal now prioritizes explicit tab order over visual position.
  • Tests

    • Added/expanded test coverage for tab-order exposure, property-pane behavior, keyboard traversal (including Shift+Tab), and tab-order sanitization.
    • Added tests for the Clearable Numeric Input control behavior.

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 end-to-end explicit widget tab ordering. A new widgetTabOrder.ts utility provides sanitizeTabOrder and TAB_ORDER_ATTRIBUTE. ClearableNumericInputControl is introduced as a property panel control, wired into the registry. helpers.ts injects an Accessibility section into eligible widgets' property pane configs. PositionedContainer emits data-tab-order, and tabbable traversal logic respects explicit ordering over visual position.

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 or undefined; test suite covers valid numbers, numeric strings, null/blank auto values, and all rejection cases.
ClearableNumericInputControl component and registry
app/client/src/components/propertyControls/ClearableNumericInputControl.tsx, app/client/src/components/propertyControls/ClearableNumericInputControl.test.tsx, app/client/src/components/propertyControls/index.ts
New ClearableNumericInputControl renders a NumberInput, calls deleteProperties on clear, ignores NaN, and implements canDisplayValueInUI; registered in PropertyControls and PropertyControlPropsType; tests cover persistence, clear/delete, clamping, placeholder, and canDisplayValueInUI.
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 sort call-sites in getTabbableDescendants and getNextTabbableDescendant switch from sortWidgetsByPosition to sortTabbableWidgets; test suite expands into comprehensive coverage including sibling, canvas, modal, nested, and Shift+Tab regression cases.
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 TAB_ORDER_PROPERTY_NAME, TAB_ORDER_SECTION_NAME, an Anvil/internal exclusion list, shouldExposeTabOrderProperty, createTabOrderPropertyPaneSection, and addTabOrderToPropertyPaneConfig; factory/index.tsx wraps both property pane config paths before existing enhancement; integration tests validate inclusion/exclusion across all registered widget types.
PositionedContainer data-tab-order rendering
app/client/src/components/designSystems/appsmith/PositionedContainer.tsx, app/client/src/layoutSystems/fixedlayout/common/PositionedComponentLayer.tsx, app/client/src/components/designSystems/appsmith/PositionedContainer.test.tsx
PositionedContainer gains an optional tabOrder prop sanitized via sanitizeTabOrder and emits data-tab-order only for valid values; PositionedComponentLayer forwards the prop; tests assert presence/absence across valid, null, blank, and invalid inputs and confirm no native tabindex is set.

Sequence Diagram

sequenceDiagram
  rect rgba(70, 130, 180, 0.5)
    Note over User,PositionedContainer: Property panel sets tabOrder
    User->>ClearableNumericInputControl: enters numeric value
    ClearableNumericInputControl->>WidgetDSL: updateProperty("tabOrder", parsedNumber)
    User->>ClearableNumericInputControl: clears value
    ClearableNumericInputControl->>WidgetDSL: deleteProperties(["tabOrder"])
  end
  rect rgba(60, 179, 113, 0.5)
    Note over PositionedComponentLayer,PositionedContainer: Render propagation
    PositionedComponentLayer->>PositionedContainer: tabOrder prop from DSL
    PositionedContainer->>sanitizeTabOrder: props.tabOrder
    sanitizeTabOrder-->>PositionedContainer: number | undefined
    PositionedContainer->>DOM: data-tab-order="N" (only if valid)
  end
  rect rgba(210, 105, 30, 0.5)
    Note over handleTab,sortTabbableWidgets: Keyboard navigation
    handleTab->>getTabbableDescendants: scope, shiftKey, currentWidget
    getTabbableDescendants->>sortTabbableWidgets: descendants
    sortTabbableWidgets->>getExplicitTabOrder: reads data-tab-order per element
    getExplicitTabOrder->>sanitizeTabOrder: attribute string
    sanitizeTabOrder-->>sortTabbableWidgets: number | undefined
    sortTabbableWidgets-->>getTabbableDescendants: explicit-first ordered list
    getTabbableDescendants-->>handleTab: next focus target
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🎹 Tab, tab, tab through fields with grace,
No more wandering all over the place.
A number in the panel, a data-tab-order born,
Explicit rank bestowed, position-sort forlorn.
Anvil widgets sit this dance right out,
While standard widgets shuffle roundabout. 🔢

🚥 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 accurately and specifically summarizes the primary change: introducing a shared tabOrder property for keyboard Tab navigation across standard widgets.
Linked Issues check ✅ Passed The PR fully addresses #37947 by implementing developer-controlled tabOrder for fixed-layout widgets, enabling explicit override of position-based Tab order with proper scope handling and Auto fallback.
Out of Scope Changes check ✅ Passed All changes directly support the tabOrder feature: property plumbing, control implementation, runtime rendering, tabbing logic, and comprehensive test coverage. No extraneous modifications detected.
Description check ✅ Passed PR description comprehensively covers changes, motivation, compatibility, security, and test coverage with clear structure and linked issue.

✏️ 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.

@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

* field always means "Auto"
* - never persists invalid placeholder strings
*/
class TabOrderControl extends BaseControl<TabOrderControlProps> {

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.

Ideally we try to make these controls generic so that it can be re-used and in this case it looks we are aren't doing anything specific so can we not call it TabOrderControl and call is something generic.

* @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?

…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 on lines +54 to +59
return (
value !== "" &&
value !== null &&
value !== undefined &&
!isNaN(Number(value))
);

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

canDisplayValueInUI and clear semantics disagree on whitespace input.

Lines 54-59 treat " " as numeric (Number(...)), while Line 65 treats trimmed whitespace as clear/unset. This can enable JS→UI toggle for values the control considers blank.

Suggested fix
   static canDisplayValueInUI(config: ControlData, value: any): boolean {
-    return (
-      value !== "" &&
-      value !== null &&
-      value !== undefined &&
-      !isNaN(Number(value))
-    );
+    if (value === null || value === undefined) return false;
+    const normalized = typeof value === "string" ? value.trim() : value;
+    if (normalized === "") return false;
+    return !isNaN(Number(normalized));
   }

Also applies to: 65-66

🤖 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.tsx`
around lines 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.

Comment on lines +65 to +69
if (value === undefined || value.trim() === "") {
// Clearing the field unsets the property instead of persisting a blank
if (propertyValue !== undefined && propertyValue !== null) {
this.deleteProperties([propertyName]);
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clearing does not remove an existing null value from DSL.

On Line 67, the null check prevents deleteProperties from running when propertyValue is null, so clearing can leave a stale key instead of unsetting it.

Suggested fix
-      if (propertyValue !== undefined && propertyValue !== null) {
+      if (propertyValue !== undefined) {
         this.deleteProperties([propertyName]);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (value === undefined || value.trim() === "") {
// Clearing the field unsets the property instead of persisting a blank
if (propertyValue !== undefined && propertyValue !== null) {
this.deleteProperties([propertyName]);
}
if (value === undefined || value.trim() === "") {
// Clearing the field unsets the property instead of persisting a blank
if (propertyValue !== undefined) {
this.deleteProperties([propertyName]);
}
🤖 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.tsx`
around lines 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.

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