mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
4dfccaa2a4
From the large-screen review (code findings H1, M1, M2, M3, M4, M7, L1, L3, L4, L6, L7): - The size class is one value in JavaScript (hooks/sizeClass.ts) and the stylesheet follows it: MobileShell writes it to the shell as data-size, the wide:/split: variants and mobile.css key on that instead of media queries of their own. A window that only grows shorter at the same width while a field has focus is Android's soft keyboard, not a new window, so the rail, the form sheets and the panes no longer fold away (and take the focused field with them) while one types. - The large-screen layout is the app's only. In a browser the touch shell is the phone's below 768px and stays compact there, as before this branch. - Shortcuts are the app's only, so a browser keeps Ctrl+1 and Alt+Left. The command key is the platform's own (Cmd on Apple, Ctrl elsewhere), digits are matched by the key's place as well (AZERTY), and nothing runs while a sheet or another modal dialog is open: the page listens to nothing and the app is handed no key commands, so Cmd+[ can no longer drop an unsaved form. - Inside a trip Cmd+N belongs to the trip shell and creates what the tab on show creates, named after it. - Inside the touch shell the desktop navbar of the few PageShell routes is hidden and --nav-h is 0. - The rail comes before the page in the DOM, so a keyboard reaches it first. - The phone's deep day colour for a journey card without a photo stays the phone's; the desktop and shared timelines keep their faint wash. Its card is opaque, a suggestion included, so the map's names do not read through. - The trip widget's iPad mini layout keys on the iPad mini's width alone. - The trip dock gets the scroll-edge strip and the denser dock glass the global dock has. - Duplicate test id FE-MOB-PANES-016 renumbered.
64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
import { useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';
|
|
|
|
/**
|
|
* The overlays open on screen, newest last, so a way back closes the one on
|
|
* top before it leaves the page: the iOS edge swipe (MEdgeBack) and Android's
|
|
* back gesture (native/nativeApp.ts) both ask here first. Each entry is the
|
|
* overlay's own close path, so an unsaved-changes question still asks.
|
|
*/
|
|
type Close = () => void;
|
|
const stack: { id: number; close: () => void }[] = [];
|
|
let nextId = 1;
|
|
const listeners = new Set<() => void>();
|
|
|
|
function changed(): void {
|
|
for (const listener of listeners) listener();
|
|
}
|
|
|
|
function subscribe(listener: () => void): () => void {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
}
|
|
|
|
/** Closes the overlay on top. False when none is open, so the caller goes back instead. */
|
|
export function closeTopOverlay(): boolean {
|
|
const top = stack[stack.length - 1];
|
|
if (!top) return false;
|
|
top.close();
|
|
return true;
|
|
}
|
|
|
|
export function hasOpenOverlay(): boolean {
|
|
return stack.length > 0;
|
|
}
|
|
|
|
/** Whether an overlay is open, for a component that changes with it (the keyboard shortcuts). */
|
|
export function useHasOpenOverlay(): boolean {
|
|
return useSyncExternalStore(subscribe, hasOpenOverlay, () => false);
|
|
}
|
|
|
|
/** Registers an overlay while `open`, with the way it closes. */
|
|
export function useOverlay(open: boolean, close: Close): void {
|
|
const closeRef = useRef(close);
|
|
useLayoutEffect(() => {
|
|
closeRef.current = close;
|
|
});
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const entry = { id: nextId++, close: () => closeRef.current() };
|
|
stack.push(entry);
|
|
changed();
|
|
return () => {
|
|
const i = stack.findIndex((e) => e.id === entry.id);
|
|
if (i >= 0) stack.splice(i, 1);
|
|
changed();
|
|
};
|
|
}, [open]);
|
|
}
|
|
|
|
/** Test helper. */
|
|
export function _resetOverlayStack(): void {
|
|
stack.length = 0;
|
|
changed();
|
|
}
|