Skip to content

[direction provider] Fix RTL component behavior#4840

Merged
atomiks merged 4 commits into
mui:masterfrom
atomiks:codex/fix-rtl-behavior
May 18, 2026
Merged

[direction provider] Fix RTL component behavior#4840
atomiks merged 4 commits into
mui:masterfrom
atomiks:codex/fix-rtl-behavior

Conversation

@atomiks
Copy link
Copy Markdown
Contributor

@atomiks atomiks commented May 18, 2026

Several direction-sensitive paths still treated left/right or wheel movement as LTR, so RTL keyboard and scroll interactions could stall or move the wrong way.

This also uncovered a bug with Scroll Area where the wheel listener was not registered depending on the render timing.

Root cause

Combobox, Scroll Area, Navigation Menu, and logical positioning coverage did not consistently pass or assert DirectionProvider state.

Changes

  • Pass RTL direction into Combobox grid navigation.
  • Mirror Combobox chip left/right keyboard behavior in RTL.
  • Clamp horizontal Scroll Area wheel movement to the RTL negative scrollLeft range.
  • Mirror vertical Navigation Menu trigger opening in RTL.
  • Add RTL coverage for Popover logical-side positioning and Direction Provider runtime/types.

@atomiks atomiks added type: bug It doesn't behave as expected. i18n Internationalization. The infrastructure used by localization. component: combobox Changes related to the combobox component. component: scroll area Changes related to the scroll area component. component: navigation menu Changes related to the navigation menu component. component: popover Changes related to the popover component. labels May 18, 2026 — with ChatGPT Codex Connector
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 18, 2026

commit: ac01d73

@code-infra-dashboard
Copy link
Copy Markdown

code-infra-dashboard Bot commented May 18, 2026

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+98B(+0.02%) 🔺+121B(+0.08%)

Details of bundle changes

Performance

Total duration: 1,318.74 ms -261.79 ms(-16.6%) | Renders: 50 (+0) | Paint: 2,001.25 ms -390.52 ms(-16.3%)

Test Duration Renders
Tabs mount (200 instances) 239.88 ms ▼-60.70 ms(-20.2%) 4 (+0)
Select mount (200 instances) 148.81 ms ▼-52.57 ms(-26.1%) 3 (+0)
Scroll Area mount (300 instances) 99.13 ms ▼-33.83 ms(-25.4%) 3 (+0)
Mixed surface mount (app-like density) 80.57 ms ▼-26.84 ms(-25.0%) 5 (+0)
Select open (500 options) 48.70 ms ▼-17.20 ms(-26.1%) 14 (+0)

7 tests within noise — details


Check out the code infra dashboard for more information about this PR.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 18, 2026

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit ac01d73
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a0b15f54750d50008fb4be4
😎 Deploy Preview https://deploy-preview-4840--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@atomiks atomiks force-pushed the codex/fix-rtl-behavior branch from 5b7b979 to 9747e92 Compare May 18, 2026 12:32
@atomiks atomiks changed the title [react] Fix RTL component behavior [all components] Fix RTL component behavior May 18, 2026
@atomiks atomiks changed the title [all components] Fix RTL component behavior [direction provider] Fix RTL component behavior May 18, 2026
@atomiks atomiks force-pushed the codex/fix-rtl-behavior branch from 9747e92 to 78ccb2a Compare May 18, 2026 12:43
@atomiks atomiks force-pushed the codex/fix-rtl-behavior branch from 78ccb2a to 0a9ab67 Compare May 18, 2026 12:56
@atomiks atomiks marked this pull request as ready for review May 18, 2026 12:58
@atomiks
Copy link
Copy Markdown
Contributor Author

atomiks commented May 18, 2026

Code Review (GPT-5.5)

Approve ✅ The RTL interaction fixes are clean, and the remaining Scroll Area wheel edge/clamp coverage gap is now addressed.

1. Bugs / Issues (None)

No branch-relevant actionable issues remain.

Root Cause & Patch Assessment

The PR now covers the direction-sensitive paths that caused the RTL regressions: Combobox chip/input arrow navigation, Combobox grid list navigation, vertical Navigation Menu open keys, Popover logical side callback data, and Scroll Area horizontal RTL wheel behavior.

Test Coverage Assessment

Claude's useful follow-up was the optional Scroll Area wheel edge/clamp coverage gap. That is now covered by additional ScrollArea.Scrollbar wheel tests for:

  • horizontal LTR clamping at both edges
  • horizontal RTL clamping at both edges
  • vertical clamping at both edges

Validated locally with:

  • pnpm test:jsdom ScrollAreaScrollbar --no-watch
  • pnpm test:chromium ScrollAreaScrollbar --no-watch
  • pnpm exec prettier --check packages/react/src/scroll-area/scrollbar/ScrollAreaScrollbar.test.tsx
  • pnpm exec eslint packages/react/src/scroll-area/scrollbar/ScrollAreaScrollbar.test.tsx
  • git diff --check

Copy link
Copy Markdown
Member

@flaviendelangle flaviendelangle left a comment

Choose a reason for hiding this comment

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

Only missing tests, feel free to skip those:


PR #4840 Review Summary — [direction provider] Fix RTL component behavior

PR: #4840
Author: atomiks
Branch: codex/fix-rtl-behaviormaster
Stats: +429 / -34 across 12 files

Critical Issues (3 found)

  • pr-test-analyzer: Missing LTR equivalent of the "registers after scrollbar becomes visible" test ScrollAreaScrollbar.test.tsx — the actual production bug fix only has RTL coverage; an LTR regression would slip through CI.
  • pr-test-analyzer: direction dep on wheel useEffect is untested ScrollAreaScrollbar.tsx:114 — no test verifies that toggling DirectionProvider direction re-attaches the listener with the new sign convention.
  • pr-test-analyzer: RTL grid navigation only covers horizontal axis ComboboxRoot.test.tsx:2563 — the LTR companion exercises ArrowDown/ArrowUp cross-row navigation, but the RTL test only asserts horizontal. Cross-orientation RTL handling in useListNavigation.ts is unprotected.

