Skip to content

Add a shared direction utility to wonder-blocks-core - #3167

Draft
maddy531 wants to merge 1 commit into
mainfrom
WB-2060.is-rtl
Draft

Add a shared direction utility to wonder-blocks-core#3167
maddy531 wants to merge 1 commit into
mainfrom
WB-2060.is-rtl

Conversation

@maddy531

@maddy531 maddy531 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

WB-2060. Three packages each had their own way of asking "is this layout RTL?":

Where What it did
wonder-blocks-tabs tabs.tsx:281 !!element.closest("[dir=rtl]")
wonder-blocks-date-picker date-picker.tsx:273 closest("[dir]")?.getAttribute("dir") || "ltr"
wonder-blocks-modal use-direction-detection.ts a hook, never exported from the package, used by DrawerDialog only

This consolidates them into wonder-blocks-core:

getDirection(element?)  // "ltr" | "rtl"
isRtl(element?)         // boolean
useDirection(ref?)      // "ltr" | "rtl"
useIsRtl(ref?)          // boolean

Net −243 lines.

Two design decisions worth your attention

1. Both a function and a hook, because one caller can't use a hook. Tabs resolves direction inside a keyboard handler from event.currentTarget — hooks are illegal there. DrawerDialog and DatePicker resolve during render. So the primitive is a plain function and the hooks are thin wrappers over it. The hooks add no behaviour today; they exist because the ticket asked for a core hook, they mark the render-time contract, and they give us one place to add reactivity (e.g. a MutationObserver) later without touching call sites.

2. The DOM is the only source of truth — the direction option is gone. The old hook accepted an explicit direction intended for a RequestInfo-style value. Bea raised exactly this on the DrawerLauncher PR:

I'm wondering if the utility should also only rely on the DOM so that things can't get out of sync (for example, RequestInfo says something different from the dir attribute in the DOM).

She's right, and the evidence supports it: no caller ever passed that option — not in production, not in tests. Logical CSS properties resolve against the dir attribute, so the DOM wins visually regardless; a second source could only ever disagree. defaultDirection had no caller either, so both options are dropped. If a RequestInfo-driven case turns up later, the right fix is to set dir on the document, which this utility then reads.

Behaviour changes

Both are improvements, both are intentional, both are tested:

  • Tabs previously matched only an ancestor with literally dir=rtl, so RTL arrow-key navigation did not work when only document.documentElement carried dir="rtl" — which is the common real-world case, and what Wonder Blocks' own Storybook does. It works now.
  • DatePicker previously had no document-level fallback and assumed "ltr". It now resolves document-level direction too.

DrawerDialog is unchanged in behaviour — it called the hook with no ref (document-level), and useIsRtl() with no argument does the same.

One deliberately preserved subtlety: if an ancestor carries a dir attribute whose value we don't understand (e.g. dir="auto"), we honour that ancestor and return "ltr" rather than falling through to the document. That matches the old hook, and there's a test pinning it, since it's the case most likely to regress.

Also added an SSR guard (typeof document === "undefined") that the old hook lacked.

Test plan

  • pnpm run jestfull suite, no path filter: 236 suites pass, 0 fail
  • pnpm lint — clean
  • pnpm typecheck — clean

New tests: packages/wonder-blocks-core/src/util/__tests__/direction.test.ts and .../hooks/__tests__/use-direction.test.tsx port the 11 cases from the deleted modal test (minus the defaultDirection one) and add coverage for isRtl and the dir="auto" case. A new Tabs test covers keyboard nav when direction comes from document.documentElement.

Note on wonder-blocks-floating

Floating's rtlMirror middleware (on the unmerged feature/floating-ui branch) contains a fourth copy of this same closest("[dir='rtl']) check. I deliberately did not touch it, since that branch isn't on main yet. Worth adopting isRtl there as part of that line of work — flagging for @jandrade.

Not covered

The SSR guard isn't directly tested; jsdom always provides a document.

🤖 Generated with Claude Code

Three packages each had their own way of asking "is this layout RTL?":
an inline `closest("[dir=rtl]")` in Tabs, an inline `closest("[dir]")`
plus `getAttribute` in DatePicker, and a `useDirectionDetection` hook
that lived privately inside wonder-blocks-modal and was never exported.

Consolidate them into wonder-blocks-core:

  getDirection(element?)  -> "ltr" | "rtl"
  isRtl(element?)         -> boolean
  useDirection(ref?)      -> "ltr" | "rtl"
  useIsRtl(ref?)          -> boolean

Both a plain function and a hook are needed. Tabs resolves direction
inside a keyboard handler from `event.currentTarget`, where hooks can't
be called, while DrawerDialog and DatePicker resolve it during render.

The DOM is the only source of truth. The previous hook accepted an
explicit `direction` option intended for a `RequestInfo`-style value,
but no caller ever passed it, and a second source of truth can disagree
with the DOM while the DOM wins visually. Its `defaultDirection` option
had no caller either, so both options are dropped.

