The history stack has been global since the Android back gesture landed -- every navigation is an entry and `popstate` restores any of them in either direction. What the report describes as "back is tab-scoped" is that the only way back was a detail view's own button, which leaves the screen with the view it belongs to: click over to Tracks and the album you were reading is still one entry away with nothing on screen saying so. `<nav-history>` is that affordance, plus `nav.back` / `nav.forward` on Alt+Left / Alt+Right -- the browser's own combination, and clear of the bare arrows that seek, since a binding matches on its full canonical string. Forward is not back negated, which is why the old `pushedEntries` counter is gone rather than extended: `popstate` carries no direction and fires identically both ways, so one counter decremented on every pop reads a forward as a second back. Each entry carries its index and the shell keeps the current one and a high-water mark, which also survives a jump of more than one. The buttons dispatch the events the rest of the app already dispatches rather than calling `history` themselves -- the shell owns the guard that stops a press at the root leaving the app, and a second caller reaching for history is how the old `navStack` came to disagree with the platform. Below 900px the control stands down: the top bar is what runs out of room first below that, and nothing becomes unreachable -- the shortcuts are global at every width and the phone has the platform's gesture. Closes #6
134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
import { LitElement, html, css } from 'lit';
|
|
import { customElement } from 'lit/decorators.js';
|
|
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
|
import { designTokens } from '../../styles/tokens.css';
|
|
import { HistoryController } from '@store/controllers/history-controller';
|
|
|
|
/**
|
|
* Global back and forward, in the top bar (#6).
|
|
*
|
|
* **The stack was already global; the affordance was not.** Every
|
|
* navigation has been a history entry since the Android back gesture
|
|
* landed, and `popstate` restores any of them in either direction --
|
|
* `back-navigation.spec.ts` has asserted `goForward()` since it was
|
|
* written. What the report describes as "back is tab-scoped" is that
|
|
* the *only* way back was a detail view's own button, which vanishes
|
|
* the moment you leave for another tab: the album you were reading is
|
|
* still one entry away, and nothing on screen says so or offers it.
|
|
*
|
|
* Four things about this are load-bearing.
|
|
*
|
|
* **It asks the shell rather than the History API.** `history.length`
|
|
* counts entries this app did not push and never shrinks, and there is
|
|
* no way to ask where in the list you are -- so a control derived from
|
|
* it is confidently wrong at both ends. `historyStore` is the shell's
|
|
* own numbering.
|
|
*
|
|
* **A control that cannot act is `disabled`, not hidden.** This is the
|
|
* one place in the app where that is right rather than the fault
|
|
* `library-status-indicator` was: back and forward are a *pair* whose
|
|
* positions the user learns, and a button that disappears at the end
|
|
* of the list moves the other one under the cursor. It is also what
|
|
* every browser does, which is the whole design brief here.
|
|
*
|
|
* **The buttons dispatch the events the rest of the app already
|
|
* dispatches**, `navigate-back` and `navigate-forward`, rather than
|
|
* calling `history.back()` themselves. The shell owns the guard -- one
|
|
* press is one entry, and at the root there is nothing of ours to go
|
|
* back to -- and a second caller reaching for `history` directly is
|
|
* how the old `navStack` came to disagree with the platform.
|
|
*
|
|
* **It is desktop chrome.** Below 600px the phone has a system back
|
|
* gesture (and, on Android, a hardware/gesture Back that this app
|
|
* hooks), the top bar is 3.25em with three other things in it, and two
|
|
* more 32px targets there would be the first thing to overflow. Hidden
|
|
* by `index.css` at that width, next to the rest of the phone header's
|
|
* concessions.
|
|
*/
|
|
@customElement('nav-history')
|
|
export class NavHistory extends LitElement {
|
|
private historyCtrl = new HistoryController(this);
|
|
|
|
static override styles = [designTokens, css`
|
|
:host {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25em;
|
|
/* A grid item's implicit minimum is its content; this one
|
|
genuinely cannot shrink, so it says so rather than
|
|
letting the header widen the body. */
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
button {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 2em;
|
|
height: 2em;
|
|
padding: 0;
|
|
border: none;
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
color: var(--yj-text-primary, #f8f9fa);
|
|
cursor: pointer;
|
|
font-size: 1em;
|
|
}
|
|
|
|
button:hover:not(:disabled) {
|
|
background-color: var(--yj-bg-overlay, #495057);
|
|
}
|
|
|
|
button:focus-visible {
|
|
outline: 2px solid var(--yj-accent, #ffd43b);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
button:disabled {
|
|
/* Not a contrast failure: a disabled control is exempt from
|
|
1.4.3, and the pair has to read as unavailable rather
|
|
than merely quiet. */
|
|
color: var(--yj-text-tertiary, #868e96);
|
|
cursor: default;
|
|
}
|
|
`];
|
|
|
|
private go(direction: 'back' | 'forward') {
|
|
this.dispatchEvent(new CustomEvent(`navigate-${direction}`, {
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
}
|
|
|
|
override render() {
|
|
const { canBack, canForward } = this.historyCtrl.depth;
|
|
|
|
return html`
|
|
<button
|
|
type="button"
|
|
data-testid="history-back"
|
|
aria-label="Back"
|
|
?disabled=${!canBack}
|
|
@click=${() => this.go('back')}
|
|
>
|
|
<wa-icon name="arrow-left"></wa-icon>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
data-testid="history-forward"
|
|
aria-label="Forward"
|
|
?disabled=${!canForward}
|
|
@click=${() => this.go('forward')}
|
|
>
|
|
<wa-icon name="arrow-right"></wa-icon>
|
|
</button>
|
|
`;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'nav-history': NavHistory;
|
|
}
|
|
}
|