Skip to content

[CLASS-13991] Auto-mirror directional PhosphorIcons in RTL - #3174

Open
maddy531 wants to merge 10 commits into
mainfrom
CLASS-13991.rtl-directional-icons
Open

[CLASS-13991] Auto-mirror directional PhosphorIcons in RTL#3174
maddy531 wants to merge 10 commits into
mainfrom
CLASS-13991.rtl-directional-icons

Conversation

@maddy531

@maddy531 maddy531 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

PhosphorIcon now auto-mirrors icons whose glyph encodes a reading direction in RTL, driven by a central allowlist.
mirrorInRtl overrides the allowlist per call site in both directions:

  • true mirrors a custom directional SVG, false opts an allowlisted icon out.
  • false is for absolute directions — e.g. perseus's graph-editor arrows, where "left" means decreasing x in every locale.

Issue: CLASS-13991
Follow-up: CLASS-14877 (same support in the Icon component)

Breaking change

Released as major. Any consumer doing isRTL ? caretLeft : caretRight will double-flip in RTL after upgrading, and must pass the LTR-facing icon instead (or mirrorInRtl={false}).

Rollout

  1. wonder-blocks — this PR. Publish as major.
  2. perseus — bump wonder-blocks-icon, drop the 2 scrollable-view.tsx swaps, and opt the graph editor's x-axis arrows out with mirrorInRtl={false}. Publish.
  1. frontend — one PR: bump @khanacademy/perseus and @khanacademy/wonder-blocks-icon together (both are exact-pinned in the same pnpm-workspace.yaml catalog), and remove the 11 caret/arrow swaps.
  • Khan/frontend#15137

Test plan

  • pnpm test
  • pnpm lint
  • pnpm typecheck
  • Chromatic — ExpectedToMirror / ExpectedNotToMirror cover the allowlist
    in LTR and RTL panels; CustomIconMirrorInRtl covers the override

@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 976777e

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

This PR includes changesets to release 20 packages
Name Type
@khanacademy/wonder-blocks-icon Major
@khanacademy/wonder-blocks-accordion Patch
@khanacademy/wonder-blocks-badge Patch
@khanacademy/wonder-blocks-banner Patch
@khanacademy/wonder-blocks-birthday-picker Patch
@khanacademy/wonder-blocks-button Patch
@khanacademy/wonder-blocks-date-picker Patch
@khanacademy/wonder-blocks-dropdown Patch
@khanacademy/wonder-blocks-form Patch
@khanacademy/wonder-blocks-icon-button Patch
@khanacademy/wonder-blocks-link Patch
@khanacademy/wonder-blocks-search-field Patch
@khanacademy/wonder-blocks-switch Patch
@khanacademy/wonder-blocks-tabs Patch
eslint-plugin-wonder-blocks-demo Patch
@khanacademy/wonder-blocks-card Patch
@khanacademy/wonder-blocks-modal Patch
@khanacademy/wonder-blocks-popover Patch
@khanacademy/wonder-blocks-pill Patch
@khanacademy/wonder-blocks-tooltip 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 11, 2026

Copy link
Copy Markdown
Contributor

Size Change: +1.54 kB (+1.17%)

Total Size: 133 kB

📦 View Changed
Filename Size Change
packages/wonder-blocks-icon/dist/es/index.js 2.68 kB +788 B (+41.63%) 🚨
packages/wonder-blocks-labeled-field/dist/es/index.js 4.22 kB +752 B (+21.68%) 🚨
ℹ️ 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-core/dist/es/index.js 2.6 kB
packages/wonder-blocks-data/dist/es/index.js 5.51 kB
packages/wonder-blocks-date-picker/dist/es/index.js 8.06 kB
packages/wonder-blocks-dropdown/dist/es/index.js 20.6 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-layout/dist/es/index.js 1.69 kB
packages/wonder-blocks-link/dist/es/index.js 1.54 kB
packages/wonder-blocks-modal/dist/es/index.js 7.36 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-tabs/dist/es/index.js 5.62 kB
packages/wonder-blocks-testing-core/dist/es/index.js 4.12 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.5 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 11, 2026

