diff --git a/plugin-designs/big-picture/big-picture.vue b/plugin-designs/big-picture/big-picture.vue index e5b9cdfe6..f02100847 100644 --- a/plugin-designs/big-picture/big-picture.vue +++ b/plugin-designs/big-picture/big-picture.vue @@ -105,17 +105,32 @@ const heading = computed(() => config.value.title // // Set on the HOST rather than on .bp-root, deliberately: `dir` takes effect through the inherited // CSS `direction`, and inheritance crosses shadow boundaries — so one attribute covers this -// element's own chrome AND both nested elements beneath it. Only ever SET, never cleared: a host +// element's own chrome AND both nested elements beneath it. Set and CLEARED, but only ever ours: a host // page that already declares its own dir owns that decision. const textDir = computed(() => { const l = hostLang.value || getRuntimeLocale() || detectLocale() return RTL_LOCALES.has(l) ? 'rtl' : 'ltr' }) const _self = getCurrentInstance() +// ★ WE CLEAR ONLY THE DIRECTION WE SET. The line below used to be a one-way +// `if (rtl) setAttribute('dir','rtl')` with no else, so a reader who switched from an RTL locale to +// an LTR one (ar -> zh) kept a right-to-left layout for the rest of the session: Chinese laid out +// backwards, which reads as a broken page rather than a stale setting. +// The original reason for never clearing is still honoured, and is why this remembers rather than +// just removing: a host page that declares its OWN dir owns that decision, so the value found before +// we first wrote is restored verbatim, and removed only when there was none. +let _dirPrev = null function reflectDir() { try { const host = _self?.proxy?.$el?.getRootNode?.()?.host - if (host && textDir.value === 'rtl') host.setAttribute('dir', 'rtl') + if (!host || !host.setAttribute) return + if (textDir.value === 'rtl') { + if (_dirPrev === null) _dirPrev = host.getAttribute('dir') || '' + host.setAttribute('dir', 'rtl') + } else if (_dirPrev !== null) { + if (_dirPrev) host.setAttribute('dir', _dirPrev); else host.removeAttribute('dir') + _dirPrev = null + } } catch (_) { /* no host (SSR/tests) — nothing to reflect */ } } onMounted(reflectDir) diff --git a/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue b/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue index 2dc479774..8e24e81a1 100644 --- a/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue +++ b/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue @@ -300,7 +300,7 @@ watchEffect(() => { if (hostLang.value) setRuntimeLocale(hostLang.value) }) // Set on the custom-element HOST rather than on the layout inside it, deliberately: `dir` takes // effect through the inherited CSS `direction`, and inheritance crosses shadow boundaries — so one // attribute on the host covers this composite's own chrome AND every nested element beneath it, -// which is the half the audit found missing. Only ever SET, never cleared: a host page that already +// which is the half the audit found missing. Set and CLEARED, but only ever ours: a host page that already // declares its own dir owns that decision. const textDir = computed(() => { const l = hostLang.value || getRuntimeLocale() || detectLocale() @@ -316,10 +316,21 @@ const textDir = computed(() => { // it is". Measured in this tree before, under Vue 3.5.42, when research-map.vue's emitFilterState // died the same way; the fix is its _selfInstance, and this is the same fix. const _selfInstance = getCurrentInstance() +// ★ WE CLEAR ONLY THE DIRECTION WE SET. This was a one-way `if (rtl) setAttribute` with no else, +// so ar -> zh kept a right-to-left layout for the rest of the session. The host page's own dir is +// still respected: the value found before we first wrote is restored verbatim. +let _dirPrev = null function reflectDir() { try { const host = _selfInstance?.proxy?.$el?.getRootNode?.()?.host - if (host && textDir.value === 'rtl') host.setAttribute('dir', 'rtl') + if (!host || !host.setAttribute) return + if (textDir.value === 'rtl') { + if (_dirPrev === null) _dirPrev = host.getAttribute('dir') || '' + host.setAttribute('dir', 'rtl') + } else if (_dirPrev !== null) { + if (_dirPrev) host.setAttribute('dir', _dirPrev); else host.removeAttribute('dir') + _dirPrev = null + } } catch (_) { /* no host (SSR/tests) — nothing to reflect */ } } onMounted(reflectDir) diff --git a/plugin-designs/people-groups-grid/people-groups-grid.vue b/plugin-designs/people-groups-grid/people-groups-grid.vue index 38079304d..96e623668 100644 --- a/plugin-designs/people-groups-grid/people-groups-grid.vue +++ b/plugin-designs/people-groups-grid/people-groups-grid.vue @@ -278,16 +278,31 @@ const languageCode = computed(() => // page it owns and useless for an element dropped into someone else's, which is this grid's whole // job. Set on the HOST, like both composites: `dir` takes effect through the inherited CSS // `direction`, and inheritance crosses shadow boundaries, so one attribute turns the element and -// everything it renders. Only ever SET, never cleared — a host page that declares its own dir owns +// everything it renders. Set and CLEARED, but only ever ours — a host page that declares its own dir owns // that decision. const textDir = computed(() => { const l = languageCode.value || getRuntimeLocale() || detectLocale() return RTL_LOCALES.has(String(l).split('-')[0].toLowerCase()) ? 'rtl' : 'ltr' }) +// ★ WE CLEAR ONLY THE DIRECTION WE SET. The line below used to be a one-way +// `if (rtl) setAttribute('dir','rtl')` with no else, so a reader who switched from an RTL locale to +// an LTR one (ar -> zh) kept a right-to-left layout for the rest of the session: Chinese laid out +// backwards, which reads as a broken page rather than a stale setting. +// The original reason for never clearing is still honoured, and is why this remembers rather than +// just removing: a host page that declares its OWN dir owns that decision, so the value found before +// we first wrote is restored verbatim, and removed only when there was none. +let _dirPrev = null function reflectDir() { try { const host = hostEl() - if (host && host.setAttribute && textDir.value === 'rtl') host.setAttribute('dir', 'rtl') + if (!host || !host.setAttribute) return + if (textDir.value === 'rtl') { + if (_dirPrev === null) _dirPrev = host.getAttribute('dir') || '' + host.setAttribute('dir', 'rtl') + } else if (_dirPrev !== null) { + if (_dirPrev) host.setAttribute('dir', _dirPrev); else host.removeAttribute('dir') + _dirPrev = null + } } catch (_) { /* no host (SSR/tests) — nothing to reflect */ } } watch(textDir, reflectDir) diff --git a/plugin-designs/research-map-plus-grid/research-map-plus-grid.vue b/plugin-designs/research-map-plus-grid/research-map-plus-grid.vue index 26d39dcfc..aac865d90 100644 --- a/plugin-designs/research-map-plus-grid/research-map-plus-grid.vue +++ b/plugin-designs/research-map-plus-grid/research-map-plus-grid.vue @@ -152,7 +152,7 @@ const hostList = computed(() => (typeof hostConfig.value?.list === 'string' ? ho // Set on the custom-element HOST rather than on the layout inside it, deliberately: `dir` takes // effect through the inherited CSS `direction`, and inheritance crosses shadow boundaries — so one // attribute on the host covers this composite's own chrome AND every nested element beneath it, -// which is the half the audit found missing. Only ever SET, never cleared: a host page that already +// which is the half the audit found missing. Set and CLEARED, but only ever ours: a host page that already // declares its own dir owns that decision. const textDir = computed(() => { const l = hostLang.value || getRuntimeLocale() || detectLocale() @@ -168,10 +168,25 @@ const textDir = computed(() => { // it is". Measured in this tree before, under Vue 3.5.42, when research-map.vue's emitFilterState // died the same way; the fix is its _selfInstance, and this is the same fix. const _selfInstance = getCurrentInstance() +// ★ WE CLEAR ONLY THE DIRECTION WE SET. The line below used to be a one-way +// `if (rtl) setAttribute('dir','rtl')` with no else, so a reader who switched from an RTL locale to +// an LTR one (ar -> zh) kept a right-to-left layout for the rest of the session: Chinese laid out +// backwards, which reads as a broken page rather than a stale setting. +// The original reason for never clearing is still honoured, and is why this remembers rather than +// just removing: a host page that declares its OWN dir owns that decision, so the value found before +// we first wrote is restored verbatim, and removed only when there was none. +let _dirPrev = null function reflectDir() { try { const host = _selfInstance?.proxy?.$el?.getRootNode?.()?.host - if (host && textDir.value === 'rtl') host.setAttribute('dir', 'rtl') + if (!host || !host.setAttribute) return + if (textDir.value === 'rtl') { + if (_dirPrev === null) _dirPrev = host.getAttribute('dir') || '' + host.setAttribute('dir', 'rtl') + } else if (_dirPrev !== null) { + if (_dirPrev) host.setAttribute('dir', _dirPrev); else host.removeAttribute('dir') + _dirPrev = null + } } catch (_) { /* no host (SSR/tests) — nothing to reflect */ } } onMounted(reflectDir)