Tabs and DatePicker now also resolve document-level direction, which
their inline checks did not. That fixes RTL arrow-key navigation in Tabs
when only `document.documentElement` carries `dir="rtl"`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e028378

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 33 packages
Name Type
@khanacademy/wonder-blocks-date-picker Patch
@khanacademy/wonder-blocks-modal Patch
@khanacademy/wonder-blocks-core Minor
@khanacademy/wonder-blocks-tabs Patch
@khanacademy/wonder-blocks-dropdown Patch
@khanacademy/wonder-blocks-popover Patch
@khanacademy/wonder-blocks-tooltip Patch
@khanacademy/wonder-blocks-accordion Patch
@khanacademy/wonder-blocks-announcer Patch
@khanacademy/wonder-blocks-badge Patch
@khanacademy/wonder-blocks-banner Patch
@khanacademy/wonder-blocks-birthday-picker Patch
@khanacademy/wonder-blocks-breadcrumbs Patch
@khanacademy/wonder-blocks-button Patch
@khanacademy/wonder-blocks-card Patch
@khanacademy/wonder-blocks-cell Patch
@khanacademy/wonder-blocks-clickable Patch
@khanacademy/wonder-blocks-data Patch
@khanacademy/wonder-blocks-form Patch
@khanacademy/wonder-blocks-grid Patch
@khanacademy/wonder-blocks-icon-button Patch
@khanacademy/wonder-blocks-icon Patch
@khanacademy/wonder-blocks-labeled-field Patch
@khanacademy/wonder-blocks-layout Patch
@khanacademy/wonder-blocks-link Patch
@khanacademy/wonder-blocks-pill Patch
@khanacademy/wonder-blocks-progress-spinner Patch
@khanacademy/wonder-blocks-search-field Patch
@khanacademy/wonder-blocks-switch Patch
@khanacademy/wonder-blocks-testing Patch
@khanacademy/wonder-blocks-toolbar Patch
@khanacademy/wonder-blocks-typography Patch
eslint-plugin-wonder-blocks-demo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Size Change: +13 B (+0.01%)

Total Size: 131 kB

📦 View Changed
Filename Size Change
packages/wonder-blocks-core/dist/es/index.js 2.82 kB +215 B (+8.27%) 🔍
packages/wonder-blocks-date-picker/dist/es/index.js 8.03 kB -26 B (-0.32%)
packages/wonder-blocks-modal/dist/es/index.js 7.19 kB -169 B (-2.3%)
packages/wonder-blocks-tabs/dist/es/index.js 5.61 kB -7 B (-0.12%)
ℹ️ View Unchanged
Filename Size
packages/eslint-plugin-wonder-blocks/dist/es/index.js 7.11 kB
packages/wonder-blocks-accordion/dist/es/index.js 3.02 kB
packages/wonder-blocks-announcer/dist/es/index.js 2.43 kB
packages/wonder-blocks-badge/dist/es/index.js 2.03 kB
packages/wonder-blocks-banner/dist/es/index.js 2.01 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.93 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 798 B
packages/wonder-blocks-button/dist/es/index.js 4.28 kB
packages/wonder-blocks-card/dist/es/index.js 1.09 kB
packages/wonder-blocks-cell/dist/es/index.js 2.19 kB
packages/wonder-blocks-clickable/dist/es/index.js 2.61 kB
packages/wonder-blocks-data/dist/es/index.js 5.51 kB
packages/wonder-blocks-dropdown/dist/es/index.js 20.4 kB
packages/wonder-blocks-form/dist/es/index.js 6.39 kB
packages/wonder-blocks-grid/dist/es/index.js 1.25 kB
packages/wonder-blocks-icon-button/dist/es/index.js 4.06 kB
packages/wonder-blocks-icon/dist/es/index.js 1.89 kB
packages/wonder-blocks-labeled-field/dist/es/index.js 3.47 kB
packages/wonder-blocks-layout/dist/es/index.js 1.69 kB
packages/wonder-blocks-link/dist/es/index.js 1.54 kB
packages/wonder-blocks-pill/dist/es/index.js 1.32 kB
packages/wonder-blocks-popover/dist/es/index.js 4.41 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.49 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.12 kB
packages/wonder-blocks-styles/dist/es/index.js 464 B
packages/wonder-blocks-switch/dist/es/index.js 1.6 kB
packages/wonder-blocks-testing-core/dist/es/index.js 4.09 kB
packages/wonder-blocks-testing/dist/es/index.js 978 B
packages/wonder-blocks-theming/dist/es/index.js 384 B
packages/wonder-blocks-timing/dist/es/index.js 1.53 kB
packages/wonder-blocks-tokens/dist/es/index.js 6.48 kB
packages/wonder-blocks-toolbar/dist/es/index.js 906 B
packages/wonder-blocks-tooltip/dist/es/index.js 6.19 kB
packages/wonder-blocks-typography/dist/es/index.js 1.04 kB

compressed-size-action

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (6ba50fe) and published all packages with changesets to npm.

You can install the packages in frontend by running:

./dev/tools/deploy_wonder_blocks.js --tag="PR3167"

Packages can also be installed manually by running:

pnpm add @khanacademy/wonder-blocks-<package-name>@PR3167

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-vqoeexdavi.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 541
Tests with visual changes 0
Total stories 866
Inherited (not captured) snapshots [TurboSnap] 0
Tests on the build 541

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant