diff --git a/config/scripts/mobile-web-app/mobile-web-app-rich-markdown-render.test.mjs b/config/scripts/mobile-web-app/mobile-web-app-rich-markdown-render.test.mjs index 867e60823f7..d2b7d2a83a0 100644 --- a/config/scripts/mobile-web-app/mobile-web-app-rich-markdown-render.test.mjs +++ b/config/scripts/mobile-web-app/mobile-web-app-rich-markdown-render.test.mjs @@ -14,6 +14,7 @@ import { lucideBarrelPlugin } from '../build-mobile-web-app-bundle.mjs' import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs' +import { themeSwitchTest } from './mobile-web-app-theme-switch.mjs' import { textInputFontSizeFloor } from './platform-seams/mobile-web-app-text-input-font-size-seam.mjs' import { createBundleServer, @@ -77,6 +78,7 @@ import { createElement, useEffect, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' import { SafeAreaProvider } from 'react-native-safe-area-context' import { MobileRichMarkdownEditor } from './MobileRichMarkdownEditor' +import { applyMobileColorScheme } from '../theme/mobile-theme' /** * What the route's own navigator supplies and a bare mount does not: react-navigation wraps every @@ -101,6 +103,7 @@ function Harness() { useEffect(() => { globalThis.__orcaEditor = { set: (next) => setState((previous) => ({ ...previous, ...next })), + setScheme: applyMobileColorScheme, changes: [], secondChanges: [], links: [], @@ -293,9 +296,8 @@ async function setContentWithin(page, markdown, selector) { // Nested ul-in-p serialization once returned unchanged text, leaving next command against previous document. const bodyFor = (index) => `body text ${String(index)}` -function readSurface(page, surface = '#first-surface') { - return page.evaluate((id) => document.querySelector(`${id} #editor`)?.innerHTML ?? null, surface) -} +const readSurface = (page, surface = '#first-surface') => + page.evaluate((id) => document.querySelector(`${id} #editor`)?.innerHTML ?? null, surface) async function select(page, how) { await page.evaluate((mode) => { @@ -328,9 +330,9 @@ describeEditor( browser = await engine.launch() }, 180_000) - afterAll(async () => { - await browser?.close() - }) + afterAll(() => browser?.close()) + + themeSwitchTest(() => browser, openPage) it('mounts the document, reports ready through the seam and never touches the bridge', async () => { const { page, consoleErrors } = await openPage(browser) diff --git a/config/scripts/mobile-web-app/mobile-web-app-theme-switch.mjs b/config/scripts/mobile-web-app/mobile-web-app-theme-switch.mjs new file mode 100644 index 00000000000..09d7327d403 --- /dev/null +++ b/config/scripts/mobile-web-app/mobile-web-app-theme-switch.mjs @@ -0,0 +1,42 @@ +import { expect, it } from 'vitest' + +const readStyling = (page) => + page.evaluate(() => { + const editor = document.querySelector('#editor') + return { + scheme: document.getElementById('orca-rich-markdown-document-style').dataset.scheme, + scoped: editor.parentElement.classList.contains('orca-rich-markdown-document-host'), + taskListStyle: getComputedStyle(editor.querySelector('ul[data-type="taskList"]')) + .listStyleType + } + }) + +async function setScheme(page, scheme) { + await page.evaluate((next) => globalThis.__orcaEditor.setScheme(next), scheme) + await page.waitForFunction( + (next) => document.getElementById('orca-rich-markdown-document-style').dataset.scheme === next, + scheme + ) + await page.evaluate( + () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))) + ) +} + +// React Native Web swaps the themed host View's className on a scheme change. +export function themeSwitchTest(getBrowser, openPage) { + it('keeps a mounted editor styled through color scheme changes', async () => { + const { page, consoleErrors } = await openPage(getBrowser()) + try { + await setScheme(page, 'dark') + await page.evaluate(() => globalThis.__orcaEditor.set({ content: '- [ ] Open task' })) + await page.waitForSelector('#editor ul[data-type="taskList"]') + for (const scheme of ['dark', 'light', 'dark']) { + await setScheme(page, scheme) + expect(await readStyling(page)).toEqual({ scheme, scoped: true, taskListStyle: 'none' }) + } + expect(consoleErrors).toEqual([]) + } finally { + await page.close() + } + }, 180_000) +} diff --git a/mobile/src/components/MobileRichMarkdownEditor.tsx b/mobile/src/components/MobileRichMarkdownEditor.tsx index 547c251eed5..250fe530425 100644 --- a/mobile/src/components/MobileRichMarkdownEditor.tsx +++ b/mobile/src/components/MobileRichMarkdownEditor.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useImperativeHandle, + useLayoutEffect, useMemo, useRef, useState, @@ -178,9 +179,11 @@ function MobileRichMarkdownEditorWebInner( // Mount document once; read promptForUrl via closure because rerunning on changing callback props discards live caret/document. }, []) - useEffect(() => { - if (documentRef.current) { - ensureDocumentStyle() + // Layout effect: restore the host's scope class before paint after a themed className swap. + useLayoutEffect(() => { + const host = hostRef.current + if (documentRef.current && host instanceof HTMLElement) { + ensureDocumentStyle(host) } }, [colorScheme]) diff --git a/mobile/src/components/rich-markdown/rich-markdown-web-document-mount.ts b/mobile/src/components/rich-markdown/rich-markdown-web-document-mount.ts index 4fb05a96bd7..487ecf403b3 100644 --- a/mobile/src/components/rich-markdown/rich-markdown-web-document-mount.ts +++ b/mobile/src/components/rich-markdown/rich-markdown-web-document-mount.ts @@ -28,7 +28,9 @@ export const RICH_MARKDOWN_HOST_CLASS = 'orca-rich-markdown-document-host' // Plant sheet once per page, scoped to host class; move document-wide theme/color/font globals onto host itself. // Leave sheet in head on unmount: removing host class makes it inert, and next mount reuses it. // Rewrite it in place when the color scheme changes; live editors keep their DOM and caret. -export function ensureDocumentStyle() { +export function ensureDocumentStyle(host: HTMLElement) { + // React Native Web replaces themed View classes when its background changes. + host.classList.add(RICH_MARKDOWN_HOST_CLASS) const scheme = getMobileColorScheme() const existing = document.getElementById(STYLE_ELEMENT_ID) if (existing?.dataset.scheme === scheme) { @@ -51,8 +53,7 @@ export function mountRichMarkdownWebDocument( host: HTMLElement, hooks: RichMarkdownWebDocumentHooks ): RichMarkdownWebDocument { - ensureDocumentStyle() - host.classList.add(RICH_MARKDOWN_HOST_CLASS) + ensureDocumentStyle(host) host.innerHTML = RICH_MARKDOWN_EDITOR_MARKUP const started = startDocumentOrGiveTheHostBack(host, hooks) return {