[6.x] Fix control panel text direction on RTL sites#15023
Merged
Conversation
reka-ui's TabsRoot defaults to dir="ltr", forcing the tab-panel subtree LTR even when the CP is RTL. Default the dir prop to useUiDirection()'s direction while keeping it overridable.
The UI-direction reset only wrapped the fieldtype component, not the field's label/instructions/errors. Nested fields (e.g. inside a Bard set) inherited the ambient content direction for their chrome instead of the UI direction. Move the dir reset onto the Field component itself so it covers the whole field, while leaf fieldtypes still opt back into content direction on their own value inputs.
The Set node view renders inside the Bard editor's content-direction subtree, so its header chrome (drag handle, title, instructions icon, switch, options dropdown) inherited content direction instead of UI direction. Bind the set container's dir to useUiDirection() so the chrome follows UI direction while nested field values still opt into content direction on their own inputs.
CodeMirror's 'direction' option only sets an internal CSS class for per-line text bidi and never puts a dir attribute on its own wrapping element, so the write-mode editor surface relied on ambient inheritance for everything else (cursor rendering, layout). Bind :dir="contentDirection" directly on the CodeMirror mount element, matching what the preview pane already does.
CodeMirror 5 doesn't inherit an ancestor's dir attribute — it only
honors its own 'direction' option, applied via setOption. The prior
fix bound :dir on the mount wrapper, which is inert for CodeMirror's
own rendering and incorrectly applied content direction to the
floating toolbar chrome that lives in the same wrapper. Replace it
with a watcher that calls codemirror.setOption('direction', ...)
whenever contentDirection resolves or changes.
The vendored resources/css/vendors/codemirror.css sets
'.CodeMirror-rtl pre { direction: ltr; }', which deviates from
upstream CodeMirror 5 (direction: rtl) and forces every line back to
LTR even when the .CodeMirror-rtl class is present. Add a
higher-specificity override scoped to .markdown-fieldtype rather
than hand-editing the vendored file.
Container.vue's direction computed fell back to document.documentElement.dir (the CP/UI direction) whenever the resolved site had no direction, coupling content direction to the CP. This made field values render RTL whenever EITHER the CP or the content was RTL, instead of following the site alone. Site::direction() always resolves for a matched site, so the only case this fallback affected was an unresolved site handle — default that to 'ltr' instead of the CP direction.
CodeMirror only marks rtl lines explicitly (.CodeMirror-rtl, todo 65's CSS); its ltr lines have no explicit direction and just inherit from the nearest ancestor with a dir attribute. Since the <Field> wrapper carries the CP/UI direction (todo 61), an ltr markdown value nested in an rtl CP inherited the CP's rtl instead of staying ltr. Set dir directly on CodeMirror's own wrapper element for both directions, at init and in the existing contentDirection watcher, so the editor is never at the mercy of ambient inheritance.
Per PR #10931 and its revert #10992, the markdown editor is treated as source/code-adjacent and is intentionally always ltr, regardless of content direction. Revert the editor-direction machinery added by the recent RTL work (CodeMirror 'direction' init option, the contentDirection watcher, the explicit wrapper dir, and the .CodeMirror-rtl override), and restore the upstream base .CodeMirror rule's 'direction: ltr' that the vendored copy had dropped, so the editor reliably stays ltr instead of inheriting the CP direction from the <Field> wrapper. Only the rendered .markdown-preview pane continues to follow content direction; its code blocks stay ltr via the existing .markdown-preview pre rule.
- Container.vue: fall back to the UI direction (not a literal 'ltr') when the site can't be resolved, so site-less publish containers (user profile forms, wizards, config forms) don't force LTR inputs in an RTL CP. - ContentDirection.vue: rewrite on reka-ui's Primitive so addons can pass as-child/as instead of always getting an unconditional wrapper div, which was breaking grid/flex/table-cell layouts. - TextFieldtype.vue: remove the dead :direction="config.direction" binding (no such config option, and Input.vue doesn't consume a direction prop); the real handling is the adjacent input-attrs dir binding.
useUiDirection's module-level dir ref is initialized at import time and only updates via an awaited MutationObserver microtask, so tests that set document.documentElement.dir without flushing it relied on an earlier test in the same file having already done so. Move the attribute set + microtask flush into a shared beforeEach (with an afterEach reset) in ContentDirection.test.js and Container.test.js so every test is self-contained and passes with test.only. ui-direction.test.js already resets modules and re-imports the composable fresh per test, so it was already order-independent; verified via test.only, no changes needed there.
The ui/index.js export-ordering hack existed because content-direction.js imported injectContainerContext from the heavy Publish/Container.vue, so evaluating the ContentDirection export early in the barrel dragged Container.vue's whole component graph in mid-evaluation and crashed on a circular-import TDZ (Cannot access 'Dropdown' before initialization). - Extract the context tuple into Publish/context.js; Container.vue now imports and re-exports it for the existing direct importers. - content-direction.js imports injectContainerContext from the new light context.js module instead of Container.vue. - Move ContentDirection.vue into Publish/, since it's conceptually part of the publish container's public surface. - ui/index.js no longer needs the ordering workaround; ContentDirection is now exported from its normal alphabetical position.
Move useUiDirection and useContentDirection from the core package entry to the ui subpath, alongside ContentDirection, so the whole direction toolkit is cohesive on @statamic/cms/ui instead of split across core and ui. To keep components/ui/index.js, bootstrap/cms/ui.js, and packages/cms/src/ui.js in sync (Package.test.js asserts all three expose the same export set), the composables are re-exported from the components/ui barrel itself, following the existing registerIconSet precedent for non-component utility exports living there. This is safe now that content-direction.js depends on the light Publish/context.js rather than the heavy Container.vue.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This refines the text-direction behavior introduced in #13024 ("Publish container text direction"). That PR applied the site/content's
dirto the whole field — including its chrome (labels, buttons, table/grid structure, toolbars). On an RTL site edited from an LTR Control Panel (or vice versa), that flips UI elements that should always follow the CP's own direction, not the content's.What changed
This is a visual-only change and only manifests where content and UI direction diverge (e.g. an RTL site edited from an LTR Control Panel, or vice versa).
Markdown fieldtype (intentional exception)
The Markdown editor stays LTR always — it's a source editor that commonly holds code, tables, links and HTML, which are far more legible LTR. This honors the prior deliberate decision (#10931 tried RTL; reverted in #10992). It also restores an upstream
.CodeMirror { direction: ltr }base rule the vendored CSS copy had dropped, so the editor no longer inherits the CP direction. The Markdown preview pane now follows the content direction (it renders the final prose, so RTL display is correct there; code blocks within it stay LTR).New public API
Two composables and a wrapper component are now exported via
@statamic/cms/ui(so they import alongside the other UI components you'd use them with):useUiDirection()— reactive Control Panel UI direction, kept live via a MutationObserver on<html dir>.useContentDirection()— reactive content direction for the current publish container, falling back to UI direction outside of one.<ContentDirection>— appliesuseContentDirection()'s value viadir. Built on reka-ui'sPrimitive, so it acceptsas(defaultdiv) andas-childto change or drop the wrapper element for grid/flex/table-cell layouts.The shared
@ui<Tabs>and<Field>components also gained an optionaldirprop, defaulting to the CP UI direction — so any<Tabs>now follows the CP direction by default (relevant if an addon uses these primitives directly).Addon developers: if your fieldtype renders free-form text, it now defaults to the UI direction like everything else. If you want a value element to follow content direction instead (matching core text/textarea/bard fieldtypes), opt it in with
useContentDirection()or wrap it in<ContentDirection>(both from@statamic/cms/ui).