Important Issues (3 found)

  • pr-test-analyzer: Flaky-prone chromium test for "registers after scrollbar becomes visible" — relies on viewport.scrollLeft === -50 after a real layout pass without faking dims like the sibling test. Tighten with waitFor/toBeLessThan(0) or use Object.defineProperty like the others.
  • pr-test-analyzer: RTL clamping test misses the partial-movement case ScrollAreaScrollbar.test.tsx — all RTL assertions hit exact edges (0 or -800). Add e.g. scrollLeft = -100, deltaX: 50 → expect -50 to actually exercise the new Math.min/Math.max arithmetic in RTL.
  • comment-analyzer: Missing "why" comment at ScrollAreaScrollbar.tsx:99-100 — the minScroll = -maxScroll / maxScrollValue = 0 swap encodes the non-obvious "RTL browsers report negative scrollLeft" convention. The pre-existing onPointerDown block at line 175 already has a comment about this; the new wheel handler is now the inconsistent one.

Suggestions (8 found)

  • pr-test-analyzer: NavigationMenuTrigger RTL test should also verify horizontal RTL still opens with ArrowDown and that ArrowRight no longer opens it in vertical RTL NavigationMenuTrigger.test.tsx.
  • pr-test-analyzer: ComboboxChip RTL test should assert ArrowRight at non-zero selectionStart is a no-op (guards against the selectionStart === 0 check being dropped in the RTL branch).
  • pr-test-analyzer: Wrap trigger.focus() in await act(...) in the NavigationMenu RTL test to match the sibling pattern at line 119.
  • pr-test-analyzer / code-reviewer: Heavy duplication — the "allows horizontal scrolling away from the RTL start edge" test inlines ~40 lines instead of calling renderWheelTest({ direction: 'rtl' }).
  • code-reviewer: Use TextDirection type from @base-ui/react/direction-provider for renderWheelTest's direction param instead of inline 'ltr' | 'rtl'.
  • pr-test-analyzer: Add a nested-provider override test to DirectionProvider.test.tsx (inner provider overrides outer).
  • pr-test-analyzer: Add an inline-end symmetric assertion to the popover RTL test.
  • silent-failure-hunter: Asymmetric RTL convention detection — onPointerDown at ScrollAreaScrollbar.tsx:175 detects the negative-scrollLeft convention at runtime, while the new wheel handler assumes it unconditionally. Consider sharing a helper (low priority; modern Blink follows the negative convention).

Strengths

  • No critical correctness issues, no silent failures, no removed error paths. The math refactor (Math.min/Math.max clamping) is actually more robust than the prior strict-equality edge checks (sub-pixel tolerant).
  • Bug fix is well-implemented: hoisting shouldRender + adding it to the useEffect deps correctly fixes the missing-listener-after-deferred-measurement bug. React commits refs before effects run, so there's no listener-missing window.
  • useDirection() contract is sound — defaults to 'ltr', never undefined. The new test pins this. No direction === 'rtl' site can silently coerce to the wrong default.
  • ArrowDown is correctly NOT mirrored in NavigationMenuTrigger's horizontal-menubar path and ScrollArea's vertical wheel — direction only affects horizontal axis.
  • rtl: direction === 'rtl' in AriaCombobox correctly uses the documented public option on useListNavigation at useListNavigation.ts:187.
  • Test patterns follow project conventions: it.skipIf(isJSDOM) for layout-dependent tests, Object.defineProperty for fake dims (matches existing pattern in the same file), DirectionProvider.spec.tsx uses expectType + @ts-expect-error.
  • The variable naming (previousChipKey, nextChipKey, verticalOpenKey) is self-documenting — comment-analyzer flagged no junk comments and only one missing "why" (in the wheel handler).

ARIA spec check — ArrowRightArrowLeft mirror in vertical RTL menu

The WAI-ARIA APG Menu/Menubar pattern describes keyboard interactions assuming LTR only and contains zero references to RTL — an acknowledged documentation gap (the APG explicitly calls out RTL for sliders, spin buttons, and toolbars).

The de-facto convention across jQuery UI, Angular CDK, React Aria, and Radix UI is to mirror horizontal arrow keys for menus in RTL. The PR's change at NavigationMenuTrigger.tsx:773 matches that convention. ✅

Recommended Action

  1. Critical first: add LTR coverage of the listener re-registration bug fix (C1), add a direction-switch wheel test (C2), and extend the RTL grid test with vertical/cross-row assertions (C3).
  2. Then important: tighten the flaky chromium test (I1), add the partial-movement RTL clamping assertion (I3), and add the one-line "negative scrollLeft in RTL" comment in the wheel handler.
  3. Optional polish: dedupe renderWheelTest, add await act around trigger.focus(), add nested-provider test.
  4. No blockers for merge from a correctness standpoint — all three code-focused agents (code-reviewer, silent-failure-hunter, comment-analyzer) found zero critical issues. All Critical items are test-coverage gaps, not code defects.

@atomiks atomiks merged commit ad99b44 into mui:master May 18, 2026
23 checks passed
@atomiks atomiks deleted the codex/fix-rtl-behavior branch May 18, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: combobox Changes related to the combobox component. component: navigation menu Changes related to the navigation menu component. component: popover Changes related to the popover component. component: scroll area Changes related to the scroll area component. i18n Internationalization. The infrastructure used by localization. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants