diff --git a/playwright/helpers/app-test-helpers.ts b/playwright/helpers/app-test-helpers.ts index 665d038..ea2972e 100644 --- a/playwright/helpers/app-test-helpers.ts +++ b/playwright/helpers/app-test-helpers.ts @@ -125,18 +125,18 @@ export const addWorkspaceTab = async ( page: Page, { kind = 'component' }: { kind?: 'component' | 'styles' } = {}, ) => { - await page.getByRole('button', { name: 'Add tab options' }).click() + await page.getByRole('button', { name: 'Add workspace tab' }).click() if (kind === 'styles') { - await page.getByRole('menuitem', { name: 'styles' }).click() + await page.getByRole('button', { name: 'Add styles tab' }).click() return } - await page.getByRole('menuitem', { name: 'module' }).click() + await page.getByRole('button', { name: 'Add module tab' }).click() } export const openWorkspaceTab = async (page: Page, fileName: string) => { const pattern = new RegExp(`^Open tab ${escapeRegex(fileName)}$`) - await page.getByRole('tab', { name: pattern }).click() + await page.getByRole('button', { name: pattern }).click() } export const setWorkspaceTabSource = async ( @@ -162,7 +162,7 @@ export const setWorkspaceTabSource = async ( } export const setComponentEditorSource = async (page: Page, source: string) => { - await page.getByRole('tab', { name: 'Open tab App.tsx' }).click() + await page.getByRole('button', { name: 'Open tab App.tsx' }).click() const editorContent = page .locator('.editor-panel[data-editor-kind="component"] .cm-content') .first() @@ -173,7 +173,7 @@ export const setComponentEditorSource = async (page: Page, source: string) => { } export const setStylesEditorSource = async (page: Page, source: string) => { - await page.getByRole('tab', { name: 'Open tab app.css' }).click() + await page.getByRole('button', { name: 'Open tab app.css' }).click() const editorContent = page .locator('.editor-panel[data-editor-kind="styles"] .cm-content') .first() @@ -227,9 +227,9 @@ export const ensurePanelToolsVisible = async ( panelName: 'component' | 'styles', ) => { if (panelName === 'styles') { - await page.getByRole('tab', { name: 'Open tab app.css' }).click() + await page.getByRole('button', { name: 'Open tab app.css' }).click() } else { - await page.getByRole('tab', { name: 'Open tab App.tsx' }).click() + await page.getByRole('button', { name: 'Open tab App.tsx' }).click() } const button = getToolsButton(page, panelName) diff --git a/playwright/layout-panels.spec.ts b/playwright/layout-panels.spec.ts index fdadf99..be61b37 100644 --- a/playwright/layout-panels.spec.ts +++ b/playwright/layout-panels.spec.ts @@ -29,6 +29,38 @@ test('supports theme toggles', async ({ page }) => { expect(previewBackgroundColor).toBe('rgb(36, 86, 168)') }) +test('light theme defaults preview background to white', async ({ page }) => { + await waitForInitialRender(page) + + await page.getByLabel('Use light theme').click() + + const previewBackgroundColor = await page.evaluate(() => { + const previewHost = document.getElementById('preview-host') + return previewHost ? getComputedStyle(previewHost).backgroundColor : '' + }) + + expect(previewBackgroundColor).toBe('rgb(255, 255, 255)') +}) + +test('dark theme defaults preview background to editor background', async ({ page }) => { + await waitForInitialRender(page) + + const colors = await page.evaluate(() => { + const previewHost = document.getElementById('preview-host') + const componentPanel = document.getElementById('editor-panel-component') + + return { + preview: previewHost ? getComputedStyle(previewHost).backgroundColor : '', + editor: componentPanel ? getComputedStyle(componentPanel).backgroundColor : '', + } + }) + + const toRgbChannels = (value: string) => + (value.match(/\d+/g) ?? []).slice(0, 3).map(entry => Number.parseInt(entry, 10)) + + expect(toRgbChannels(colors.preview)).toEqual(toRgbChannels(colors.editor)) +}) + test('fixed layout keeps preview panel height within editor stack height', async ({ page, }) => { @@ -122,7 +154,7 @@ test('prevents collapsing all three panels at once', async ({ page }) => { const stylesPanel = page.locator('#editor-panel-styles') await getCollapseButton(page, 'component').click() - await page.getByRole('tab', { name: 'Open tab app.css' }).click() + await page.getByRole('button', { name: 'Open tab app.css' }).click() await getCollapseButton(page, 'styles').click() await expect(componentPanel).toHaveClass(/panel--collapsed-vertical/) @@ -139,7 +171,7 @@ test('prevents collapsing all three panels at once', async ({ page }) => { 'At least one panel must remain expanded.', ) - await page.getByRole('tab', { name: 'Open tab App.tsx' }).click() + await page.getByRole('button', { name: 'Open tab App.tsx' }).click() await getCollapseButton(page, 'component').click() await expectCollapseButtonState(page, 'preview', { axis: 'horizontal', @@ -199,7 +231,7 @@ test('gear tools toggles default inactive and switch active/inactive per panel', await expect(componentTools).toHaveAttribute('aria-pressed', 'false') await expect(componentTools).toHaveAttribute('title', 'Show component tools') - await page.getByRole('tab', { name: 'Open tab app.css' }).click() + await page.getByRole('button', { name: 'Open tab app.css' }).click() await stylesTools.click() await expect(stylesPanel).not.toHaveClass(/panel--tools-hidden/) await expect(stylesTools).toHaveAttribute('aria-pressed', 'true') @@ -213,7 +245,7 @@ test('fixed layout keeps inactive editor panel hidden', async ({ page }) => { const stylesPanel = page.locator('#editor-panel-styles') const assertEntryPanelVisible = async () => { - await page.getByRole('tab', { name: 'Open tab App.tsx' }).click() + await page.getByRole('button', { name: 'Open tab App.tsx' }).click() await expect(componentPanel).toBeVisible() await expect(stylesPanel).toBeHidden() } diff --git a/playwright/rendering-modes.spec.ts b/playwright/rendering-modes.spec.ts index 57e4b08..f70acbc 100644 --- a/playwright/rendering-modes.spec.ts +++ b/playwright/rendering-modes.spec.ts @@ -39,9 +39,9 @@ test('renders in react mode with css modules', async ({ page }) => { await ensurePanelToolsVisible(page, 'component') await ensurePanelToolsVisible(page, 'styles') - await page.getByRole('tab', { name: 'Open tab App.tsx' }).click() + await page.getByRole('button', { name: 'Open tab App.tsx' }).click() await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react') - await page.getByRole('tab', { name: 'Open tab app.css' }).click() + await page.getByRole('button', { name: 'Open tab app.css' }).click() await page.getByRole('combobox', { name: 'Style mode' }).selectOption('module') await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') await expectPreviewHasRenderedContent(page) @@ -416,13 +416,13 @@ test('requires render button when auto render is disabled', async ({ page }) => const autoRenderToggle = page.getByLabel('Auto render') const renderButton = page.getByRole('button', { name: 'Render' }) - await page.getByRole('tab', { name: 'Open tab App.tsx' }).click() + await page.getByRole('button', { name: 'Open tab App.tsx' }).click() await autoRenderToggle.uncheck() await expect(renderButton).toBeVisible() - await page.getByRole('tab', { name: 'Open tab app.css' }).click() + await page.getByRole('button', { name: 'Open tab app.css' }).click() await page.getByRole('combobox', { name: 'Style mode' }).selectOption('module') - await page.getByRole('tab', { name: 'Open tab App.tsx' }).click() + await page.getByRole('button', { name: 'Open tab App.tsx' }).click() await renderButton.click() await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered') diff --git a/playwright/workspace-tabs.spec.ts b/playwright/workspace-tabs.spec.ts index 1a1b5f6..7c08bdf 100644 --- a/playwright/workspace-tabs.spec.ts +++ b/playwright/workspace-tabs.spec.ts @@ -39,20 +39,18 @@ test('removing active tab selects deterministic adjacent tab', async ({ page }) await addWorkspaceTab(page) await addWorkspaceTab(page) - await page.getByRole('tab', { name: 'Open tab module-2.tsx' }).click() - await expect(page.getByRole('tab', { name: 'Open tab module-2.tsx' })).toHaveAttribute( - 'aria-selected', - 'true', - ) + await page.getByRole('button', { name: 'Open tab module-2.tsx' }).click() + await expect( + page.getByRole('button', { name: 'Open tab module-2.tsx' }), + ).toHaveAttribute('aria-current', 'true') await page.getByRole('button', { name: 'Remove tab module-2.tsx' }).click() await confirmRemoveDialog(page) - await expect(page.getByRole('tab', { name: 'Open tab module-2.tsx' })).toHaveCount(0) - await expect(page.getByRole('tab', { name: 'Open tab module-3.tsx' })).toHaveAttribute( - 'aria-selected', - 'true', - ) + await expect(page.getByRole('button', { name: 'Open tab module-2.tsx' })).toHaveCount(0) + await expect( + page.getByRole('button', { name: 'Open tab module-3.tsx' }), + ).toHaveAttribute('aria-current', 'true') }) test('removing non-active tab does not change active tab', async ({ page }) => { @@ -62,20 +60,18 @@ test('removing non-active tab does not change active tab', async ({ page }) => { await addWorkspaceTab(page) await addWorkspaceTab(page) - await page.getByRole('tab', { name: 'Open tab module-3.tsx' }).click() - await expect(page.getByRole('tab', { name: 'Open tab module-3.tsx' })).toHaveAttribute( - 'aria-selected', - 'true', - ) + await page.getByRole('button', { name: 'Open tab module-3.tsx' }).click() + await expect( + page.getByRole('button', { name: 'Open tab module-3.tsx' }), + ).toHaveAttribute('aria-current', 'true') await page.getByRole('button', { name: 'Remove tab module-2.tsx' }).click() await confirmRemoveDialog(page) - await expect(page.getByRole('tab', { name: 'Open tab module-2.tsx' })).toHaveCount(0) - await expect(page.getByRole('tab', { name: 'Open tab module-3.tsx' })).toHaveAttribute( - 'aria-selected', - 'true', - ) + await expect(page.getByRole('button', { name: 'Open tab module-2.tsx' })).toHaveCount(0) + await expect( + page.getByRole('button', { name: 'Open tab module-3.tsx' }), + ).toHaveAttribute('aria-current', 'true') }) test('renaming module tab keeps name and path synchronized', async ({ page }) => { @@ -87,9 +83,9 @@ test('renaming module tab keeps name and path synchronized', async ({ page }) => to: 'card-item.tsx', }) - const tab = page.getByRole('tab', { name: 'Open tab card-item.tsx' }) + const tab = page.getByRole('button', { name: 'Open tab card-item.tsx' }) await expect(tab).toHaveAttribute('title', 'src/components/card-item.tsx') - await expect(page.getByRole('tab', { name: 'Open tab module.tsx' })).toHaveCount(0) + await expect(page.getByRole('button', { name: 'Open tab module.tsx' })).toHaveCount(0) }) test('renaming module tab preserves source content', async ({ page }) => { @@ -107,8 +103,8 @@ test('renaming module tab preserves source content', async ({ page }) => { to: 'value-card.tsx', }) - await page.getByRole('tab', { name: 'Open tab App.tsx' }).click() - await page.getByRole('tab', { name: 'Open tab value-card.tsx' }).click() + await page.getByRole('button', { name: 'Open tab App.tsx' }).click() + await page.getByRole('button', { name: 'Open tab value-card.tsx' }).click() const editorContent = page .locator('.editor-panel[data-editor-kind="component"] .cm-content') @@ -125,27 +121,26 @@ test('active tab remains source of truth for visible editor panel', async ({ pag const componentPanel = page.locator('#editor-panel-component') const stylesPanel = page.locator('#editor-panel-styles') - await page.getByRole('tab', { name: 'Open tab app.css' }).click() - await expect(page.getByRole('tab', { name: 'Open tab app.css' })).toHaveAttribute( - 'aria-selected', + await page.getByRole('button', { name: 'Open tab app.css' }).click() + await expect(page.getByRole('button', { name: 'Open tab app.css' })).toHaveAttribute( + 'aria-current', 'true', ) await expect(stylesPanel).not.toHaveAttribute('hidden', '') await expect(componentPanel).toHaveAttribute('hidden', '') - await page.getByRole('tab', { name: 'Open tab module-2.tsx' }).click() - await expect(page.getByRole('tab', { name: 'Open tab module-2.tsx' })).toHaveAttribute( - 'aria-selected', - 'true', - ) + await page.getByRole('button', { name: 'Open tab module-2.tsx' }).click() + await expect( + page.getByRole('button', { name: 'Open tab module-2.tsx' }), + ).toHaveAttribute('aria-current', 'true') await expect(componentPanel).not.toHaveAttribute('hidden', '') await expect(stylesPanel).toHaveAttribute('hidden', '') await page.locator('#collapse-component').click() - await page.getByRole('tab', { name: 'Open tab app.css' }).click() + await page.getByRole('button', { name: 'Open tab app.css' }).click() - await expect(page.getByRole('tab', { name: 'Open tab app.css' })).toHaveAttribute( - 'aria-selected', + await expect(page.getByRole('button', { name: 'Open tab app.css' })).toHaveAttribute( + 'aria-current', 'true', ) await expect(stylesPanel).not.toHaveAttribute('hidden', '') @@ -158,19 +153,17 @@ test('startup restores last active workspace tab after reload', async ({ page }) await addWorkspaceTab(page) await addWorkspaceTab(page) - await page.getByRole('tab', { name: 'Open tab module-2.tsx' }).click() - await expect(page.getByRole('tab', { name: 'Open tab module-2.tsx' })).toHaveAttribute( - 'aria-selected', - 'true', - ) + await page.getByRole('button', { name: 'Open tab module-2.tsx' }).click() + await expect( + page.getByRole('button', { name: 'Open tab module-2.tsx' }), + ).toHaveAttribute('aria-current', 'true') await page.reload() await waitForInitialRender(page) - await expect(page.getByRole('tab', { name: 'Open tab module-2.tsx' })).toHaveAttribute( - 'aria-selected', - 'true', - ) + await expect( + page.getByRole('button', { name: 'Open tab module-2.tsx' }), + ).toHaveAttribute('aria-current', 'true') await expect(page.locator('#editor-panel-component')).not.toHaveAttribute('hidden', '') await expect(page.locator('#editor-panel-styles')).toHaveAttribute('hidden', '') }) @@ -178,11 +171,11 @@ test('startup restores last active workspace tab after reload', async ({ page }) test('add menu can create styles tab while component tab is active', async ({ page }) => { await waitForInitialRender(page) - await page.getByRole('tab', { name: 'Open tab App.tsx' }).click() + await page.getByRole('button', { name: 'Open tab App.tsx' }).click() await addWorkspaceTab(page, { kind: 'styles' }) - await expect(page.getByRole('tab', { name: 'Open tab module.css' })).toHaveAttribute( - 'aria-selected', + await expect(page.getByRole('button', { name: 'Open tab module.css' })).toHaveAttribute( + 'aria-current', 'true', ) await expect(page.locator('#editor-panel-styles')).not.toHaveAttribute('hidden', '') @@ -197,8 +190,8 @@ test('add menu stays closed until triggered and closes on outside click', async }) => { await waitForInitialRender(page) - const addButton = page.getByRole('button', { name: 'Add tab options' }) - const addMenu = page.getByRole('menu', { name: 'Add tab type' }) + const addButton = page.getByRole('button', { name: 'Add workspace tab' }) + const addMenu = page.getByRole('group', { name: 'Add workspace tab' }) await expect(addMenu).toBeHidden() await addButton.click() @@ -207,3 +200,24 @@ test('add menu stays closed until triggered and closes on outside click', async await page.getByRole('status', { name: 'App status' }).click() await expect(addMenu).toBeHidden() }) + +test('add menu keyboard interaction manages focus on open and escape close', async ({ + page, +}) => { + await waitForInitialRender(page) + + const addButton = page.getByRole('button', { name: 'Add workspace tab' }) + const addMenu = page.getByRole('group', { name: 'Add workspace tab' }) + const addModuleButton = page.getByRole('button', { name: 'Add module tab' }) + + await addButton.focus() + await page.keyboard.press('ArrowDown') + + await expect(addMenu).toBeVisible() + await expect(addModuleButton).toBeFocused() + + await page.keyboard.press('Escape') + + await expect(addMenu).toBeHidden() + await expect(addButton).toBeFocused() +}) diff --git a/src/app.js b/src/app.js index 741ddfe..2c5cad1 100644 --- a/src/app.js +++ b/src/app.js @@ -226,6 +226,17 @@ const showAppToast = message => { const previewBackground = createPreviewBackgroundController({ previewBgColorInput, getPreviewHost: () => previewHost, + getDefaultPreviewBackgroundColor: () => { + if (document.documentElement.dataset.theme === 'light') { + return '#ffffff' + } + + if (componentEditorPanel instanceof HTMLElement) { + return getComputedStyle(componentEditorPanel).backgroundColor + } + + return '' + }, }) const layoutTheme = createLayoutThemeController({ @@ -1584,6 +1595,24 @@ const setWorkspaceTabAddMenuOpen = isOpen => { if (workspaceTabAddMenu instanceof HTMLElement) { workspaceTabAddMenu.hidden = !nextOpen } + + if ( + nextOpen && + document.activeElement === workspaceTabAddButton && + workspaceTabAddModule instanceof HTMLButtonElement + ) { + workspaceTabAddModule.focus() + } + + if ( + !nextOpen && + workspaceTabAddMenu instanceof HTMLElement && + document.activeElement instanceof Node && + workspaceTabAddMenu.contains(document.activeElement) && + workspaceTabAddButton instanceof HTMLButtonElement + ) { + workspaceTabAddButton.focus() + } } const renderWorkspaceTabs = () => { @@ -1606,11 +1635,10 @@ const renderWorkspaceTabs = () => { for (const tab of tabs) { const isActive = tab.id === activeTabId - const tabContainer = document.createElement('div') + const tabContainer = document.createElement('li') tabContainer.className = 'workspace-tab' - tabContainer.setAttribute('role', 'presentation') + tabContainer.dataset.active = isActive ? 'true' : 'false' tabContainer.dataset.tabId = tab.id - tabContainer.setAttribute('aria-selected', isActive ? 'true' : 'false') tabContainer.addEventListener('click', event => { const clickTarget = event.target if (!(clickTarget instanceof Element)) { @@ -1684,8 +1712,11 @@ const renderWorkspaceTabs = () => { fileNameNode.textContent = tabDisplay.fileName || tab.name selectButton.append(fileNameNode) - selectButton.setAttribute('role', 'tab') - selectButton.setAttribute('aria-selected', isActive ? 'true' : 'false') + if (isActive) { + selectButton.setAttribute('aria-current', 'true') + } else { + selectButton.removeAttribute('aria-current') + } selectButton.setAttribute('aria-label', `Open tab ${tab.name}`) selectButton.addEventListener('click', event => { event.stopPropagation() @@ -3035,6 +3066,9 @@ if (workspaceTabAddButton instanceof HTMLButtonElement) { if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') { event.preventDefault() setWorkspaceTabAddMenuOpen(true) + if (workspaceTabAddModule instanceof HTMLButtonElement) { + workspaceTabAddModule.focus() + } } }) } diff --git a/src/index.html b/src/index.html index c6c568b..3c21f51 100644 --- a/src/index.html +++ b/src/index.html @@ -299,42 +299,40 @@