fix(web): keep sidebar scrollbar inside inset
This commit is contained in:
@@ -106,6 +106,8 @@ interface ListMetrics {
|
|||||||
overflows: boolean
|
overflows: boolean
|
||||||
/** Border-box width minus client width: the space the scrollbar takes out of the content area. */
|
/** Border-box width minus client width: the space the scrollbar takes out of the content area. */
|
||||||
band: number
|
band: number
|
||||||
|
/** Distance from the first row background's right edge to the list border box. */
|
||||||
|
rowEdgeInset: number
|
||||||
/** Client-area right edge in viewport coordinates (`clientWidth` excludes the scrollbar band). */
|
/** Client-area right edge in viewport coordinates (`clientWidth` excludes the scrollbar band). */
|
||||||
clientRight: number
|
clientRight: number
|
||||||
/** Border-box right edge in viewport coordinates. */
|
/** Border-box right edge in viewport coordinates. */
|
||||||
@@ -133,6 +135,8 @@ function measureList(page: Page): Promise<ListMetrics> {
|
|||||||
if (list === null) throw new Error('sidebar session list not in the DOM')
|
if (list === null) throw new Error('sidebar session list not in the DOM')
|
||||||
const time = list.querySelector<HTMLElement>('[class*="time"]')
|
const time = list.querySelector<HTMLElement>('[class*="time"]')
|
||||||
if (time === null) throw new Error('no row relative-time element in the sidebar list')
|
if (time === null) throw new Error('no row relative-time element in the sidebar list')
|
||||||
|
const row = list.querySelector<HTMLElement>('[role="treeitem"]')
|
||||||
|
if (row === null) throw new Error('no row in the sidebar list')
|
||||||
// Each indirection variable is resolved through its own throwaway probe
|
// Each indirection variable is resolved through its own throwaway probe
|
||||||
// appended to the list: `var()` substitution then happens where the list
|
// appended to the list: `var()` substitution then happens where the list
|
||||||
// sits in the cascade, which is the claim, and `color` normalizes whatever
|
// sits in the cascade, which is the claim, and `color` normalizes whatever
|
||||||
@@ -167,6 +171,7 @@ function measureList(page: Page): Promise<ListMetrics> {
|
|||||||
const style = getComputedStyle(list)
|
const style = getComputedStyle(list)
|
||||||
const pseudoWidth = getComputedStyle(list, '::-webkit-scrollbar').width
|
const pseudoWidth = getComputedStyle(list, '::-webkit-scrollbar').width
|
||||||
const barWidth = pseudoWidth === 'auto' ? 15 : Number.parseFloat(pseudoWidth)
|
const barWidth = pseudoWidth === 'auto' ? 15 : Number.parseFloat(pseudoWidth)
|
||||||
|
const listRect = list.getBoundingClientRect()
|
||||||
return {
|
return {
|
||||||
gutter: style.scrollbarGutter,
|
gutter: style.scrollbarGutter,
|
||||||
width: pseudoWidth,
|
width: pseudoWidth,
|
||||||
@@ -177,9 +182,10 @@ function measureList(page: Page): Promise<ListMetrics> {
|
|||||||
token: resolve('--dsh-scrollbar-thumb'),
|
token: resolve('--dsh-scrollbar-thumb'),
|
||||||
hoverToken: resolve('--dsh-scrollbar-thumb-hover'),
|
hoverToken: resolve('--dsh-scrollbar-thumb-hover'),
|
||||||
overflows: list.scrollHeight > list.clientHeight,
|
overflows: list.scrollHeight > list.clientHeight,
|
||||||
band: list.getBoundingClientRect().width - list.clientWidth,
|
band: listRect.width - list.clientWidth,
|
||||||
clientRight: list.getBoundingClientRect().left + list.clientWidth,
|
rowEdgeInset: listRect.right - row.getBoundingClientRect().right,
|
||||||
borderRight: list.getBoundingClientRect().right,
|
clientRight: listRect.left + list.clientWidth,
|
||||||
|
borderRight: listRect.right,
|
||||||
timeRight: time.getBoundingClientRect().right,
|
timeRight: time.getBoundingClientRect().right,
|
||||||
// The bar is drawn in the rightmost `barWidth` of the border box, whether
|
// The bar is drawn in the rightmost `barWidth` of the border box, whether
|
||||||
// or not that space was reserved. Its width comes from the sheet where the
|
// or not that space was reserved. Its width comes from the sheet where the
|
||||||
@@ -188,7 +194,26 @@ function measureList(page: Page): Promise<ListMetrics> {
|
|||||||
// absent. Taking the UA width as the fallback is what keeps the assertion
|
// absent. Taking the UA width as the fallback is what keeps the assertion
|
||||||
// honest: assuming 0 there would report no occlusion precisely in the
|
// honest: assuming 0 there would report no occlusion precisely in the
|
||||||
// state that has it.
|
// state that has it.
|
||||||
timeCoveredBy: Math.max(0, time.getBoundingClientRect().right - (list.getBoundingClientRect().right - barWidth)),
|
timeCoveredBy: Math.max(0, time.getBoundingClientRect().right - (listRect.right - barWidth)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measure only overflow and row inset, which remain observable when every
|
||||||
|
* session is hidden under a collapsed workspace group.
|
||||||
|
* @param page - the page under test.
|
||||||
|
* @returns the list overflow state and first row's trailing inset.
|
||||||
|
*/
|
||||||
|
function measureRowInset(page: Page): Promise<Pick<ListMetrics, 'overflows' | 'rowEdgeInset'>> {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const list = document.querySelector<HTMLElement>('[role="tree"][aria-label="Sessions"]')
|
||||||
|
if (list === null) throw new Error('sidebar session list not in the DOM')
|
||||||
|
const row = list.querySelector<HTMLElement>('[role="treeitem"]')
|
||||||
|
if (row === null) throw new Error('no row in the sidebar list')
|
||||||
|
return {
|
||||||
|
overflows: list.scrollHeight > list.clientHeight,
|
||||||
|
rowEdgeInset: list.getBoundingClientRect().right - row.getBoundingClientRect().right,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -222,6 +247,7 @@ function renderGeometry(light: ListMetrics, dark: ListMetrics): string {
|
|||||||
`- --dsh-scrollbar-thumb-hover: ${metrics.hoverToken}`,
|
`- --dsh-scrollbar-thumb-hover: ${metrics.hoverToken}`,
|
||||||
`- list overflows: ${String(metrics.overflows)}`,
|
`- list overflows: ${String(metrics.overflows)}`,
|
||||||
`- reserved band: ${String(metrics.band)}px`,
|
`- reserved band: ${String(metrics.band)}px`,
|
||||||
|
`- row background inset from the sidebar edge: ${String(metrics.rowEdgeInset)}px`,
|
||||||
`- relative time covered by the bar: ${String(metrics.timeCoveredBy)}px`,
|
`- relative time covered by the bar: ${String(metrics.timeCoveredBy)}px`,
|
||||||
`- relative time ends inside the content area: ${String(metrics.timeRight <= metrics.clientRight)}`,
|
`- relative time ends inside the content area: ${String(metrics.timeRight <= metrics.clientRight)}`,
|
||||||
`- content area ends before the border box: ${String(metrics.clientRight < metrics.borderRight)}`,
|
`- content area ends before the border box: ${String(metrics.clientRight < metrics.borderRight)}`,
|
||||||
@@ -299,6 +325,7 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
|||||||
// drawn over it. Removing the declaration makes it exactly 0. The value
|
// drawn over it. Removing the declaration makes it exactly 0. The value
|
||||||
// itself is not pinned — it tracks `scrollbar-width` and the platform.
|
// itself is not pinned — it tracks `scrollbar-width` and the platform.
|
||||||
expect(metrics.band).toBeGreaterThan(0)
|
expect(metrics.band).toBeGreaterThan(0)
|
||||||
|
expect(metrics.rowEdgeInset).toBe(12)
|
||||||
// The reported symptom, stated directly: no part of the row's relative time
|
// The reported symptom, stated directly: no part of the row's relative time
|
||||||
// lies under the bar. Measures 7 on clean master — the `h` of `1h` is the
|
// lies under the bar. Measures 7 on clean master — the `h` of `1h` is the
|
||||||
// covered part. Unlike the client-edge comparison below it does not go
|
// covered part. Unlike the client-edge comparison below it does not go
|
||||||
@@ -317,6 +344,20 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
|||||||
expect(tripwire.pageErrors).toEqual([])
|
expect(tripwire.pageErrors).toEqual([])
|
||||||
}, 60_000)
|
}, 60_000)
|
||||||
|
|
||||||
|
it('keeps the row background inset when overflow disappears', async () => {
|
||||||
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-stable-inset'))
|
||||||
|
expect(await measureRowInset(page)).toEqual({ overflows: true, rowEdgeInset: 12 })
|
||||||
|
const bucket = page.getByText('Ungrouped', { exact: true }).locator('..').locator('..')
|
||||||
|
await bucket.click()
|
||||||
|
try {
|
||||||
|
await expect.poll(async () => (await measureRowInset(page)).overflows, { timeout: 10_000 }).toBe(false)
|
||||||
|
expect(await measureRowInset(page)).toEqual({ overflows: false, rowEdgeInset: 12 })
|
||||||
|
} finally {
|
||||||
|
await expandSeededSessions(page)
|
||||||
|
}
|
||||||
|
expect(tripwire.pageErrors).toEqual([])
|
||||||
|
}, 60_000)
|
||||||
|
|
||||||
it('renders the themed thumb through the WebKit path in both palettes', async () => {
|
it('renders the themed thumb through the WebKit path in both palettes', async () => {
|
||||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-theme'))
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-theme'))
|
||||||
const light = await measureList(page)
|
const light = await measureList(page)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
- --dsh-scrollbar-thumb-hover: rgb(212, 212, 212)
|
- --dsh-scrollbar-thumb-hover: rgb(212, 212, 212)
|
||||||
- list overflows: true
|
- list overflows: true
|
||||||
- reserved band: 8px
|
- reserved band: 8px
|
||||||
|
- row background inset from the sidebar edge: 12px
|
||||||
- relative time covered by the bar: 0px
|
- relative time covered by the bar: 0px
|
||||||
- relative time ends inside the content area: true
|
- relative time ends inside the content area: true
|
||||||
- content area ends before the border box: true
|
- content area ends before the border box: true
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
- --dsh-scrollbar-thumb-hover: rgb(84, 85, 87)
|
- --dsh-scrollbar-thumb-hover: rgb(84, 85, 87)
|
||||||
- list overflows: true
|
- list overflows: true
|
||||||
- reserved band: 8px
|
- reserved band: 8px
|
||||||
|
- row background inset from the sidebar edge: 12px
|
||||||
- relative time covered by the bar: 0px
|
- relative time covered by the bar: 0px
|
||||||
- relative time ends inside the content area: true
|
- relative time ends inside the content area: true
|
||||||
- content area ends before the border box: true
|
- content area ends before the border box: true
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
mid-slide. */
|
mid-slide. */
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
--dsh-sidebar-inline-padding: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 6px 12px;
|
padding: 6px var(--dsh-sidebar-inline-padding);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--dsw-specific-sidebar-fill);
|
background: var(--dsw-specific-sidebar-fill);
|
||||||
color: var(--dsw-alias-label-primary);
|
color: var(--dsw-alias-label-primary);
|
||||||
@@ -189,14 +190,15 @@
|
|||||||
max-width: 0;
|
max-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Region seat: always mounted so the foot never moves; the browser inside
|
/* Region seat: always mounted so the foot never moves. Its trailing margin
|
||||||
handles its own wide/rail content. */
|
cancels the wide shell inset so the nested scrollbar can sit at the sidebar
|
||||||
|
edge; the browser restores that inset inside its own rows. */
|
||||||
.regionArea {
|
.regionArea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-right: -12px;
|
margin-right: calc(-1 * var(--dsh-sidebar-inline-padding));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
38
packages/client/ui-sidebar/tests/sidebar-styles.spec.ts
Normal file
38
packages/client/ui-sidebar/tests/sidebar-styles.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/** Sidebar shell inset contract shared with the nested workspace browser. */
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
const css = readFileSync(fileURLToPath(new URL('../src/client/SidebarRoot.module.css', import.meta.url)), 'utf8')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declarations of one exact selector, keyed by property.
|
||||||
|
* @param selector - exact selector text.
|
||||||
|
* @returns the normalized declarations, or undefined when absent.
|
||||||
|
*/
|
||||||
|
function declarations(selector: string): Map<string, string> | undefined {
|
||||||
|
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
||||||
|
for (const [, selectorList = '', body = ''] of withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
||||||
|
if (!selectorList.split(',').map(value => value.trim()).includes(selector)) continue
|
||||||
|
const found = new Map<string, string>()
|
||||||
|
for (const part of body.split(';')) {
|
||||||
|
const colon = part.indexOf(':')
|
||||||
|
if (colon === -1) continue
|
||||||
|
found.set(part.slice(0, colon).trim(), part.slice(colon + 1).trim().replace(/\s+/g, ' '))
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SidebarRoot.module.css inset', () => {
|
||||||
|
it('shares and cancels the wide shell trailing padding structurally', () => {
|
||||||
|
const root = declarations('.root')
|
||||||
|
expect(root?.get('--dsh-sidebar-inline-padding')).toBe('12px')
|
||||||
|
expect(root?.get('padding')).toBe('6px var(--dsh-sidebar-inline-padding)')
|
||||||
|
expect(declarations('.regionArea')?.get('margin-right')).toBe(
|
||||||
|
'calc(-1 * var(--dsh-sidebar-inline-padding))',
|
||||||
|
)
|
||||||
|
expect(declarations('.collapsed .regionArea')?.get('margin-right')).toBe('0')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -4,12 +4,14 @@
|
|||||||
rail state renders only the two 36x36 icon controls. */
|
rail state renders only the two 36x36 icon controls. */
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
|
--dsh-session-list-edge-inset: var(--dsh-sidebar-inline-padding);
|
||||||
|
--dsh-session-list-scrollbar-width: 8px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-right: 12px;
|
padding-right: var(--dsh-session-list-edge-inset);
|
||||||
}
|
}
|
||||||
|
|
||||||
.root.rail {
|
.root.rail {
|
||||||
@@ -173,7 +175,7 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-right: -12px;
|
margin-right: calc(-1 * var(--dsh-session-list-edge-inset));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +197,7 @@
|
|||||||
.fade {
|
.fade {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: var(--dsh-session-list-edge-inset);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: 72px;
|
height: 72px;
|
||||||
background: linear-gradient(to bottom, transparent, var(--dsw-specific-sidebar-fill));
|
background: linear-gradient(to bottom, transparent, var(--dsw-specific-sidebar-fill));
|
||||||
@@ -211,27 +213,19 @@
|
|||||||
from { opacity: 0; }
|
from { opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List: the only scrolling region. Block, not a flex column: as flex items
|
/* List: the only scrolling region. Block children keep their design heights
|
||||||
the 54/34 rows would shrink under content overflow; block children keep
|
under content overflow. The stable 8px themed scrollbar and the remaining
|
||||||
their design heights do not shrink under content overflow. Its right
|
padding together equal the shell's right inset, with or without overflow. */
|
||||||
padding is the sole gap between row backgrounds and the sidebar edge; the
|
|
||||||
outer seats move their clip boundary out so they add no hidden inset. */
|
|
||||||
.list {
|
.list {
|
||||||
--dsh-session-list-edge-inset: 12px;
|
|
||||||
--dsh-session-list-scrollbar-width: 8px;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: var(--dsh-session-list-edge-inset);
|
padding-right: calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width));
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list::-webkit-scrollbar {
|
.flatList > * + *,
|
||||||
width: var(--dsh-session-list-scrollbar-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list > [role='treeitem'] + [role='treeitem'],
|
|
||||||
.searchTree > [role='treeitem'] + [role='treeitem'],
|
.searchTree > [role='treeitem'] + [role='treeitem'],
|
||||||
.groupSection > * + * {
|
.groupSection > * + * {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ function FlatList({ useSessions, open, forkSession, onSessionRename, onSessionAr
|
|||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
return (
|
return (
|
||||||
<div className={clsx(css.treeBody, css.wide)}>
|
<div className={clsx(css.treeBody, css.wide)}>
|
||||||
<div className={css.list} role="tree" aria-label={t('section.sessions')}>
|
<div className={clsx(css.list, css.flatList)} role="tree" aria-label={t('section.sessions')}>
|
||||||
{rows.length === 0 && (
|
{rows.length === 0 && (
|
||||||
<div className={css.empty}>{t('empty.none')}</div>
|
<div className={css.empty}>{t('empty.none')}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* WorkspaceBrowser scroll-region style contract, asserted against the CSS text
|
* WorkspaceBrowser spacing contract, asserted against the CSS text on disk:
|
||||||
* on disk: the session list keeps one stable right inset for row hover fills,
|
* row fills share the shell's trailing inset, the stable scrollbar counts
|
||||||
* with or without overflow, while outer clip seats add no hidden second inset.
|
* inside it, and flat, grouped, and search views keep their intended rhythm.
|
||||||
*/
|
*/
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
@@ -10,46 +10,56 @@ import { describe, expect, it } from 'vitest'
|
|||||||
const css = readFileSync(fileURLToPath(new URL('../src/client/WorkspaceBrowser.module.css', import.meta.url)), 'utf8')
|
const css = readFileSync(fileURLToPath(new URL('../src/client/WorkspaceBrowser.module.css', import.meta.url)), 'utf8')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Declarations of one class rule, keyed by property with whitespace collapsed.
|
* Declarations of one selector rule, keyed by property with whitespace collapsed.
|
||||||
* Declaration order and trailing semicolons are normalized away.
|
* Declaration order and trailing semicolons are normalized away.
|
||||||
* @param className - local class name, without the leading dot.
|
* @param selector - one exact selector, including a leading dot for local classes.
|
||||||
* @returns the rule's declarations, or undefined when no such rule exists.
|
* @returns the rule's declarations, or undefined when no such rule exists.
|
||||||
*/
|
*/
|
||||||
function declarations(className: string): Map<string, string> | undefined {
|
function declarations(selector: string): Map<string, string> | undefined {
|
||||||
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
||||||
const match = new RegExp(String.raw`(^|[\s,}])\.${className}\s*\{([^{}]*)\}`).exec(withoutComments)
|
for (const [, selectorList = '', body = ''] of withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
||||||
if (match === null) return undefined
|
if (!selectorList.split(',').map(value => value.trim()).includes(selector)) continue
|
||||||
const found = new Map<string, string>()
|
const found = new Map<string, string>()
|
||||||
// The body group is unconditional in the pattern; the fallback only satisfies
|
for (const part of body.split(';')) {
|
||||||
// noUncheckedIndexedAccess.
|
const colon = part.indexOf(':')
|
||||||
for (const part of (match[2] ?? '').split(';')) {
|
if (colon === -1) continue
|
||||||
const colon = part.indexOf(':')
|
found.set(part.slice(0, colon).trim(), part.slice(colon + 1).trim().replace(/\s+/g, ' '))
|
||||||
if (colon === -1) continue
|
}
|
||||||
found.set(part.slice(0, colon).trim(), part.slice(colon + 1).trim().replace(/\s+/g, ' '))
|
return found
|
||||||
}
|
}
|
||||||
return found
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('WorkspaceBrowser.module.css list', () => {
|
describe('WorkspaceBrowser.module.css list', () => {
|
||||||
const root = declarations('root')
|
const root = declarations('.root')
|
||||||
const listArea = declarations('listArea')
|
const listArea = declarations('.listArea')
|
||||||
const list = declarations('list')
|
const list = declarations('.list')
|
||||||
const treeBody = declarations('treeBody')
|
|
||||||
|
|
||||||
it('is the scrolling region', () => {
|
it('is the scrolling region', () => {
|
||||||
expect(list).toBeDefined()
|
expect(list).toBeDefined()
|
||||||
expect(list!.get('overflow-y')).toBe('auto')
|
expect(list!.get('overflow-y')).toBe('auto')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps row backgrounds edge-flush with the scrolling region', () => {
|
it('counts the themed scrollbar inside the shell trailing inset', () => {
|
||||||
expect(root?.get('padding-right')).toBe('12px')
|
expect(root?.get('--dsh-session-list-edge-inset')).toBe('var(--dsh-sidebar-inline-padding)')
|
||||||
expect(listArea?.get('margin-right')).toBe('-12px')
|
expect(root?.get('--dsh-session-list-scrollbar-width')).toBe('8px')
|
||||||
expect(treeBody?.get('margin-right')).toBeUndefined()
|
expect(root?.get('padding-right')).toBe('var(--dsh-session-list-edge-inset)')
|
||||||
expect(list?.get('margin-right')).toBeUndefined()
|
expect(listArea?.get('margin-right')).toBe('calc(-1 * var(--dsh-session-list-edge-inset))')
|
||||||
expect(list?.get('padding-right')).toBe('var(--dsh-session-list-edge-inset)')
|
expect(declarations('.fade')?.get('right')).toBe('var(--dsh-session-list-edge-inset)')
|
||||||
|
expect(list?.get('padding-right')).toBe(
|
||||||
|
'calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width))',
|
||||||
|
)
|
||||||
|
expect(declarations('.list::-webkit-scrollbar')).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('reserves the scrollbar inside the stable visual inset', () => {
|
it('reserves the scrollbar whether or not the list overflows', () => {
|
||||||
expect(list!.get('scrollbar-gutter')).toBe('stable')
|
expect(list!.get('scrollbar-gutter')).toBe('stable')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps 2px between rows and 4px between workspace groups', () => {
|
||||||
|
expect(declarations('.flatList > * + *')?.get('margin-top')).toBe('2px')
|
||||||
|
expect(declarations(".searchTree > [role='treeitem'] + [role='treeitem']")?.get('margin-top')).toBe('2px')
|
||||||
|
expect(declarations('.groupSection > * + *')?.get('margin-top')).toBe('2px')
|
||||||
|
expect(declarations('.groupSection + .groupSection')?.get('margin-top')).toBe('4px')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user