Copy link
Copy Markdown
Contributor

npm Snapshot: Published

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

You can install the packages in frontend by running:

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

Packages can also be installed manually by running:

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

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

A new build was pushed to Chromatic! 🚀

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

Chromatic results:

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

@maddy531
maddy531 force-pushed the CLASS-13991.rtl-directional-icons branch 5 times, most recently from 1d78b30 to b85ba7e Compare August 11, 2026 17:19
@maddy531
maddy531 force-pushed the CLASS-13991.rtl-directional-icons branch from d16c894 to f124ade Compare August 11, 2026 18:51
@maddy531
maddy531 marked this pull request as ready for review August 11, 2026 20:44
@khan-actions-bot
khan-actions-bot requested a review from a team August 11, 2026 20:44
@maddy531
maddy531 requested review from beaesguerra and removed request for a team August 11, 2026 20:44

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.

I added a custom icon for testing purposes/for storybook.

@beaesguerra

Copy link
Copy Markdown
Member

@claude review once

@beaesguerra beaesguerra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for working on this @maddy531! This is a great idea, and will improve our rtl support!

I left some comments in the PR - The implementation overall looks good to me, though I wanted to discuss more how this will be rolled out since there are considerations for breaking changes!

Comment thread .changeset/phosphor-icon-rtl-mirroring.md Outdated
Comment thread __docs__/wonder-blocks-icon/accessibility.mdx Outdated
Comment thread __docs__/wonder-blocks-icon/phosphor-icon-rtl.mdx
Comment thread __docs__/wonder-blocks-icon/phosphor-icon-rtl.stories.tsx
Comment thread __docs__/wonder-blocks-icon/phosphor-icon.argtypes.ts Outdated
Comment thread packages/wonder-blocks-icon/src/components/__tests__/phosphor-icon.test.tsx Outdated
Comment thread packages/wonder-blocks-icon/src/components/phosphor-icon.tsx Outdated
* whitelist (e.g. a custom directional SVG). Defaults to whitelist
* behavior when omitted. Does not disable mirroring for whitelisted icons.
*/
mirrorInRtl?: boolean;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Idea for later: It would be great to support the mirrorInRtl prop in the Icon component too (which is used for custom icons that are multi-colored)!

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.

Created a ticket for this idea!

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Beyond the two inline nits, I also checked whether shouldMirrorIconInRtl fails safe when a bundler inlines a small SVG as a data: URI (some production configs do this for small assets) — it returns false in that case rather than throwing or matching incorrectly, so the icon simply isn't mirrored rather than breaking. Not a bug, just a note on what was checked.

Extended reasoning...

This is a brief informational note rather than a full review, per the ruled-out-issue exception: bugs were already found and posted as inline comments (test convention nit, changeset minor-vs-major nit), so a full approve/defer verdict isn't warranted here. I additionally verified the data-URI inlining edge case in directional-icons.ts — getPhosphorIconName explicitly returns undefined for any string starting with data:, and shouldMirrorIconInRtl treats that as false, so if a bundler ever inlines a whitelisted icon as a data URI, the icon silently renders unmirrored rather than crashing or matching the wrong icon. That's a reasonable fail-safe default given there's no way to recover a name from a data URI, so I did not flag it as a bug.

Comment thread packages/wonder-blocks-icon/src/util/directional-icons.test.ts Outdated
Comment thread .changeset/phosphor-icon-rtl-mirroring.md Outdated
@beaesguerra
beaesguerra requested a review from a team August 13, 2026 22:15
@maddy531
maddy531 force-pushed the CLASS-13991.rtl-directional-icons branch from 72980b9 to 28788bc Compare August 20, 2026 13:50
@maddy531
maddy531 requested a review from beaesguerra August 20, 2026 17:02
@maddy531

