From d5b4bf0ac17368e18859892cb805e24ef47dbe32 Mon Sep 17 00:00:00 2001 From: "Fabian Hamm (Privat)" Date: Tue, 4 Aug 2026 19:38:49 +0200 Subject: [PATCH] fix(camera): get the upload sheet out from in front of the shutter button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping "Kamera" opened the viewfinder with the Galerie/Kamera sheet still sitting over the bottom of it, covering the capture controls. The sheet is `fixed`, so it could not be scrolled out of the way: the only route to the shutter was the phone's back button, which is not a discoverable step and is one most guests would read as "the camera is broken". Two independent causes, both fixed, because either one alone leaves a gap. The sheet never closed. It stays mounted for its translate-y animation and nothing told it the camera had taken over, so it kept its panel, its backdrop and its `aria-modal` while a full-screen overlay was up. `CameraCapture` now reports when its preview is live and the sheet dismisses itself on that signal. Deliberately on the preview, not on the tap. Closing when "Kamera" is pressed would dismiss the sheet before we know the camera works at all — and it often does not: a denied permission, no camera, or any non-secure context (where `navigator.mediaDevices` is simply absent) all end at the error panel. Closing early would leave the guest looking at that error with nothing behind it. Gated on `loadedmetadata`, the sheet is still there when the camera fails, so "Schließen" returns them to where they were. The signal is one-shot, because flipping the lens or switching photo/video re-acquires the stream and re-announcing "ready" would ask the caller to redo a dismissal it has already done. And the stacking was ambiguous. Both elements were `z-50` and the sheet is rendered after the camera, so it won on paint order. The overlay moves to `z-[60]` — the tier the Toaster already occupies, so toasts still surface above the viewfinder on DOM order. This is the part that holds regardless of timing: the controls are now reachable during the permission prompt and on the error panel, before anything has been dismissed. Focus follows the same reasoning. When the camera closes the sheet, restoring focus immediately would put it on the FAB *behind* the overlay, where a Tab could walk the page underneath; it is restored when the overlay goes away instead. Co-Authored-By: Claude Opus 5 --- .../src/lib/components/CameraCapture.svelte | 32 +++++++++++- .../src/lib/components/UploadSheet.svelte | 49 ++++++++++++++++--- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/components/CameraCapture.svelte b/frontend/src/lib/components/CameraCapture.svelte index 501b623..c4d8f56 100644 --- a/frontend/src/lib/components/CameraCapture.svelte +++ b/frontend/src/lib/components/CameraCapture.svelte @@ -5,9 +5,29 @@ interface Props { oncapture: (blob: Blob, type: 'photo' | 'video') => void; onclose: () => void; + /// Fired once, when the live preview is actually on screen — not when the camera was + /// merely requested. The caller uses it to dismiss whatever launched the camera. + /// + /// Gated on the preview rather than on `getUserMedia` resolving, because the two differ + /// exactly where it matters: if permission is denied, or the page is not a secure context, + /// this never fires and the caller's UI stays put behind the error panel — so "Schließen" + /// returns the guest to where they were instead of dropping them somewhere they never + /// asked to be. + onready?: () => void; } - let { oncapture, onclose }: Props = $props(); + let { oncapture, onclose, onready }: Props = $props(); + + /// `onready` is a one-shot. `startCamera` re-runs on every lens flip and photo/video switch, + /// and re-announcing "the camera is ready" mid-session would ask the caller to redo a + /// dismissal it has already done. + let announcedReady = false; + + function handlePreviewLive() { + if (announcedReady) return; + announcedReady = true; + onready?.(); + } let videoEl: HTMLVideoElement = $state()!; let canvasEl: HTMLCanvasElement = $state()!; @@ -157,7 +177,14 @@ } -
+ +
{#if error} @@ -197,6 +224,7 @@ autoplay playsinline muted + onloadedmetadata={handlePreviewLive} class="h-full w-full object-cover {facingMode === 'user' ? 'scale-x-[-1]' : ''}" > diff --git a/frontend/src/lib/components/UploadSheet.svelte b/frontend/src/lib/components/UploadSheet.svelte index 9b76e29..f230bd8 100644 --- a/frontend/src/lib/components/UploadSheet.svelte +++ b/frontend/src/lib/components/UploadSheet.svelte @@ -1,4 +1,5 @@ {#if showCamera} - + {/if}