Drop the phone's top bar; search becomes a button and a modal #167
@@ -162,6 +162,15 @@ export class PageHeader extends LitElement {
|
|||||||
@state()
|
@state()
|
||||||
private collapsed: ReadonlySet<string> = new Set();
|
private collapsed: ReadonlySet<string> = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the count has been given up. Derived, like `collapsed`.
|
||||||
|
*
|
||||||
|
* It is the last thing to yield and the only thing here that is
|
||||||
|
* neither an identity nor an action — see `measureFit`.
|
||||||
|
*/
|
||||||
|
@state()
|
||||||
|
private countCollapsed = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private menuOpen = false;
|
private menuOpen = false;
|
||||||
|
|
||||||
@@ -531,12 +540,6 @@ export class PageHeader extends LitElement {
|
|||||||
|
|
||||||
if (!header) return;
|
if (!header) return;
|
||||||
|
|
||||||
if (this.actions.length === 0) {
|
|
||||||
this.commitCollapsed(new Set());
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttons = new Map<string, HTMLElement>();
|
const buttons = new Map<string, HTMLElement>();
|
||||||
|
|
||||||
for (const el of this.renderRoot.querySelectorAll<HTMLElement>(
|
for (const el of this.renderRoot.querySelectorAll<HTMLElement>(
|
||||||
@@ -549,6 +552,7 @@ export class PageHeader extends LitElement {
|
|||||||
|
|
||||||
const more = this.moreButton;
|
const more = this.moreButton;
|
||||||
const title = this.renderRoot.querySelector('h1');
|
const title = this.renderRoot.querySelector('h1');
|
||||||
|
const count = this.renderRoot.querySelector<HTMLElement>('.count');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nothing is clipped — which is not the same as the header not
|
* Nothing is clipped — which is not the same as the header not
|
||||||
@@ -570,6 +574,8 @@ export class PageHeader extends LitElement {
|
|||||||
|
|
||||||
if (more) more.hidden = true;
|
if (more) more.hidden = true;
|
||||||
|
|
||||||
|
if (count) count.hidden = false;
|
||||||
|
|
||||||
const collapsed = new Set<string>();
|
const collapsed = new Set<string>();
|
||||||
|
|
||||||
if (!fits()) {
|
if (!fits()) {
|
||||||
@@ -586,7 +592,42 @@ export class PageHeader extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.commitCollapsed(collapsed);
|
this.commitCollapsed(collapsed, this.collapseCount(count, fits));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last thing to give way, after every action is in the menu and
|
||||||
|
* the title has already run out.
|
||||||
|
*
|
||||||
|
* There are four things competing for this row and three of them
|
||||||
|
* cannot go. The **title** yields first and is allowed to ellipsis
|
||||||
|
* away entirely at 320px, because the navigation also says which
|
||||||
|
* page you are on. The **sort** control and the **actions** are
|
||||||
|
* each the only place they are said, so an action collapses into
|
||||||
|
* the menu rather than disappearing and the sort control stays.
|
||||||
|
* That leaves the **count**, which is the one purely informational
|
||||||
|
* item on the row — an empty page says so in its empty state, and a
|
||||||
|
* full one is being looked at.
|
||||||
|
*
|
||||||
|
* It became reachable rather than theoretical with #57: below 600px
|
||||||
|
* the header also carries the phone's search button, and on
|
||||||
|
* Playlists at 320px that is 43px more than the row has. Measured
|
||||||
|
* there: title 0, count 50, sort 143, search 40, "More actions" 38,
|
||||||
|
* five 12px gaps and 32px of gutters — 363 in 320, with the More
|
||||||
|
* button ending 27px past the edge. Something has to go, and this
|
||||||
|
* is the only candidate that is not an action.
|
||||||
|
*
|
||||||
|
* @returns whether the count was given up.
|
||||||
|
*/
|
||||||
|
private collapseCount(
|
||||||
|
count: HTMLElement | null,
|
||||||
|
fits: () => boolean,
|
||||||
|
): boolean {
|
||||||
|
if (count === null || fits()) return false;
|
||||||
|
|
||||||
|
count.hidden = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Lowest priority first; ties broken from the right. */
|
/** Lowest priority first; ties broken from the right. */
|
||||||
@@ -601,7 +642,9 @@ export class PageHeader extends LitElement {
|
|||||||
.map(({ action }) => action);
|
.map(({ action }) => action);
|
||||||
}
|
}
|
||||||
|
|
||||||
private commitCollapsed(next: Set<string>): void {
|
private commitCollapsed(next: Set<string>, countHidden: boolean): void {
|
||||||
|
this.countCollapsed = countHidden;
|
||||||
|
|
||||||
const same =
|
const same =
|
||||||
next.size === this.collapsed.size &&
|
next.size === this.collapsed.size &&
|
||||||
[...next].every((id) => this.collapsed.has(id));
|
[...next].every((id) => this.collapsed.has(id));
|
||||||
@@ -769,7 +812,16 @@ export class PageHeader extends LitElement {
|
|||||||
|
|
||||||
const noun = this.count === 1 ? this.countNoun : plural;
|
const noun = this.count === 1 ? this.countNoun : plural;
|
||||||
|
|
||||||
return html`<span class="count" data-testid="page-count"
|
// Rendered whether or not it fits, and hidden with an
|
||||||
|
// attribute -- the same shape the action buttons use, and for
|
||||||
|
// the same reason: `measureFit` starts every pass from
|
||||||
|
// all-visible, so it needs a node to un-hide. Returning
|
||||||
|
// `nothing` here would take the count away for the rest of the
|
||||||
|
// session the first time a 320px window appeared.
|
||||||
|
return html`<span
|
||||||
|
class="count"
|
||||||
|
data-testid="page-count"
|
||||||
|
?hidden=${this.countCollapsed}
|
||||||
>${this.count.toLocaleString()} ${noun}</span
|
>${this.count.toLocaleString()} ${noun}</span
|
||||||
>`;
|
>`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user