From c811d9f2679229d54ec5e62a77c5d8cf388df060 Mon Sep 17 00:00:00 2001 From: lvbaiqing Date: Sun, 8 Nov 2026 22:25:59 +0800 Subject: [PATCH 1/3] fix(capsule): stabilize translation language popup --- openless-all/app/e2e/ask-anything.spec.ts | 8 ++++++ .../app/src/components/CapsuleStyles.css | 6 +++++ .../components/CapsuleTranslationBadge.tsx | 25 +++++++++++++------ openless-all/app/src/components/ui/Select.tsx | 7 +++++- .../app/src/components/ui/SelectLite.tsx | 11 +++++++- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/openless-all/app/e2e/ask-anything.spec.ts b/openless-all/app/e2e/ask-anything.spec.ts index ff59e264b..b8a7666a2 100644 --- a/openless-all/app/e2e/ask-anything.spec.ts +++ b/openless-all/app/e2e/ask-anything.spec.ts @@ -19,6 +19,14 @@ test('translation uses the shared popup and native headroom; save failure preser await picker.click(); const popup = page.locator('.ol-select-popup'); await expect(popup).toBeVisible(); + await expect(popup).toHaveClass(/ol-capsule-language-popup/); + await expect(popup).toHaveCSS('backdrop-filter', 'none'); + await popup.evaluate((node) => node.setAttribute('data-test-retained', 'true')); + for (let i = 0; i < 8; i += 1) { + await page.evaluate((level) => (window as any).fixture.setCapsule({ level }), i / 10); + await expect(popup).toHaveAttribute('data-test-retained', 'true'); + await expect(popup).toHaveCSS('opacity', '1'); + } const clipped = await popup.evaluate((node) => getComputedStyle(node).clipPath.startsWith('shape('), ); diff --git a/openless-all/app/src/components/CapsuleStyles.css b/openless-all/app/src/components/CapsuleStyles.css index 7e1db9b47..e0d7937ce 100644 --- a/openless-all/app/src/components/CapsuleStyles.css +++ b/openless-all/app/src/components/CapsuleStyles.css @@ -105,6 +105,12 @@ pointer-events: auto; max-width: 174px; } +.ol-select-popup.ol-capsule-language-popup { + background: var(--ol-select-popover-bg); + backdrop-filter: none; + -webkit-backdrop-filter: none; + transition: none; +} .ol-translation-language .ol-select-trigger { height: 22px; min-width: 92px; diff --git a/openless-all/app/src/components/CapsuleTranslationBadge.tsx b/openless-all/app/src/components/CapsuleTranslationBadge.tsx index 74d7d13f6..ac8093712 100644 --- a/openless-all/app/src/components/CapsuleTranslationBadge.tsx +++ b/openless-all/app/src/components/CapsuleTranslationBadge.tsx @@ -25,6 +25,7 @@ export function CapsuleTranslationBadge({ visible = true }: { visible?: boolean const [menuOpen, setMenuOpen] = useState(false); const menuEpoch = useRef(0); const menuOwner = useRef(null); + const opening = useRef(false); const closeTimer = useRef>(); const shown = useRef(visible); shown.current = visible; @@ -96,12 +97,19 @@ export function CapsuleTranslationBadge({ visible = true }: { visible?: boolean [i18n.language, target], ); const selected = languages.find((language) => language.nativeName === target); - const options = languages.filter( - (language) => - language.nativeName === target || - !isTranslationTargetRedundant(language.nativeName, snapshot?.workingLanguages ?? []), + const options = useMemo( + () => + languages + .filter( + (language) => + language.nativeName === target || + !isTranslationTargetRedundant(language.nativeName, snapshot?.workingLanguages ?? []), + ) + .map((language) => ({ value: language.nativeName, label: language.displayName })), + [languages, target, snapshot?.workingLanguages], ); const openMenu = async (next: boolean) => { + if (next && opening.current) return; const epoch = ++menuEpoch.current; if (closeTimer.current) clearTimeout(closeTimer.current); if (!next) { @@ -122,6 +130,7 @@ export function CapsuleTranslationBadge({ visible = true }: { visible?: boolean if (!shown.current || !active?.mutable) return; pickerSession.current = active.sessionId; menuOwner.current = active.sessionId; + opening.current = true; try { await setCapsuleLanguageMenu(true, active.sessionId); await new Promise((resolve) => @@ -139,6 +148,8 @@ export function CapsuleTranslationBadge({ visible = true }: { visible?: boolean } } catch { if (epoch === menuEpoch.current) setError(true); + } finally { + opening.current = false; } }; useEffect(() => { @@ -207,10 +218,7 @@ export function CapsuleTranslationBadge({ visible = true }: { visible?: boolean
({ - value: language.nativeName, - label: language.displayName, - }))} + options={options} placeholder={selected?.displayName || target || '…'} ariaLabel={t('translation.target.title')} disabled={!visible || busy || !snapshot?.mutable} @@ -219,6 +227,7 @@ export function CapsuleTranslationBadge({ visible = true }: { visible?: boolean onChange={(value) => void change(value)} side="top" popupStyle={{ width: 240 }} + popupClassName="ol-capsule-language-popup" />
{error && ( diff --git a/openless-all/app/src/components/ui/Select.tsx b/openless-all/app/src/components/ui/Select.tsx index 6430d6aa0..c42e364d5 100644 --- a/openless-all/app/src/components/ui/Select.tsx +++ b/openless-all/app/src/components/ui/Select.tsx @@ -14,6 +14,7 @@ interface SelectProps { open?: boolean; side?: 'top' | 'bottom'; popupStyle?: CSSProperties; + popupClassName?: string; } /** Standard Base UI Select; keyboard navigation, typeahead and focus return stay upstream. */ @@ -29,6 +30,7 @@ export function Select({ open, side = 'bottom', popupStyle, + popupClassName, }: SelectProps) { return ( - + {options.map((option) => ( @@ -104,6 +107,7 @@ export function SelectLite({ open: controlledOpen, side, popupStyle, + popupClassName, }} onOpenChange={handleOpenChange} /> @@ -124,6 +128,7 @@ function SearchableSelect({ open, side = 'bottom', popupStyle, + popupClassName, }: SelectLiteProps) { const [query, setQuery] = useState(''); const inputRef = useRef(null); @@ -181,7 +186,11 @@ function SearchableSelect({ sideOffset={8} collisionPadding={8} > - +
Date: Sun, 8 Nov 2026 22:49:01 +0800 Subject: [PATCH 2/3] test(ui): await corrections page before geometry checks --- openless-all/app/e2e/control-geometry-regressions.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openless-all/app/e2e/control-geometry-regressions.spec.ts b/openless-all/app/e2e/control-geometry-regressions.spec.ts index 1ab9af29f..49e5ce18c 100644 --- a/openless-all/app/e2e/control-geometry-regressions.spec.ts +++ b/openless-all/app/e2e/control-geometry-regressions.spec.ts @@ -138,6 +138,11 @@ test('history list keeps its rounded surface and native trackpad scrolling; corr await page.screenshot({ path: test.info().outputPath('history-list.png') }); await page.getByRole('button', { name: '工具', exact: true }).click(); await page.getByRole('button', { name: '纠正规则', exact: true }).click(); + await expect(page.locator('[data-ol-page="corrections"]')).toHaveAttribute( + 'data-transition-phase', + 'settled', + { timeout: 15_000 }, + ); await settleNavigation(page); const add = page.getByRole('button', { name: '添加', exact: true }); await expect(add).toBeVisible(); From 82c28d01caee8eda2d5affe4e50b2b2dd4c65945 Mon Sep 17 00:00:00 2001 From: lvbaiqing Date: Sun, 8 Nov 2026 22:50:12 +0800 Subject: [PATCH 3/3] test(ui): wait for tools navigation to expand --- .../app/e2e/control-geometry-regressions.spec.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/openless-all/app/e2e/control-geometry-regressions.spec.ts b/openless-all/app/e2e/control-geometry-regressions.spec.ts index 49e5ce18c..f3ba12a22 100644 --- a/openless-all/app/e2e/control-geometry-regressions.spec.ts +++ b/openless-all/app/e2e/control-geometry-regressions.spec.ts @@ -136,8 +136,18 @@ test('history list keeps its rounded surface and native trackpad scrolling; corr }); expect(wheelPrevented).toBe(false); await page.screenshot({ path: test.info().outputPath('history-list.png') }); - await page.getByRole('button', { name: '工具', exact: true }).click(); - await page.getByRole('button', { name: '纠正规则', exact: true }).click(); + const tools = page.getByRole('button', { name: '工具', exact: true }); + await tools.click(); + await tools.evaluate(async (node) => { + await Promise.all( + node + .parentElement!.getAnimations({ subtree: true }) + .map((animation) => animation.finished.catch(() => {})), + ); + }); + const corrections = page.getByRole('button', { name: '纠正规则', exact: true }); + await corrections.click(); + await expect(corrections).toHaveAttribute('aria-current', 'page'); await expect(page.locator('[data-ol-page="corrections"]')).toHaveAttribute( 'data-transition-phase', 'settled',