Skip to content

Commit 9f8d3e7

Browse files
committed
fix(tabs): keep touch sizing stable across activity changes
1 parent d23e93d commit 9f8d3e7

3 files changed

Lines changed: 72 additions & 9 deletions

File tree

‎apps/desktop/e2e/browser-chrome.spec.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,30 @@ mountBrowserChromeFixture(useBrowserPanelOcclusion);`,
194194
.evaluate((element) => element.getBoundingClientRect().width)
195195
)
196196
.toBeGreaterThanOrEqual(48)
197+
const attentionItem = page.locator('[data-tab-strip-item="tab-1"]')
198+
const indicator = attentionItem.locator('[data-row-action-indicator]')
199+
const controls = attentionItem.locator('[data-row-action-controls]')
200+
await expect(indicator).toBeVisible()
201+
await expect(indicator).toHaveCSS('opacity', '1')
202+
await expect(controls).toHaveCSS('opacity', '1')
203+
await attentionItem.getByRole('button', { name: /^Close / }).click({ trial: true })
204+
expect(
205+
await attentionItem.evaluate((element) => {
206+
const title = element.querySelector('[data-overflow-text]')?.getBoundingClientRect()
207+
const indicator = element
208+
.querySelector('[data-row-action-indicator]')
209+
?.getBoundingClientRect()
210+
const close = element.querySelector('[aria-label^="Close "]')?.getBoundingClientRect()
211+
return (
212+
title &&
213+
indicator &&
214+
close &&
215+
title.right <= indicator.left &&
216+
indicator.right <= close.left &&
217+
close.right <= element.getBoundingClientRect().right
218+
)
219+
})
220+
).toBe(true)
197221
await page.screenshot({ path: testInfo.outputPath('crowded-tabs-touch.png') })
198222
const beforeSelection = await attention.evaluate(
199223
(element: HTMLElement) => element.offsetWidth
@@ -203,6 +227,27 @@ mountBrowserChromeFixture(useBrowserPanelOcclusion);`,
203227
expect(await attention.evaluate((element: HTMLElement) => element.offsetWidth)).toBe(
204228
beforeSelection
205229
)
230+
await page.locator('#toggle-activity').click()
231+
expect(await attention.evaluate((element: HTMLElement) => element.offsetWidth)).toBe(
232+
beforeSelection
233+
)
234+
await page.locator('#toggle-activity').click()
235+
await page.locator('#medium-tabs').click()
236+
await page.getByRole('tab').first().click()
237+
const intrinsicWidth = await attention.evaluate(
238+
(element: HTMLElement) => element.offsetWidth
239+
)
240+
expect(intrinsicWidth).toBeGreaterThan(144)
241+
expect(intrinsicWidth).toBeLessThan(200)
242+
await attention.click()
243+
expect(await attention.evaluate((element: HTMLElement) => element.offsetWidth)).toBe(
244+
intrinsicWidth
245+
)
246+
await page.locator('#toggle-activity').click()
247+
expect(await attention.evaluate((element: HTMLElement) => element.offsetWidth)).toBe(
248+
intrinsicWidth
249+
)
250+
await page.locator('#toggle-activity').click()
206251
} finally {
207252
await session.send('Emulation.setTouchEmulationEnabled', { enabled: false })
208253
await session.detach()

‎apps/desktop/e2e/fixtures/browser-chrome.tsx‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ function BrowserChromeFixture({ useOcclusion }: BrowserChromeFixtureProps) {
4646
const [activeTabId, setActiveTabId] = useState<string | null>(null)
4747
const [selected, setSelected] = useState('tab-0')
4848
const [tabCount, setTabCount] = useState(8)
49-
const [shortTitles, setShortTitles] = useState(false)
49+
const [titleLength, setTitleLength] = useState<'short' | 'medium' | 'long'>('long')
50+
const [attention, setAttention] = useState(true)
5051
const [error, setError] = useState<string | null>(null)
5152
const api = (globalThis as typeof globalThis & { simDesktop: SimDesktopApi }).simDesktop
5253
const { snapshot, snapshotLayer, onSnapshotError } = useOcclusion(
@@ -93,26 +94,43 @@ function BrowserChromeFixture({ useOcclusion }: BrowserChromeFixtureProps) {
9394
id='eight-tabs'
9495
onClick={() => {
9596
setTabCount(8)
96-
setShortTitles(false)
97+
setTitleLength('long')
9798
}}
9899
>
99100
Eight tabs
100101
</Button>
101102
<Button id='many-tabs' onClick={() => setTabCount(18)}>
102103
Many tabs
103104
</Button>
104-
<Button id='short-tabs' onClick={() => setShortTitles(true)}>
105+
<Button id='short-tabs' onClick={() => setTitleLength('short')}>
105106
Short titles
106107
</Button>
108+
<Button
109+
id='medium-tabs'
110+
onClick={() => {
111+
setTitleLength('medium')
112+
setTabCount(4)
113+
}}
114+
>
115+
Medium titles
116+
</Button>
117+
<Button id='toggle-activity' onClick={() => setAttention((value) => !value)}>
118+
Toggle activity
119+
</Button>
107120
</div>
108121
{error && <p role='alert'>{error}</p>}
109122
<TabStrip
110123
tabs={Array.from({ length: tabCount }, (_, index) => ({
111124
id: `tab-${index}`,
112-
title: shortTitles ? 'A' : `Example resource ${index + 1} with a descriptive title`,
125+
title:
126+
titleLength === 'short'
127+
? 'A'
128+
: titleLength === 'medium'
129+
? 'Medium title'
130+
: `Example resource ${index + 1} with a descriptive title`,
113131
icon: <File className='size-[16px] shrink-0' />,
114132
active: selected === `tab-${index}`,
115-
attention: index === 1,
133+
attention: attention && index === 1,
116134
}))}
117135
variant='floating'
118136
onSelect={setSelected}

‎packages/emcn/src/components/tab-strip/tab-strip.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ const TAB_TRANSITION = { duration: 0.1, ease: [0.2, 0, 0, 1] as const }
4242
* the available space. Their 112px minimum leaves 50px for the title beside a
4343
* 16px icon and visible close button, including OverflowText's fade. Keep the
4444
* same minimum in every interaction state so revealing actions never shifts
45-
* tabs beneath the pointer. Crowded rows then scroll.
45+
* tabs beneath the pointer. Touch layouts reserve both action slots even when
46+
* no activity indicator is present, so activity changes cannot resize tabs.
47+
* Crowded rows then scroll.
4648
*/
4749
const TAB_WIDTH: Record<TabStripVariant, string> = {
4850
attached: 'w-[156px] min-w-[96px] shrink',
@@ -356,8 +358,7 @@ const Tab = forwardRef<HTMLDivElement, TabProps>(function Tab(
356358
tab.pinned ? 'justify-center px-0' : 'justify-start gap-1.5 px-2',
357359
closeable && 'pr-8',
358360
closeable &&
359-
tab.attention &&
360-
!tab.active &&
361+
(variant === 'floating' || (tab.attention && !tab.active)) &&
361362
'[@media(any-pointer:coarse)]:pr-[62px] [@media(hover:none)]:pr-[62px]',
362363
TAB_SHAPE[variant],
363364
tab.selected && !tab.active && TAB_SELECTED[variant],
@@ -408,7 +409,6 @@ const Tab = forwardRef<HTMLDivElement, TabProps>(function Tab(
408409
tab.pinned ? 'w-[34px] min-w-[34px] max-w-[34px] flex-none' : TAB_WIDTH[variant],
409410
variant === 'floating' &&
410411
closeable &&
411-
tab.attention &&
412412
'[@media(any-pointer:coarse)]:min-w-36 [@media(hover:none)]:min-w-36',
413413
dragging && 'opacity-30'
414414
)}

0 commit comments

Comments
 (0)