Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 82 additions & 2 deletions ui-web/src/chrome/Dock.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
* data-* cannot see: the roles and flags that are not data-*, the elements
* handed over empty because another writer owns them, that the literals come
* from the catalogue rather than from a copy in the JSX, the two contracts the
* field carries -- an IME's Enter, the palette's Escape -- and where the three
* popovers off the bar stand.
* field carries -- an IME's Enter, the palette's Escape -- what keeps the bar
* under the field to one row, and where the three popovers off the bar stand.
*/
// @ts-expect-error Vitest provides Node built-ins without adding Node types to the browser bundle.
import { readFileSync } from 'node:fs'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

Expand Down Expand Up @@ -416,6 +418,84 @@ describe('the dock once a language is applied', () => {
})
})

/* The bar under the field keeps to one row at every width the document allows.
*
* That is the stylesheet's to hold, not the markup's. A flex row that may wrap
* picks its breaks from its items' whole widths before any of them shrinks, so
* a second line is the only answer such a row has to a narrow column -- and the
* send button, last in the row, is what lands on it. So the row never wraps,
* and the items whose width is their text give instead: the folder, the mode,
* the model and the meter.
*
* happy-dom lays nothing out, so these cases read what page.css cascades onto
* the bar as rendered: the mechanism, not the geometry. Only a browser can show
* the row itself.
*/
describe('the bar under the field', () => {
let sheet: HTMLStyleElement | null = null

function styled(): void {
render()
sheet = document.createElement('style')
sheet.textContent = readFileSync('src/styles/page.css', 'utf8') as string
document.head.append(sheet)
}

afterEach(() => {
sheet?.remove()
sheet = null
})

const style = (n: Element): CSSStyleDeclaration => getComputedStyle(n)
const bar = (): Element => document.querySelector('.dock-in > .under') as Element

/* Every box from a label up to the bar: each is as wide as the label until
it is told it may be less. */
function upTo(id: string): Element[] {
const out: Element[] = []
for (let n: Element | null = el(id); n && n !== bar(); n = n.parentElement) out.push(n)
return out
}

it('never wraps', () => {
styled()
expect(style(bar()).flexWrap).not.toBe('wrap')
})

it('lets every box between a label and the bar give, and ends the label in an ellipsis', () => {
styled()
for (const id of ['wdName', 'permName', 'modelName', 'meter']) {
for (const n of upTo(id)) {
const at = `${id} at ${n.id || n.className}`
expect(style(n).minWidth, at).toMatch(/^0(px)?$/)
expect(style(n).flexShrink, at).not.toBe('0')
}
const label = style(el(id))
expect([label.overflow, label.textOverflow, label.whiteSpace], id).toEqual(['hidden', 'ellipsis', 'nowrap'])
}
})

/* Shrinking in proportion to width takes the most pixels from the widest box,
and a chip is mostly its icon and its padding: a short model name beside a
long folder went to nothing, and its caret out of the chip. So each takes an
equal share of what the fixed controls leave and never more than its own
width -- a short name stays whole while a long one gives. */
it('shares what the controls leave equally, each at most its own width', () => {
styled()
for (const n of [anchor('wdChip'), anchor('permChip'), el('modelChip'), el('meter')]) {
const at = n.id || (n.firstElementChild as Element).id
expect([style(n).flexGrow, style(n).maxWidth], at).toEqual(['1', 'max-content'])
expect(style(n).flexBasis, at).toMatch(/^0(px|%)?$/)
}
})

it('keeps the controls that are not text at their own size', () => {
styled()
for (const n of [el('plusBtn'), el('ctxChip'), el('go')]) expect(style(n).flexShrink, n.id).toBe('0')
expect(style(anchor('plusBtn')).minWidth).not.toMatch(/^0/)
})
})

/* The three popovers off the bar, and the one thing about them a golden of
* the band cannot see: where each stands.
*
Expand Down
20 changes: 17 additions & 3 deletions ui-web/src/styles/page.css
Original file line number Diff line number Diff line change
Expand Up @@ -2451,10 +2451,24 @@ textarea:focus, textarea:focus-visible,
@keyframes go-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .go[data-state="sending"] svg { animation-duration: 2.4s; } }

/* wraps instead of pushing the usage line past the edge on a narrow window */
/* Pulled left of the field it sits under, so the first control's own padding
lines its glyph up with the text above rather than with its box. */
.under { display: flex; align-items: center; gap: 4px; margin: 0; padding: 16px 16px 16px 8px; flex-wrap: wrap; }
lines its glyph up with the text above rather than with its box.

One row at every width the document allows, never wrapped: a row that may
wrap picks its breaks from its items' whole widths before any of them
shrinks, so the send button, last in the row, is what a narrow column would
drop onto a line of its own. The items whose width is their text -- the
folder, the mode, the meter and the model -- give instead, each taking an
equal share of what the fixed controls leave and never more than its own
width, so a short name stays whole while a long one ends in an ellipsis. */
.under { display: flex; align-items: center; gap: 4px; margin: 0; padding: 16px 16px 16px 8px; }
.under > .chrome-anch:has(> .chip), .under > .chip.model, .under > .meter {
flex: 1 1 0; min-width: 0; max-width: max-content;
}
/* The chip gives with the anchor it hangs in, and only its name clips: the
anchor must not, because its popover stands outside the anchor's box. */
.under > .chrome-anch > .chip { flex-shrink: 1; min-width: 0; }
#wdName, #permName, #modelName { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The "+" (src/chrome/PlusMenu.tsx): a 30px round target; open, its cross turns
into a close mark. */
.plus { flex: none; width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center; color: var(--rail-ink); }
Expand Down
Loading