Make the mobile nav sheet swipe-to-close instead of fighting pull-to-refresh #13
5 changed files with 129 additions and 3 deletions
|
|
@ -358,6 +358,9 @@ Then put it behind your existing reverse proxy like everything else
|
|||
movie-poster tiles — circular artist tiles, square album tiles,
|
||||
a real hero header, and a mobile "now playing" full-screen player
|
||||
— see docs/architecture.md
|
||||
- [x] Mobile menu's bottom sheet is now swipe-to-close (a real drag
|
||||
handle, not just a decorative grabber) instead of fighting the
|
||||
browser's own pull-to-refresh gesture — see docs/architecture.md
|
||||
- [ ] Try a full party end-to-end with two real accounts (still the main gap)
|
||||
- [ ] Try casting to a real Chromecast (only the SDK's own bootstrap is
|
||||
confirmed here — no real device/session)
|
||||
|
|
|
|||
|
|
@ -121,6 +121,12 @@
|
|||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity .2s;
|
||||
/* A touch starting here has no scrollable content of its own to
|
||||
"contain" (overscroll-behavior wouldn't apply) - this is what
|
||||
stops the browser reading a drag over the dimmed backdrop as a
|
||||
page-level gesture (pull-to-refresh) instead of just dismissing
|
||||
the sheet via the existing tap-to-close below. */
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.mobileNavScrim.open { opacity: 1; pointer-events: auto; }
|
||||
|
|
@ -133,6 +139,12 @@
|
|||
z-index: 21;
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
/* Stops a drag that hits the top of the sheet's OWN scroll (its
|
||||
library grid can be taller than 75vh) from chaining past this
|
||||
element to the page underneath and surfacing as pull-to-refresh -
|
||||
complements touch-action:none on the dedicated grabber handle
|
||||
below, which instead covers a drag that never scrolls anything. */
|
||||
overscroll-behavior: contain;
|
||||
background: var(--bg-header);
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 8px 16px calc(20px + env(safe-area-inset-bottom, 0px));
|
||||
|
|
@ -143,12 +155,27 @@
|
|||
|
||||
.mobileNavSheet.open { transform: translateY(0); }
|
||||
|
||||
/* Dragging (see Shell.ts) suspends the transition above for 1:1
|
||||
tracking under the finger, then this class restores it for the
|
||||
snap-back/close animation - same "instant while dragging, animated
|
||||
on release" split views/player/Player.css's own seek bar uses. */
|
||||
.mobileNavSheet.dragging { transition: none; }
|
||||
|
||||
/* Bigger drag/touch target than the pill it contains (36x4px is far
|
||||
below a real touch-target minimum) - see Shell.html/Shell.ts. */
|
||||
.sheetGrabberHandle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 14px 0 10px;
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.sheetGrabber {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
border-radius: 99px;
|
||||
background: var(--muted);
|
||||
margin: 10px auto 6px;
|
||||
}
|
||||
|
||||
/* Accueil and the (conditional) Listes de lecture row both use this -
|
||||
|
|
|
|||
|
|
@ -32,7 +32,14 @@
|
|||
|
||||
<div class="mobileNavScrim" id="mobileNavScrim"></div>
|
||||
<div class="mobileNavSheet" id="mobileNavSheet">
|
||||
<div class="sheetGrabber"></div>
|
||||
<!-- Bigger invisible drag target than the visible pill itself (see
|
||||
.sheetGrabberHandle) - touch-action:none so this app's own
|
||||
pointer handlers (Shell.ts) drive the drag instead of the
|
||||
browser's default touch handling, which otherwise reads a
|
||||
downward drag here as pull-to-refresh (the sheet sits right at
|
||||
the top of the page's own scroll position, same conflict a
|
||||
plain scrim tap-only close used to leave unsolved). -->
|
||||
<div class="sheetGrabberHandle" id="sheetGrabberHandle"><div class="sheetGrabber"></div></div>
|
||||
<a href="#/home" class="navItem sheetPrimary"><span class="navIcon" data-icon="home"></span>Accueil</a>
|
||||
<div class="navSectionLabel">Bibliothèques</div>
|
||||
<div class="sheetGrid" id="mobileLibGrid"></div>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export async function renderShell(appEl: HTMLElement): Promise<HTMLElement> {
|
|||
|
||||
// ---- Mobile bottom sheet - see Shell.css's "Mobile navigation". ----
|
||||
const navTrigger = appEl.querySelector('#mobileNavTrigger') as HTMLButtonElement;
|
||||
const navSheet = appEl.querySelector('#mobileNavSheet')!;
|
||||
const navSheet = appEl.querySelector('#mobileNavSheet') as HTMLElement;
|
||||
const navScrim = appEl.querySelector('#mobileNavScrim')!;
|
||||
const closeMobileNav = () => {
|
||||
navSheet.classList.remove('open');
|
||||
|
|
@ -127,5 +127,39 @@ export async function renderShell(appEl: HTMLElement): Promise<HTMLElement> {
|
|||
});
|
||||
window.addEventListener('hashchange', closeMobileNav);
|
||||
|
||||
// Drag-to-dismiss on the grabber handle only - not the whole sheet,
|
||||
// which would fight the library grid's own scroll/tap targets (see
|
||||
// docs/architecture.md's "Mobile navigation, take two"). Reported
|
||||
// directly: without this, a scrim tap was the ONLY way to close the
|
||||
// sheet, and the instinctive swipe-it-down gesture instead fell
|
||||
// through to the browser's own pull-to-refresh (see Shell.css's
|
||||
// touch-action/overscroll-behavior on the scrim/sheet themselves -
|
||||
// this is the other half, giving that swipe something to actually do).
|
||||
const CLOSE_DRAG_THRESHOLD_PX = 90;
|
||||
const grabberHandle = appEl.querySelector('#sheetGrabberHandle') as HTMLElement;
|
||||
let dragStartY: number | null = null;
|
||||
let dragDeltaY = 0;
|
||||
|
||||
grabberHandle.addEventListener('pointerdown', (e) => {
|
||||
dragStartY = e.clientY;
|
||||
dragDeltaY = 0;
|
||||
grabberHandle.setPointerCapture(e.pointerId);
|
||||
navSheet.classList.add('dragging');
|
||||
});
|
||||
grabberHandle.addEventListener('pointermove', (e) => {
|
||||
if (dragStartY === null) return;
|
||||
dragDeltaY = Math.max(0, e.clientY - dragStartY); // only follows a downward drag - closing is the only thing this gesture does
|
||||
navSheet.style.transform = `translateY(${dragDeltaY}px)`;
|
||||
});
|
||||
function endDrag() {
|
||||
if (dragStartY === null) return;
|
||||
dragStartY = null;
|
||||
navSheet.classList.remove('dragging');
|
||||
navSheet.style.transform = ''; // hands back to the .open class's own rule - either translateY(0) (snap back) or the closeMobileNav() call below immediately overrides it anyway
|
||||
if (dragDeltaY > CLOSE_DRAG_THRESHOLD_PX) closeMobileNav();
|
||||
}
|
||||
grabberHandle.addEventListener('pointerup', endDrag);
|
||||
grabberHandle.addEventListener('pointercancel', endDrag);
|
||||
|
||||
return appEl.querySelector('#mainContent') as HTMLElement;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3775,6 +3775,61 @@ genuinely narrow viewport. **Not verified**: any of this against a
|
|||
real Emby server's actual artist/album images and names, or on a real
|
||||
phone.
|
||||
|
||||
## Mobile navigation, take two: swipe-to-close was fighting pull-to-refresh
|
||||
|
||||
Reported directly, against a real phone: the bottom sheet (see "Mobile
|
||||
navigation" above) had exactly one way to close it - tap the dimmed
|
||||
scrim - since the grabber was purely decorative. Swiping the sheet
|
||||
down, the instinctive gesture every real bottom sheet supports, fell
|
||||
through to the browser's own default touch handling instead: with
|
||||
nothing telling it otherwise, a downward drag starting on the scrim or
|
||||
the sheet reads to the browser as "the user wants to pull the page
|
||||
down," which on a phone is the built-in pull-to-refresh gesture - not
|
||||
a bug in this app's own logic, but a real gap in it (nothing was
|
||||
`preventDefault`-ing or claiming that gesture for the sheet instead).
|
||||
|
||||
**Two independent parts, not one fix**: containing the browser's own
|
||||
gesture, and giving the swipe something to actually do.
|
||||
|
||||
- `touch-action: none` on `.mobileNavScrim` and the new
|
||||
`.sheetGrabberHandle` - tells the browser not to apply *any* of its
|
||||
own default touch behavior (panning, pull-to-refresh) to a touch
|
||||
starting on either, since this app's own pointer handlers own that
|
||||
gesture now. `overscroll-behavior: contain` on `.mobileNavSheet`
|
||||
itself covers the other half of the same problem: its own library
|
||||
grid can be taller than the sheet's `75vh` cap, so a drag that hits
|
||||
*that* scroll boundary needs to stop there too, not chain past the
|
||||
sheet to the page underneath.
|
||||
- **The grabber is now a real drag handle**, not just a visual hint.
|
||||
`#sheetGrabberHandle` wraps the small visible pill in a much bigger
|
||||
invisible touch target (a real touch target, not a 36x4px sliver)
|
||||
and drives the sheet with the same pointerdown/move/up(/cancel)
|
||||
pattern `views/player/Player.css`'s own seek bar already uses:
|
||||
1:1 tracking under the finger while dragging (`.dragging` suspends
|
||||
the CSS transition for this), then either snapping back open (a
|
||||
drag under 90px) or calling the exact same `closeMobileNav()` the
|
||||
scrim tap already used (past it) - one close path, two ways to
|
||||
reach it, not a second parallel implementation. Deliberately scoped
|
||||
to the grabber alone, not the whole sheet: the sheet's own content
|
||||
(the library grid, the logout row) needs normal taps and its own
|
||||
scroll to keep working, which a whole-sheet drag-to-close would
|
||||
fight.
|
||||
|
||||
Verified: full `typecheck`/`build` (client only). The CSS properties
|
||||
(`touch-action`/`overscroll-behavior`) confirmed applied via
|
||||
`getComputedStyle` against the real compiled `app.css`. The drag logic
|
||||
itself confirmed with dispatched `PointerEvent`s carrying real
|
||||
`clientY`/`pointerId` values against the real handlers copied from
|
||||
`Shell.ts`: a 40px drag snaps back (stays open, `transform` cleared),
|
||||
a 120px drag closes, and the live `transform` mid-drag tracks the
|
||||
pointer 1:1 (confirmed at an intermediate point, not just the start/end
|
||||
state) - not just asserted from reading the threshold constant.
|
||||
Existing behavior re-confirmed unchanged in the same pass: scrim-tap-
|
||||
to-close still closes. **Not verified**: actual touch input (not a
|
||||
mouse or dispatched `PointerEvent`) on a real phone - no device
|
||||
reachable from this sandbox, same standing gap as this project's other
|
||||
touch-specific work (the video/music seek bars' own drag handling).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Playback always goes through `hls.js` against whatever Emby's
|
||||
|
|
|
|||
Loading…
Reference in a new issue