The nav components learned where the user was from the `navigate` CustomEvent, which only the outbound path dispatches: `popstate` calls `handleNavigate()` directly. So a back-navigation left both of them highlighting the view just left — desktop included, at any width, on any back across two primary views. Opening a detail view was the same cause wearing a different symptom: `app-sidebar` guarded on its own item list and kept its highlight, `bottom-nav` did not and lit nothing. It cannot be fixed by re-dispatching `navigate` — `index.ts` is that event's document listener, so that is an infinite loop, and "please go to X" is not the statement being made. `activeViewStore` is the shell saying "the active view is now X", once per navigation, `popstate` included; both navs read it through a controller and hold no `activeView` of their own. A store rather than an event because a component that mounts *after* a navigation still has to know: `bottom-nav`'s drawer builds its `app-sidebar` on open, and that copy had heard nothing at all, so the drawer opened on Home from any page in the app. Closes #72
91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
/**
|
|
* Which primary view the app is showing.
|
|
*
|
|
* The shell has always known this -- `handleNavigate()` sets
|
|
* `#main-content`'s `data-active-view` on every path, `_isBack`
|
|
* included -- and never told anyone. The nav components learned it
|
|
* from the `navigate` CustomEvent instead, which only the *outbound*
|
|
* path dispatches: the `popstate` listener calls `handleNavigate()`
|
|
* directly. So both navs kept highlighting the view you had just left
|
|
* (#72).
|
|
*
|
|
* The fix cannot be a re-dispatch of `navigate`. `index.ts` is itself a
|
|
* document listener for it, so emitting one from inside
|
|
* `handleNavigate` is an infinite loop -- and the two statements are
|
|
* different anyway: `navigate` means *please go to X*, and 28 call
|
|
* sites across 18 files say it. This says *the active view is now X*,
|
|
* which only the shell is in a position to say and only once per
|
|
* navigation.
|
|
*
|
|
* Three things about it are load-bearing.
|
|
*
|
|
* **It is a store rather than an event**, because a component that
|
|
* mounts *after* a navigation still has to know. `bottom-nav`'s "More"
|
|
* drawer creates its `<app-sidebar>` on open, and that copy had heard
|
|
* no `navigate` at all: standing on Albums, the drawer highlighted
|
|
* Home -- its `activeView` default, which existed to match the landing
|
|
* view and matched nothing else ever after. An event has no answer for
|
|
* a listener that was not there; a value does.
|
|
*
|
|
* **A detail view is not a view here.** Opening one leaves the primary
|
|
* view it was opened from lit, which is what #72 asks for and what
|
|
* `app-sidebar` used to do by accident -- it guarded on
|
|
* `navItems.some(...)`, so a name matching no item left its highlight
|
|
* alone. `bottom-nav` had no such guard and so lit nothing on a detail
|
|
* view. Neither was correct; the sidebar was stale-but-lucky, and
|
|
* stating the rule once is what makes the two agree.
|
|
*
|
|
* **Whether a view is primary is the shell's fact, not this store's.**
|
|
* `VIEW_TAGS` in `index.ts` is the list, and a copy of it here is a
|
|
* second list to forget -- so the caller passes the answer it already
|
|
* has rather than this file re-deriving it.
|
|
*/
|
|
|
|
type Subscriber = () => void;
|
|
|
|
class ActiveViewStore {
|
|
/** Empty until the shell's first navigation, which happens at
|
|
* startup from `GetDefaultPage()`. Nothing is highlighted for that
|
|
* moment, which is honest: the alternative is a written-down
|
|
* default that is right only when the default page agrees with it. */
|
|
private activeView = '';
|
|
|
|
private subscribers = new Set<Subscriber>();
|
|
|
|
/** The active primary view, e.g. `albums`. */
|
|
get(): string {
|
|
return this.activeView;
|
|
}
|
|
|
|
isActive(view: string): boolean {
|
|
return this.activeView !== '' && this.activeView === view;
|
|
}
|
|
|
|
/**
|
|
* Called by the shell on every navigation, `popstate` included.
|
|
*
|
|
* `isPrimary` is `view in VIEW_TAGS` at the call site: a detail
|
|
* view reports itself and deliberately changes nothing, so the view
|
|
* it was opened from stays lit until the user picks another one.
|
|
*/
|
|
setView(view: string, isPrimary: boolean): void {
|
|
if (!isPrimary) return;
|
|
if (view === this.activeView) return;
|
|
|
|
this.activeView = view;
|
|
this.notify();
|
|
}
|
|
|
|
subscribe(fn: Subscriber): () => void {
|
|
this.subscribers.add(fn);
|
|
|
|
return () => this.subscribers.delete(fn);
|
|
}
|
|
|
|
private notify(): void {
|
|
this.subscribers.forEach((fn) => fn());
|
|
}
|
|
}
|
|
|
|
export const activeViewStore = new ActiveViewStore();
|