feat(client): shared tabOrder property for keyboard Tab navigation across standard widgets#41895
feat(client): shared tabOrder property for keyboard Tab navigation across standard widgets#41895sebastianiv21 wants to merge 8 commits into
Conversation
Adds a single platform-level "tabOrder" property exposed in a shared Accessibility property-pane section for all standard non-Anvil widgets (injected centrally in WidgetFactory, no per-widget config changes). - New TAB_ORDER_INPUT control persists only valid non-negative integers as numbers and removes the property from the DSL when cleared, so blank always means Auto and no placeholder strings are persisted - PositionedContainer renders a sanitized data-tab-order attribute only for valid explicit values (0 is valid); no native tabIndex is used - Fixed-layout tabbing (useWidgetFocus) sorts widgets with explicit tabOrder first (ascending, duplicates tie-break by position), then Auto/invalid widgets in the existing position order; Shift+Tab reverses the same sequence. When no widget in the scope has a valid tabOrder, the previous position-based behavior is preserved exactly - Anvil-only (WDS_*, SECTION/ZONE) and internal widgets (CANVAS, SKELETON, TABS_MIGRATOR) are excluded; auto layout is intentionally unchanged since useWidgetFocus opts out of it - No DSL migration and no page version bump; existing apps keep their current Tab behavior unless tabOrder is explicitly set Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkthroughAdds end-to-end explicit widget tab ordering. A new ChangesExplicit Widget Tab Order
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27449717446. |
|
🔴🔴🔴 Cyclic Dependency Check: This PR has increased the number of cyclic dependencies by 1, when compared with the release branch. Refer this document to identify the cyclic dependencies introduced by this PR. You can view the dependency diff in the run log. Look for the check-cyclic-dependencies job in the run. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
Follow-up to the shared tabOrder feature (016eec8) after a "doesn't work" report. Investigation showed the feature is actually functional; the real gaps were clarity and missing end-to-end coverage. Investigation (verified live via Playwright on a deployed app): - Widget-level tab order WORKS at runtime for focusable widgets on the same canvas. A 3-widget test (orders top=1, mid=3, bottom=2) produced the keyboard focus cycle top -> bottom -> mid (ascending by tabOrder, not visual position), and data-tab-order propagated correctly to the deployed DOM. - The original "doesn't work" was a non-discriminating test: a 2-widget cycle ping-pongs identically whether ordered by position or tabOrder, so it cannot tell the two apart. Three widgets are required to distinguish them. - The genuine limitation is that fields INSIDE a form (JSON Form) cannot be individually ordered, because form fields are not standalone widgets and the control is injected per widget. This is by design: per-field tab numbering is the positive-tabindex accessibility anti-pattern. Within a form, native DOM order is used. Changes: - tabOrderPropertyConfig.ts: expand the help text to disclose that ordering is relative to widgets in the same container, and that fields inside a form follow the form's own order. Strings kept inline to match property-pane convention (sectionName/helpText are inline literals across the codebase). - tabbable.test.ts: add a discriminating 3-widget regression test asserting the tab sequence follows explicit order rather than position (forward and Shift+Tab). Asserts at the getTabbableDescendants level to avoid the pre-existing jsdom FOCUS_SELECTOR limitation that breaks handleTab-based tests. No feature flag added: the change is additive and inert by default (when no widget sets tabOrder, traversal falls back to the existing position-based path unchanged), so existing applications are unaffected. Verification: new test passes (suite 23 passed / 3 pre-existing jsdom failures); ESLint clean on changed files; check-types passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
🔴🔴🔴 Cyclic Dependency Check: This PR has increased the number of cyclic dependencies by 1, when compared with the release branch. Refer this document to identify the cyclic dependencies introduced by this PR. You can view the dependency diff in the run log. Look for the check-cyclic-dependencies job in the run. |
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27568604573. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
The standalone WidgetProvider/factory/tabOrderPropertyConfig.ts imported constants/PropertyControlConstants and was imported by the WidgetFactory, closing a new import cycle that the ci-client-cyclic-deps-check (dpdm) gate flagged as +1 over the base branch. Move the shared tabOrder property-pane helpers into the existing WidgetProvider/factory/helpers.ts, which already imports every module they need (PropertyControlConstants, WidgetValidation, ./types, widgets/wds/constants) and is already part of these factory cycles. This adds no new import edge, so the dpdm circular-dependency count returns to the base 2134 with zero tabOrder-related cycles. - Delete tabOrderPropertyConfig.ts; functions/constants now live in helpers.ts - factory/index.tsx imports addTabOrderToPropertyPaneConfig from ./helpers - Rename the config test to helpers.tabOrder.test.ts, importing from ./helpers No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27572072253. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
- helpers.tabOrder.test.ts: assert the current help-text wording - PositionedContainer.test.tsx: add connect() to the react-redux mock so redux-form (pulled in via reflow selectors) loads - tabbable.test.ts: drop the two composite-widget handleTab tests; they hit querySelectorAll(FOCUS_SELECTOR) whose :is(...) syntax jsdom cannot parse, and exercise unchanged legacy routing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the canvas native module available, the full suite runs and exposed
three jsdom/test-harness issues (not feature bugs):
- PositionedContainer.test.tsx: render the forwardRef default export instead
of the raw named function, which received frozen legacy context as its ref
arg ("object is not extensible")
- helpers.tabOrder.test.ts: stop calling getWidgetPropertyPaneConfig for every
registered widget with empty props (some widgets' dynamic-property
generators throw on {}). Prove blanket coverage via the pure
shouldExposeTabOrderProperty classifier across all types, and keep
representative end-to-end checks on static-pane widgets
- tabbable.test.ts: drop the handleTab describe; handleTab ends in
node.matches(FOCUS_SELECTOR), whose :is(...[tabindex='-1']) selector is
unparseable by jsdom/nwsapi. The ordering logic is fully covered via
getTabbableDescendants; handleTab is unchanged legacy plumbing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts (1)
1-2: ⚡ Quick winUse
TAB_ORDER_ATTRIBUTEin test fixtures instead of hardcodeddata-tab-order.This keeps test setup aligned with the shared contract and avoids silent drift if the attribute name changes.
Proposed refactor
import { getExplicitTabOrder, getNextTabbableDescendant, getTabbableDescendants, sortTabbableWidgets, sortWidgetsByPosition, } from "./tabbable"; +import { TAB_ORDER_ATTRIBUTE } from "utils/widgetTabOrder"; @@ if (tabOrder !== undefined) { - widget.setAttribute("data-tab-order", tabOrder); + widget.setAttribute(TAB_ORDER_ATTRIBUTE, tabOrder); } @@ - widget.setAttribute("data-tab-order", "0"); + widget.setAttribute(TAB_ORDER_ATTRIBUTE, "0"); @@ - widget.setAttribute("data-tab-order", "3"); + widget.setAttribute(TAB_ORDER_ATTRIBUTE, "3"); @@ - widget.setAttribute("data-tab-order", value); + widget.setAttribute(TAB_ORDER_ATTRIBUTE, value);Also applies to: 65-67, 98-103, 110-113
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts` around lines 1 - 2, Replace all hardcoded `data-tab-order` attribute strings in test fixtures with the `TAB_ORDER_ATTRIBUTE` constant to keep test setup aligned with the shared contract. First, ensure `TAB_ORDER_ATTRIBUTE` is imported at the top of the file in app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts alongside `getExplicitTabOrder`. Then, at lines 65-67, 98-103, and 110-113 in the same file, replace each occurrence of the string `data-tab-order` with `TAB_ORDER_ATTRIBUTE` in test fixture HTML setup. This ensures the test will automatically reflect any future changes to the actual attribute name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts`:
- Around line 1-2: Replace all hardcoded `data-tab-order` attribute strings in
test fixtures with the `TAB_ORDER_ATTRIBUTE` constant to keep test setup aligned
with the shared contract. First, ensure `TAB_ORDER_ATTRIBUTE` is imported at the
top of the file in app/client/src/utils/hooks/useWidgetFocus/tabbable.test.ts
alongside `getExplicitTabOrder`. Then, at lines 65-67, 98-103, and 110-113 in
the same file, replace each occurrence of the string `data-tab-order` with
`TAB_ORDER_ATTRIBUTE` in test fixture HTML setup. This ensures the test will
automatically reflect any future changes to the actual attribute name.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a6369dec-3eb0-4870-8f15-440167711e31
📒 Files selected for processing (13)
app/client/src/WidgetProvider/factory/helpers.tabOrder.test.tsapp/client/src/WidgetProvider/factory/helpers.tsapp/client/src/WidgetProvider/factory/index.tsxapp/client/src/components/designSystems/appsmith/PositionedContainer.test.tsxapp/client/src/components/designSystems/appsmith/PositionedContainer.tsxapp/client/src/components/propertyControls/TabOrderControl.test.tsxapp/client/src/components/propertyControls/TabOrderControl.tsxapp/client/src/components/propertyControls/index.tsapp/client/src/layoutSystems/fixedlayout/common/PositionedComponentLayer.tsxapp/client/src/utils/hooks/useWidgetFocus/tabbable.test.tsapp/client/src/utils/hooks/useWidgetFocus/tabbable.tsapp/client/src/utils/widgetTabOrder.test.tsapp/client/src/utils/widgetTabOrder.ts
Replace hardcoded "data-tab-order" strings in the test fixtures with the shared TAB_ORDER_ATTRIBUTE constant so the setup tracks the real attribute name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I've reviewed the code, The changes which I see is looking fine. I've two concerns:
|
| * field always means "Auto" | ||
| * - never persists invalid placeholder strings | ||
| */ | ||
| class TabOrderControl extends BaseControl<TabOrderControlProps> { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
I don't get why this is needed; we are already passing tabOrder in the PositionedComponentLayer.tsx which helps the tabbing by default. What is re-sorting based on tabOrder helping us with?
…Control PR review: the control did nothing tabOrder-specific, so make it generic and reusable. Rename TabOrderControl -> ClearableNumericInputControl (controlType CLEARABLE_NUMERIC_INPUT), decouple it from the tabOrder sanitizer: clearing the field unsets the property (delete from DSL); otherwise the numeric value is persisted, with range/format validation left to the property's own validation config. The shared Accessibility section now uses the generic control and passes min: 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
/build-deploy-preview skip-tests=true recreate=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27976115494. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
app/client/src/components/propertyControls/ClearableNumericInputControl.test.tsx (1)
79-87: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winAdd edge-case tests for
nullclear behavior and whitespace displayability.Please add regression tests for:
- clearing when
propertyValueisnullshould calldeleteProperties, andcanDisplayValueInUI(config, " ")should befalse.Also applies to: 134-149
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/client/src/components/propertyControls/ClearableNumericInputControl.test.tsx` around lines 79 - 87, Add two new regression test cases to the test suite in ClearableNumericInputControl.test.tsx. First, create a test that verifies when propertyValue is null and the input field is cleared, the deleteProperties function should be called (unlike the existing test which checks the already-unset field scenario). Second, add a test that verifies the canDisplayValueInUI function returns false when passed whitespace-only strings like " ". Both tests should follow the same pattern as the existing test that uses renderControl(), fireEvent.change(), and expect() assertions to verify the expected behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/client/src/components/propertyControls/ClearableNumericInputControl.tsx`:
- Around line 54-59: The canDisplayValueInUI function (lines 54-59) and the
clear semantics (lines 65-66) are inconsistent in how they handle whitespace
strings. The function currently treats whitespace-only strings like " " as
valid numeric values since Number(" ") returns 0, but the clear logic treats
trimmed whitespace as blank/unset. Fix this by adding a trim check in the
canDisplayValueInUI function to ensure that whitespace-only strings are treated
as invalid/non-numeric, making the behavior consistent with the clearing
semantics. Add a condition to check that value.trim() is not empty before
considering it a valid numeric input.
- Around line 65-69: The null check in the deleteProperties condition within the
value clearing logic prevents the property from being deleted when propertyValue
is null, leaving stale null values in the DSL. In the if statement checking
propertyValue, remove the null check (the && propertyValue !== null part) so
that deleteProperties is called whenever propertyValue is not undefined,
allowing both undefined and null values to be properly removed from the DSL when
the field is cleared.
---
Nitpick comments:
In
`@app/client/src/components/propertyControls/ClearableNumericInputControl.test.tsx`:
- Around line 79-87: Add two new regression test cases to the test suite in
ClearableNumericInputControl.test.tsx. First, create a test that verifies when
propertyValue is null and the input field is cleared, the deleteProperties
function should be called (unlike the existing test which checks the
already-unset field scenario). Second, add a test that verifies the
canDisplayValueInUI function returns false when passed whitespace-only strings
like " ". Both tests should follow the same pattern as the existing test that
uses renderControl(), fireEvent.change(), and expect() assertions to verify the
expected behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7eefad29-7b53-46fc-a330-9a2a4bdb187b
📒 Files selected for processing (5)
app/client/src/WidgetProvider/factory/helpers.tabOrder.test.tsapp/client/src/WidgetProvider/factory/helpers.tsapp/client/src/components/propertyControls/ClearableNumericInputControl.test.tsxapp/client/src/components/propertyControls/ClearableNumericInputControl.tsxapp/client/src/components/propertyControls/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- app/client/src/WidgetProvider/factory/helpers.tabOrder.test.ts
- app/client/src/WidgetProvider/factory/helpers.ts
| return ( | ||
| value !== "" && | ||
| value !== null && | ||
| value !== undefined && | ||
| !isNaN(Number(value)) | ||
| ); |
There was a problem hiding this comment.
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.
| if (value === undefined || value.trim() === "") { | ||
| // Clearing the field unsets the property instead of persisting a blank | ||
| if (propertyValue !== undefined && propertyValue !== null) { | ||
| this.deleteProperties([propertyName]); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
|
/build-deploy-preview skip-tests=true |
|
Deploying Your Preview: https://github.com/appsmithorg/appsmith/actions/runs/27977705904. |
|
Deploy-Preview-URL: https://ce-41895.dp.appsmith.com |
The shared "Tab order" property was exposed on every standard widget, including display-only ones with no focusable element (Chart, Document Viewer, Image, Divider, Progress, Statbox, Map Chart, Icon, Text, Rate). The custom Tab handler already skips these at runtime, so the field was a confusing no-op there. Narrow shouldExposeTabOrderProperty with a curated non-focusable denylist so the field only appears on widgets that can actually receive focus. Audio, Video, Map, Iframe and all input/container/list widgets stay eligible. Duplicate tab-order values are unchanged (tie-break by position, by design). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Description
TL;DR: Adds a single platform-level
tabOrderproperty, exposed in a shared Accessibility property-pane section for every standard (non-Anvil) widget, letting builders optionally control keyboard Tab order in fixed layout. Existing apps keep their exact current Tab behavior unlesstabOrderis explicitly set.What changed
WidgetFactorycentrally appends one sharedAccessibility > Tab ordersection to both property-pane paths (legacygetPropertyPaneConfigand content config) viaWidgetProvider/factory/tabOrderPropertyConfig.ts. No per-widget config edits, no per-widget property names or validation rules. Anvil-only (WDS_*,SECTION_WIDGET,ZONE_WIDGET) and internal widgets (CANVAS_WIDGET,SKELETON_WIDGET,TABS_MIGRATOR_WIDGET) are excluded; widgets without a property pane stay untouched.TAB_ORDER_INPUTcontrol (TabOrderControl) reusing the ADSNumberInput: persists only valid non-negative integers as numbers, removes the property from the DSL when cleared (blank always means Auto), never persists""or invalid input. Not JS-convertible (isJSConvertible/isBindProperty/isTriggerProperty: false), validated asValidationTypes.NUMBER { min: 0, natural: true }.PositionedContainerrenders a sanitizeddata-tab-orderattribute only for valid explicit values (0is valid and earliest). No positive nativetabIndexis ever used, so non-focusable widgets stay non-focusable.useWidgetFocus/tabbable.ts) — newsortTabbableWidgets(): when no widget in the current scope has a valid explicit order, it delegates to the unchangedsortWidgetsByPosition(exact current behavior). Otherwise, explicitly ordered widgets come first ascending (duplicates tie-break by position), Auto/invalid widgets follow in existing position order, and Shift+Tab reverses the same sequence. Modal, container, JSONForm, CheckboxGroup, SwitchGroup and ButtonGroup behaviors are preserved.Compatibility
LATEST_PAGE_VERSIONbump, no widget default pollution —tabOrderonly exists in a DSL after a builder sets it.useWidgetFocusopts out of auto layout, sotabOrderis currently enforced only by the fixed-layout custom tabbing (FlexComponentandlayoutSystems/anvil/*untouched).Security notes
sanitizeTabOrder).Tests
utils/widgetTabOrder.test.ts— sanitizer value rules (0 valid; null/blank/decimals/negatives/NaN/Infinity/non-numeric → Auto).utils/hooks/useWidgetFocus/tabbable.test.ts— all-Auto preserves position order; explicit overrides position; 0 before 1; explicit widget above/left can be next; duplicate tie-break; blank/invalid → Auto; Shift+Tab; nested container scope; modal scope; composite widgets keep native internal tabbing.WidgetProvider/factory/tabOrderPropertyConfig.test.ts— unit tests plus an integration test over all registered widgets proving standard non-Anvil widgets expose exactly one identical shared property and Anvil/internal widgets expose none.components/propertyControls/TabOrderControl.test.tsx— persistence semantics incl. regression that clearing returns to Auto without persisting an invalid value.components/designSystems/appsmith/PositionedContainer.test.tsx—data-tab-orderrenders only for valid explicit values (including0), never a nativetabindex.Fixes #37947
Automation
/ok-to-test tags="@tag.All"
🔍 Cypress test results
Tip
🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/28053399535
Commit: ab06f5f
Cypress dashboard.
Tags:
@tag.AllSpec:
Tue, 23 Jun 2026 21:28:56 UTC
Communication
Should the DevRel and Marketing teams inform users about this change?
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests