fix(host): review round 15 — lexical home normalization; webkit scrollbar hide; flex-none check; inert enumeration
This commit is contained in:
@@ -151,6 +151,13 @@
|
|||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Engines that predate scrollbar-width take the pseudo-element path (the
|
||||||
|
* two are mutually exclusive by construction — see ui-theme's scrollbar
|
||||||
|
* contract); hide the row's horizontal bar there too. */
|
||||||
|
.millerRow::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Columns split the row evenly around the divider (a solo column takes the
|
/* Columns split the row evenly around the divider (a solo column takes the
|
||||||
* whole row); 256px is the floor below which the row scrolls (scrollbar
|
* whole row); 256px is the floor below which the row scrolls (scrollbar
|
||||||
* hidden, the effect pins the child pane into view) instead of squeezing
|
* hidden, the effect pins the child pane into view) instead of squeezing
|
||||||
@@ -292,6 +299,13 @@
|
|||||||
color: var(--dsw-alias-label-primary);
|
color: var(--dsw-alias-label-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Trailing pressed check (Menu's .check parallel): flex-none so wrap or
|
||||||
|
* narrow-viewport clamp pressure never squashes the glyph — the nowrap
|
||||||
|
* label refuses to shrink, leaving the icon as the only compressible item. */
|
||||||
|
.toggleCheck {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
.footerGap {
|
.footerGap {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,30 +73,46 @@ function foldPathFor(sep: '\\' | '/'): (value: string) => string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drops every trailing separator (`HOME=/home/u//` ships verbatim while
|
* Lexically normalizes an absolute host path for comparisons: collapses
|
||||||
* resolve() strips them) down to, but never past, one leading character —
|
* repeated and trailing separators, drops `.` segments, and applies `..` —
|
||||||
* which keeps the POSIX root `/` intact. A backslash drive root (`C:\`)
|
* mirroring the backend's resolve() for the shapes an environment-supplied
|
||||||
* does lose its separator; that stays safe only because every comparison
|
* HOME legally carries verbatim (`/home/u/`, `/home//u`, `/home/u/.`)
|
||||||
* trims both sides symmetrically.
|
* while the backend's paths arrive already resolved. A lexical mirror
|
||||||
|
* only: symlinks are the backend's business, and the input always
|
||||||
|
* contains the separator (it is an absolute path).
|
||||||
*/
|
*/
|
||||||
function trimTrailingSeparator(path: string, sep: '\\' | '/'): string {
|
function normalizePathFor(sep: '\\' | '/'): (value: string) => string {
|
||||||
let end = path.length
|
return (value) => {
|
||||||
while (end > sep.length && path.endsWith(sep, end)) end -= sep.length
|
const segments = value.split(sep)
|
||||||
return path.slice(0, end)
|
const head = segments.shift()
|
||||||
|
/* v8 ignore next -- narrowing guard: split always yields at least one segment. */
|
||||||
|
if (head === undefined) return value
|
||||||
|
const out: string[] = []
|
||||||
|
for (const segment of segments) {
|
||||||
|
if (segment === '' || segment === '.') continue
|
||||||
|
if (segment === '..') {
|
||||||
|
out.pop()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.push(segment)
|
||||||
|
}
|
||||||
|
return `${head}${sep}${out.join(sep)}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Breadcrumb rows for display: inside the home subtree the chain starts at a
|
* Breadcrumb rows for display: inside the home subtree the chain starts at a
|
||||||
* localized Home crumb; outside it the full ancestry shows, the root labeled
|
* localized Home crumb; outside it the full ancestry shows, the root labeled
|
||||||
* by its own path. The home comparison folds per platform and normalizes a
|
* by its own path. The home comparison folds per platform and lexically
|
||||||
* trailing separator, so a typed-case Windows path or a `HOME=/home/u/`
|
* normalizes both sides, so a typed-case Windows path or a `HOME=/home/u/.`
|
||||||
* shape still collapses to the Home crumb.
|
* shape still collapses to the Home crumb.
|
||||||
*/
|
*/
|
||||||
function displayCrumbs(listing: DirectoryListing, homeLabel: string): DirectoryEntry[] {
|
function displayCrumbs(listing: DirectoryListing, homeLabel: string): DirectoryEntry[] {
|
||||||
const sep = separatorOf(listing)
|
const sep = separatorOf(listing)
|
||||||
const fold = foldPathFor(sep)
|
const fold = foldPathFor(sep)
|
||||||
const home = fold(trimTrailingSeparator(listing.home, sep))
|
const normalize = normalizePathFor(sep)
|
||||||
const homeIndex = listing.crumbs.findIndex(crumb => fold(trimTrailingSeparator(crumb.path, sep)) === home)
|
const home = fold(normalize(listing.home))
|
||||||
|
const homeIndex = listing.crumbs.findIndex(crumb => fold(normalize(crumb.path)) === home)
|
||||||
if (homeIndex === -1) return listing.crumbs
|
if (homeIndex === -1) return listing.crumbs
|
||||||
const tail = listing.crumbs.slice(homeIndex + 1)
|
const tail = listing.crumbs.slice(homeIndex + 1)
|
||||||
return [{ name: homeLabel, path: listing.home, hidden: false }, ...tail]
|
return [{ name: homeLabel, path: listing.home, hidden: false }, ...tail]
|
||||||
@@ -800,7 +816,7 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
|
|||||||
onClick={() => { setShowHidden(prev => !prev) }}
|
onClick={() => { setShowHidden(prev => !prev) }}
|
||||||
>
|
>
|
||||||
{t('browser.showHidden')}
|
{t('browser.showHidden')}
|
||||||
{showHidden && <IconCheckOutline16 size={14} />}
|
{showHidden && <IconCheckOutline16 size={14} className={css.toggleCheck} />}
|
||||||
</button>
|
</button>
|
||||||
<span className={css.footerGap} />
|
<span className={css.footerGap} />
|
||||||
<Button variant="outline" className={clsx(css.footerAction)} disabled={parentInert} onClick={onClose}>{t('browser.cancel')}</Button>
|
<Button variant="outline" className={clsx(css.footerAction)} disabled={parentInert} onClick={onClose}>{t('browser.cancel')}</Button>
|
||||||
|
|||||||
@@ -349,6 +349,16 @@ describe('DirectoryBrowser', () => {
|
|||||||
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'browser.editPath' }))
|
expect(document.activeElement).toBe(screen.getByRole('button', { name: 'browser.editPath' }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('a home carrying dot segments is still the display root', async () => {
|
||||||
|
// os.homedir() ships HOME verbatim; the backend resolves listing paths.
|
||||||
|
const listDirectory = vi.fn(async (path?: string) => ({ ...listingFor(path), home: `${HOME}/foo/../.` }))
|
||||||
|
mount({ listDirectory })
|
||||||
|
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
||||||
|
expect(columns()).toHaveLength(1)
|
||||||
|
expect(screen.getByRole('button', { name: 'browser.home' })).toBeTruthy()
|
||||||
|
expect(listDirectory).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('a navigation to the filesystem root keeps the single wide level', async () => {
|
it('a navigation to the filesystem root keeps the single wide level', async () => {
|
||||||
mount()
|
mount()
|
||||||
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
||||||
@@ -1176,6 +1186,7 @@ describe('DirectoryBrowser', () => {
|
|||||||
expect(cancels.map(button => button.disabled).sort()).toEqual([false, true])
|
expect(cancels.map(button => button.disabled).sort()).toEqual([false, true])
|
||||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'browser.open' }).disabled).toBe(true)
|
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'browser.open' }).disabled).toBe(true)
|
||||||
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'browser.editPath' }).disabled).toBe(true)
|
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'browser.editPath' }).disabled).toBe(true)
|
||||||
|
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'browser.showHidden' }).disabled).toBe(true)
|
||||||
for (const row of screen.getAllByRole('listitem')) {
|
for (const row of screen.getAllByRole('listitem')) {
|
||||||
expect(rowButton(row).disabled).toBe(true)
|
expect(rowButton(row).disabled).toBe(true)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user