Android: raise the page header and the phone search button to the touch floor #195

Merged
logan merged 2 commits from 186-touch-targets-page-header into main 2026-08-22 00:32:08 +00:00
3 changed files with 86 additions and 9 deletions
Showing only changes of commit 0d331666d6 - Show all commits
@@ -320,10 +320,25 @@ export class PageHeader extends LitElement {
query that no tier here renders, which is exactly how
the seek bar's phone rule came to be dead for months.
Only the *width* of this reaches the overflow fit below:
that pass measures inline size, so the height costs it
nothing, and the two square controls grow the header's
content by 22px in total. */
**The height is the box and the width is not**, and that
asymmetry is the whole of what the overflow fit below
cares about. That pass measures inline size, so a taller
control costs it nothing and a wider one costs it
directly. Growing the two square controls to 44px wide
added 22px, which fits at every width Chromium was
checked at and clipped the overflow trigger at 320px in
**WebKit** -- the engine closest to what actually ships,
and the one no machine here can run. So the horizontal
half is padding with the margin cancelling it, which is
what the issue asked for in the first place: the target
grows and the layout does not.
The cost is that a horizontal target can now overlap a
neighbour, which the box version could not. The arrow's
is deliberately lopsided for the seek bar's reason
(#187): the select is 6px to its left and there is open
space to its right, so it takes the side with nothing to
steal from. */
.sort select {
font: inherit;
color: inherit;
@@ -347,9 +362,16 @@ export class PageHeader extends LitElement {
padding: 3px 5px;
/* 28x21 before this, the smallest control in the
header and the only one that failed the floor in
both directions. */
min-inline-size: 44px;
both directions.
Vertically the box grows, because the header has the
room and nothing measures it. Horizontally the box
must not: 28 + 2 + 14 is a 44px target over a 28px
layout box, weighted right because the select is 6px
to the left. */
min-block-size: 44px;
padding-inline: 5px 21px;
margin-inline: 0 -16px;
}
.sort-dir:hover {
@@ -417,8 +439,12 @@ export class PageHeader extends LitElement {
padding: 6px 10px;
/* 38x27, and it is the route to every collapsed
action, so it is the last control that should be
hard to hit. */
min-inline-size: 44px;
hard to hit -- and the one WebKit clipped at 320px
when this was 6px wider as a box. 38 + 3 + 3 is a
44px target over a 38px layout box; the actions row
has an 8px gap, so this one can be symmetric. */
padding-inline: 13px;
margin-inline: -3px;
}
/* The display: flex above outranks the UA stylesheet's
@@ -73,9 +73,20 @@ export class SearchTrigger extends LitElement {
second opinion about it (#186). The rest of that
comment said the header's own action buttons are
smaller because they carry a label; they are 44px
now too, so that no longer distinguishes anything. */
now too, so that no longer distinguishes anything.
The extra width is a target rather than a box, for
page-header's reason: this button sits in that
header, whose overflow fit (#69) measures inline
size, and four pixels there is four pixels the
trigger for every collapsed action does not get at
320px. Height is free -- nothing measures it. */
min-width: 44px;
min-height: 44px;
/* Border-box, so the 44 above is the whole target and
the margin is what hands the four extra pixels back
to the row. */
margin-inline: -2px;
padding: 0;
background: none;
border: 1px solid var(--yj-border-subtle, #555);
@@ -90,6 +90,46 @@ describe("the page header's controls", () => {
expect(tooSmall(controls)).toEqual([]);
});
it('grows the target without growing the box, so the overflow fit is untouched', async () => {
// The regression this exists for, and it was a real one: growing
// the two square controls to 44px *wide* added 22px to the header,
// which fit at every width Chromium was checked at and clipped the
// overflow trigger at 320x600 in WebKit -- the engine closest to
// what ships, and the one no machine here can run. #69's fit pass
// measures inline size, so a taller control is free and a wider one
// is not.
//
// Negative inline margins are what keep the box out of it: the
// padding makes the target, and the margin gives the space back.
const el = await fixture<PageHeader>('page-header', {
heading: 'Playlists',
sortOptions: SORTS,
sortField: 'name',
actions: actions(),
});
el.style.width = '320px';
for (let frame = 0; frame < 3; frame += 1) {
await new Promise((r) => requestAnimationFrame(r));
await el.updateComplete;
}
for (const selector of ['.sort-dir', '.more-button']) {
const control = shadowAll<HTMLElement>(el, selector).filter(
(c) => !(c as HTMLButtonElement).hidden,
)[0];
expect(control, selector).toBeTruthy();
const style = getComputedStyle(control!);
const added =
parseFloat(style.marginInlineStart) + parseFloat(style.marginInlineEnd);
expect(added, `${selector} gives its extra width back`).toBeLessThan(0);
}
});
it('includes the overflow trigger, which is the route to the rest', async () => {
// At 320px the fit pass collapses actions into the menu, so the
// trigger is rendered — and it is then the only way to reach them,