Skip to content

Commit 2fe5383

Browse files
feat: explicit tab type creation.
1 parent df185a3 commit 2fe5383

5 files changed

Lines changed: 219 additions & 17 deletions

File tree

playwright/helpers/app-test-helpers.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,17 @@ const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$
121121

122122
export const getPreviewFrame = (page: Page) => page.frameLocator('#preview-host iframe')
123123

124-
export const addWorkspaceTab = async (page: Page) => {
125-
await page.getByRole('button', { name: 'Add tab' }).click()
124+
export const addWorkspaceTab = async (
125+
page: Page,
126+
{ kind = 'component' }: { kind?: 'component' | 'styles' } = {},
127+
) => {
128+
await page.getByRole('button', { name: 'Add tab options' }).click()
129+
if (kind === 'styles') {
130+
await page.getByRole('menuitem', { name: 'styles' }).click()
131+
return
132+
}
133+
134+
await page.getByRole('menuitem', { name: 'module' }).click()
126135
}
127136

128137
export const openWorkspaceTab = async (page: Page, fileName: string) => {

playwright/workspace-tabs.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,36 @@ test('startup restores last active workspace tab after reload', async ({ page })
174174
await expect(page.locator('#editor-panel-component')).not.toHaveAttribute('hidden', '')
175175
await expect(page.locator('#editor-panel-styles')).toHaveAttribute('hidden', '')
176176
})
177+
178+
test('add menu can create styles tab while component tab is active', async ({ page }) => {
179+
await waitForInitialRender(page)
180+
181+
await page.getByRole('tab', { name: 'Open tab App.tsx' }).click()
182+
await addWorkspaceTab(page, { kind: 'styles' })
183+
184+
await expect(page.getByRole('tab', { name: 'Open tab module.css' })).toHaveAttribute(
185+
'aria-selected',
186+
'true',
187+
)
188+
await expect(page.locator('#editor-panel-styles')).not.toHaveAttribute('hidden', '')
189+
await expect(page.locator('#editor-panel-component')).toHaveAttribute('hidden', '')
190+
await expect(page.getByRole('status', { name: 'App status' })).toContainText(
191+
'Added style tab.',
192+
)
193+
})
194+
195+
test('add menu stays closed until triggered and closes on outside click', async ({
196+
page,
197+
}) => {
198+
await waitForInitialRender(page)
199+
200+
const addButton = page.getByRole('button', { name: 'Add tab options' })
201+
const addMenu = page.getByRole('menu', { name: 'Add tab type' })
202+
203+
await expect(addMenu).toBeHidden()
204+
await addButton.click()
205+
await expect(addMenu).toBeVisible()
206+
207+
await page.getByRole('status', { name: 'App status' }).click()
208+
await expect(addMenu).toBeHidden()
209+
})

src/app.js

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,13 @@ const componentEditorHeaderLabel = document.querySelector('#editor-header-compon
8282
const stylesEditorHeaderLabel = document.querySelector('#editor-header-styles span')
8383
const aiControlsToggle = document.getElementById('ai-controls-toggle')
8484
const appThemeButtons = document.querySelectorAll('[data-app-theme]')
85+
const workspaceTabsShell = document.getElementById('workspace-tabs-shell')
8586
const workspaceTabsStrip = document.getElementById('workspace-tabs-strip')
87+
const workspaceTabAddWrap = document.getElementById('workspace-tab-add-wrap')
88+
const workspaceTabAddButton = document.getElementById('workspace-tab-add')
89+
const workspaceTabAddMenu = document.getElementById('workspace-tab-add-menu')
90+
const workspaceTabAddModule = document.getElementById('workspace-tab-add-module')
91+
const workspaceTabAddStyles = document.getElementById('workspace-tab-add-styles')
8692
const editorToolsButtons = document.querySelectorAll('[data-editor-tools-toggle]')
8793
const panelCollapseButtons = document.querySelectorAll('[data-panel-collapse]')
8894
const componentEditorPanel = document.getElementById('editor-panel-component')
@@ -182,6 +188,7 @@ const editorPool = createEditorPoolManager({ maxMounted: 2 })
182188
let workspaceTabRenameState = {
183189
tabId: '',
184190
}
191+
let workspaceTabAddMenuOpen = false
185192
let isRenderingWorkspaceTabs = false
186193
let hasPendingWorkspaceTabsRender = false
187194
const clipboardSupported = Boolean(navigator.clipboard?.writeText)
@@ -1407,6 +1414,7 @@ const syncEditorFromActiveWorkspaceTab = () => {
14071414
}
14081415

14091416
const beginWorkspaceTabRename = tabId => {
1417+
setWorkspaceTabAddMenuOpen(false)
14101418
workspaceTabRenameState = {
14111419
tabId: toNonEmptyWorkspaceText(tabId),
14121420
}
@@ -1469,6 +1477,7 @@ const finishWorkspaceTabRename = ({ tabId, nextName, cancelled = false }) => {
14691477
}
14701478

14711479
const removeWorkspaceTab = tabId => {
1480+
setWorkspaceTabAddMenuOpen(false)
14721481
const tab = workspaceTabsState.getTab(tabId)
14731482
if (!tab) {
14741483
return
@@ -1523,9 +1532,14 @@ const removeWorkspaceTab = tabId => {
15231532
})
15241533
}
15251534

1526-
const addWorkspaceTab = () => {
1527-
const activeTab = getActiveWorkspaceTab()
1528-
const normalizedKind = getTabKind(activeTab) === 'styles' ? 'styles' : 'component'
1535+
const addWorkspaceTab = kind => {
1536+
const normalizedKind =
1537+
kind === 'styles' ? 'styles' : kind === 'component' ? 'component' : ''
1538+
if (!normalizedKind) {
1539+
setStatus('Choose a tab type before adding a tab.', 'neutral')
1540+
return
1541+
}
1542+
15291543
const basePath =
15301544
normalizedKind === 'styles' ? 'src/styles/module.css' : 'src/components/module.tsx'
15311545
const language = normalizedKind === 'styles' ? 'css' : 'javascript-jsx'
@@ -1546,6 +1560,7 @@ const addWorkspaceTab = () => {
15461560
lastModified: Date.now(),
15471561
})
15481562

1563+
setWorkspaceTabAddMenuOpen(false)
15491564
setActiveWorkspaceTab(tabId)
15501565

15511566
if (normalizedKind === 'styles') {
@@ -1555,6 +1570,22 @@ const addWorkspaceTab = () => {
15551570
}
15561571
}
15571572

1573+
const setWorkspaceTabAddMenuOpen = isOpen => {
1574+
const nextOpen = Boolean(isOpen)
1575+
if (workspaceTabAddMenuOpen === nextOpen) {
1576+
return
1577+
}
1578+
1579+
workspaceTabAddMenuOpen = nextOpen
1580+
if (workspaceTabAddButton instanceof HTMLButtonElement) {
1581+
workspaceTabAddButton.setAttribute('aria-expanded', nextOpen ? 'true' : 'false')
1582+
}
1583+
1584+
if (workspaceTabAddMenu instanceof HTMLElement) {
1585+
workspaceTabAddMenu.hidden = !nextOpen
1586+
}
1587+
}
1588+
15581589
const renderWorkspaceTabs = () => {
15591590
if (!(workspaceTabsStrip instanceof HTMLElement)) {
15601591
return
@@ -1708,17 +1739,12 @@ const renderWorkspaceTabs = () => {
17081739
workspaceTabsStrip.append(tabContainer)
17091740
}
17101741

1711-
const addButton = document.createElement('button')
1712-
addButton.className = 'workspace-tab-add workspace-tab-add--strip'
1713-
addButton.id = 'workspace-tab-add'
1714-
addButton.type = 'button'
1715-
addButton.textContent = '+'
1716-
addButton.setAttribute('aria-label', 'Add tab')
1717-
addButton.title = 'Add tab'
1718-
addButton.addEventListener('click', () => {
1719-
addWorkspaceTab()
1720-
})
1721-
workspaceTabsStrip.append(addButton)
1742+
if (
1743+
workspaceTabAddWrap instanceof HTMLElement &&
1744+
workspaceTabsShell instanceof HTMLElement
1745+
) {
1746+
workspaceTabsShell.append(workspaceTabAddWrap)
1747+
}
17221748
} finally {
17231749
isRenderingWorkspaceTabs = false
17241750
}
@@ -2977,6 +3003,56 @@ window.addEventListener('beforeunload', () => {
29773003
prDrawerController.dispose()
29783004
})
29793005

3006+
document.addEventListener('pointerdown', event => {
3007+
if (!workspaceTabAddMenuOpen) {
3008+
return
3009+
}
3010+
3011+
const target = event.target
3012+
if (target instanceof Element && target.closest('#workspace-tab-add-wrap')) {
3013+
return
3014+
}
3015+
3016+
setWorkspaceTabAddMenuOpen(false)
3017+
})
3018+
3019+
document.addEventListener('keydown', event => {
3020+
if (!workspaceTabAddMenuOpen || event.key !== 'Escape') {
3021+
return
3022+
}
3023+
3024+
event.preventDefault()
3025+
setWorkspaceTabAddMenuOpen(false)
3026+
})
3027+
3028+
if (workspaceTabAddButton instanceof HTMLButtonElement) {
3029+
workspaceTabAddButton.addEventListener('click', event => {
3030+
event.stopPropagation()
3031+
setWorkspaceTabAddMenuOpen(!workspaceTabAddMenuOpen)
3032+
})
3033+
3034+
workspaceTabAddButton.addEventListener('keydown', event => {
3035+
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
3036+
event.preventDefault()
3037+
setWorkspaceTabAddMenuOpen(true)
3038+
}
3039+
})
3040+
}
3041+
3042+
if (workspaceTabAddModule instanceof HTMLButtonElement) {
3043+
workspaceTabAddModule.addEventListener('click', event => {
3044+
event.stopPropagation()
3045+
addWorkspaceTab('component')
3046+
})
3047+
}
3048+
3049+
if (workspaceTabAddStyles instanceof HTMLButtonElement) {
3050+
workspaceTabAddStyles.addEventListener('click', event => {
3051+
event.stopPropagation()
3052+
addWorkspaceTab('styles')
3053+
})
3054+
}
3055+
29803056
applyTheme(getInitialTheme(), { persist: false })
29813057
applyEditorToolsVisibility()
29823058
applyPanelCollapseState()

src/index.html

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,44 @@ <h1>
310310
role="tablist"
311311
aria-label="Workspace editor tabs"
312312
></div>
313+
<div class="workspace-tab-add-wrap" id="workspace-tab-add-wrap">
314+
<button
315+
class="workspace-tab-add workspace-tab-add--strip"
316+
id="workspace-tab-add"
317+
type="button"
318+
aria-label="Add tab options"
319+
aria-haspopup="menu"
320+
aria-expanded="false"
321+
aria-controls="workspace-tab-add-menu"
322+
title="Add tab options"
323+
>
324+
+
325+
</button>
326+
<div
327+
class="workspace-tab-add-menu"
328+
id="workspace-tab-add-menu"
329+
role="menu"
330+
aria-label="Add tab type"
331+
hidden
332+
>
333+
<button
334+
class="workspace-tab-add-menu__item"
335+
id="workspace-tab-add-module"
336+
type="button"
337+
role="menuitem"
338+
>
339+
module
340+
</button>
341+
<button
342+
class="workspace-tab-add-menu__item"
343+
id="workspace-tab-add-styles"
344+
type="button"
345+
role="menuitem"
346+
>
347+
styles
348+
</button>
349+
</div>
350+
</div>
313351
</div>
314352
</div>
315353

src/styles/panels-editor.css

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,57 @@
386386
border-bottom: none;
387387
}
388388

389+
.workspace-tab-add-wrap {
390+
position: relative;
391+
margin-left: 8px;
392+
flex: none;
393+
z-index: 16;
394+
}
395+
389396
.workspace-tab-add--strip {
390-
margin-left: auto;
397+
margin-left: 0;
391398
flex: none;
392399
}
393400

401+
.workspace-tab-add-menu {
402+
position: absolute;
403+
right: 0;
404+
top: calc(100% + 6px);
405+
display: grid;
406+
gap: 4px;
407+
min-width: 170px;
408+
padding: 6px;
409+
border-radius: 10px;
410+
border: 1px solid var(--border-strong);
411+
background: var(--surface-panel-header);
412+
box-shadow: 0 14px 28px var(--shadow-elev-1);
413+
z-index: 12;
414+
}
415+
416+
.workspace-tab-add-menu[hidden] {
417+
display: none;
418+
}
419+
420+
.workspace-tab-add-menu__item {
421+
border: 1px solid transparent;
422+
border-radius: 8px;
423+
background: transparent;
424+
color: var(--panel-text);
425+
font: inherit;
426+
text-align: left;
427+
padding: 6px 9px;
428+
cursor: pointer;
429+
}
430+
431+
.workspace-tab-add-menu__item:hover {
432+
background: var(--surface-control-hover);
433+
}
434+
435+
.workspace-tab-add-menu__item:focus-visible {
436+
outline: 2px solid var(--focus-ring);
437+
outline-offset: 1px;
438+
}
439+
394440
.workspace-tab__rename,
395441
.workspace-tab__remove {
396442
flex: none;

0 commit comments

Comments
 (0)