Copy link
Copy Markdown
Contributor Author

@claude review once

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I reviewed this PR again and found no bugs this pass — the changeset is now a major bump and the new test file includes Arrange/Act/Assert comments, addressing the two issues flagged in my earlier review. Given this is a breaking change to icon rendering that requires coordinated releases across wonder-blocks, perseus, and frontend, a human look is still worthwhile before merging.

Extended reasoning...

No bugs were found by the bug hunting system this run, and the two issues I previously flagged (changeset version bump and missing Arrange/Act/Assert comments in the new test file) have both been addressed in the current diff. The candidate issues raised and ruled out this run (div vs View in a docs-only story, the intentional caret-swap example in link.stories.tsx, the arrow-square-down-left/right exclusion rationale, and an import-order nit) were all examined and are not real bugs, consistent with the bug hunting system's conclusion.\n\nThis PR is nonetheless a major, breaking behavioral change: PhosphorIcon now auto-mirrors any allowlisted directional icon in RTL, which double-flips any existing isRtl-based icon swap at a call site. The rollout is explicitly coordinated across three repositories (wonder-blocks, perseus, frontend) with a specific sequencing requirement to avoid a visible regression window. The core logic (bundler-specific icon name extraction and allowlist matching in directional-icons.ts) is non-trivial but has thorough test coverage for edge cases (hashed filenames, weight suffixes, data URIs, partial-segment matches).\n\nGiven the breaking-change scope, the multi-repo coordination requirement, and that a human reviewer (beaesguerra) has been actively engaged in substantive design discussion on this PR (versioning strategy, the mirrorInRtl override, and the arrow-square-up-left exclusion), this falls squarely in the category of a large-scale design decision that warrants human sign-off rather than automated approval, even though no new bugs were found.

@jandrade jandrade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks great! Thanks for adding this useful functionality in WB 👏 🚀

<PhosphorIcon icon={caretRightIcon} mirrorInRtl={false} />
```

<Canvas of={PhosphorIconRtlStories.CustomIconMirrorInRtl} />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion: Could you please add a title and a description for how this is used with custom icons? thanks!

* Icon names mirrored in RTL by `PhosphorIcon`. Single source of truth —
* add or remove names in a Wonder Blocks PR rather than special-casing a call
* site. `mirrorInRtl` exists for the exceptions. See the PhosphorIcon RTL docs
* for deliberate exclusions (media, launch, trends, physical text-align, etc.).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion: I'd recommend adding a link to https://phosphoricons.com/ so folks know what icon names are expected to be included.

Comment on lines +15 to +19
Which icons mirror is decided by the allowlist in
`packages/wonder-blocks-icon/src/util/mirrored-icon-names.ts`. To add or remove
an icon, open a Wonder Blocks PR against that list rather than handling RTL
mirroring at the call site. For genuine one-offs, `mirrorInRtl` overrides the
allowlist in either direction.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion: I'd recommend adding a link to https://phosphoricons.com/ so folks know what icon names are expected to be included.

Maddy Andrade and others added 4 commits August 31, 2026 10:31
Whitelists reading-direction glyphs and flips them under :dir(rtl). Which
icons mirror is owned by Wonder Blocks — update directional-icons.ts rather
than handling RTL at the call site.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the RTL whitelist as an explicit source of truth so PhosphorIcon and
follow-on tooling (e.g. lint) can share the same list without editing the
matching helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
…sset names for RTL icon mirroring

Chromatic/Storybook production builds emit `arrow-right-[hash].svg`.
Suffix-only matching missed those, so glyphs never got the mirror class
even though flex layout reversed under dir=rtl.

Co-authored-by: Cursor <cursoragent@cursor.com>
@maddy531
maddy531 force-pushed the CLASS-13991.rtl-directional-icons branch from 7e36805 to 976777e Compare August 31, 2026 14:31
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.

3 participants