feat(shell): global back and forward in the top bar
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
This commit is contained in:
@@ -8,6 +8,7 @@ import { describe, expect, it, beforeEach } from 'vitest';
|
||||
|
||||
import { searchStore } from '@store/search-store';
|
||||
import { activeViewStore } from '@store/active-view-store';
|
||||
import { historyStore } from '@store/history-store';
|
||||
import { trackListStore } from '@store/tracklist-store';
|
||||
import { exploreCache, ARTIST_IMAGE_CACHE_LIMIT } from '@store/explore-cache';
|
||||
import { Events } from '../../src/events';
|
||||
@@ -133,6 +134,46 @@ describe('active view store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('history store', () => {
|
||||
beforeEach(() => {
|
||||
historyStore.setDepth(false, false);
|
||||
});
|
||||
|
||||
it('holds both answers, because forward is not back negated', () => {
|
||||
historyStore.setDepth(true, false);
|
||||
|
||||
expect(historyStore.get()).toEqual({ canBack: true, canForward: false });
|
||||
|
||||
// The middle of the list: both directions available at once, which
|
||||
// a single depth counter cannot express and which is the state the
|
||||
// old `pushedEntries` got wrong.
|
||||
historyStore.setDepth(true, true);
|
||||
|
||||
expect(historyStore.get()).toEqual({ canBack: true, canForward: true });
|
||||
});
|
||||
|
||||
it('does not notify when neither answer changed', () => {
|
||||
let notifications = 0;
|
||||
const off = historyStore.subscribe(() => {
|
||||
notifications += 1;
|
||||
});
|
||||
|
||||
historyStore.setDepth(true, true);
|
||||
historyStore.setDepth(true, true);
|
||||
off();
|
||||
|
||||
expect(notifications).toBe(1);
|
||||
});
|
||||
|
||||
it('starts with both unavailable, which is the truth at launch', () => {
|
||||
// A fresh session is one entry deep and that entry is *replaced*,
|
||||
// not pushed, so there is nothing of ours behind it. A control
|
||||
// that assumed otherwise would offer a press that does nothing --
|
||||
// and on Android, one the OS would have used to exit the app.
|
||||
expect(historyStore.get()).toEqual({ canBack: false, canForward: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('track list store', () => {
|
||||
it('starts from the default column set', () => {
|
||||
expect(trackListStore.getState().columnIds.length).toBeGreaterThan(0);
|
||||
|
||||
Reference in New Issue
Block a user