fix(web): PWA notch safe-area + skip autoFocus on touch devices
Adds viewportFit: 'cover' so the PWA can draw under the notch / Dynamic Island when installed. Nav and Toast read env(safe-area-inset-*) to keep their content out of the hardware cutouts (no-op on browsers without a notch — env() resolves to 0). Replaces autoFocus on the first field of CreateAccountDialog and CreateUserDialog with a useEffect that only focuses on pointer devices (matchMedia '(hover: hover) and (pointer: fine)'). Phones no longer get the soft keyboard popping the instant a dialog opens.
This commit is contained in:
parent
eebbcb3db2
commit
3bfd35ef8d
@ -10,6 +10,11 @@ export const metadata: Metadata = {
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#18181b",
|
||||
// Lets the page draw under the iPhone notch / Dynamic Island when the
|
||||
// PWA runs in standalone mode. Components that pin to the edges (Nav,
|
||||
// Toast) read env(safe-area-inset-*) to keep their content out of the
|
||||
// hardware cutouts.
|
||||
viewportFit: "cover",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useEffect, useRef, useState, useTransition } from "react";
|
||||
import { createAccount } from "@/app/actions";
|
||||
import FormDialogShell, { Field, inputClass } from "./form-dialog-shell";
|
||||
|
||||
@ -18,6 +18,7 @@ export default function CreateAccountDialog({ open, onClose, onSuccess, prefixPa
|
||||
const [password, setPassword] = useState("");
|
||||
const [status, setStatus] = useState("");
|
||||
const [link, setLink] = useState("");
|
||||
const firstFieldRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Reset on open.
|
||||
useEffect(() => {
|
||||
@ -27,6 +28,15 @@ export default function CreateAccountDialog({ open, onClose, onSuccess, prefixPa
|
||||
setStatus("");
|
||||
setLink("");
|
||||
setError(null);
|
||||
// Autofocus the first field only on devices with a fine pointer
|
||||
// (desktop). Phones skip this — the soft keyboard popping the
|
||||
// moment a dialog opens is jarring and reflows the page.
|
||||
if (typeof window !== "undefined") {
|
||||
const isPointerDevice = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
|
||||
if (isPointerDevice) {
|
||||
requestAnimationFrame(() => firstFieldRef.current?.focus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
@ -65,10 +75,10 @@ export default function CreateAccountDialog({ open, onClose, onSuccess, prefixPa
|
||||
>
|
||||
<Field label="Username" required hint={prefixPattern ? `Suggested prefix: ${prefixPattern}` : undefined}>
|
||||
<input
|
||||
ref={firstFieldRef}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
disabled={pending}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
placeholder={prefixPattern ? `${prefixPattern}1234` : "username"}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useEffect, useRef, useState, useTransition } from "react";
|
||||
import { createUser } from "@/app/actions";
|
||||
import FormDialogShell, { Field, inputClass } from "./form-dialog-shell";
|
||||
|
||||
@ -17,6 +17,7 @@ export default function CreateUserDialog({ open, onClose, onSuccess }: Props) {
|
||||
const [fPassword, setFPassword] = useState("");
|
||||
const [tUsername, setTUsername] = useState("");
|
||||
const [tPassword, setTPassword] = useState("");
|
||||
const firstFieldRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@ -25,6 +26,13 @@ export default function CreateUserDialog({ open, onClose, onSuccess }: Props) {
|
||||
setTUsername("");
|
||||
setTPassword("");
|
||||
setError(null);
|
||||
// Autofocus first field only on pointer devices — see CreateAccountDialog.
|
||||
if (typeof window !== "undefined") {
|
||||
const isPointerDevice = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
|
||||
if (isPointerDevice) {
|
||||
requestAnimationFrame(() => firstFieldRef.current?.focus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
@ -63,10 +71,10 @@ export default function CreateUserDialog({ open, onClose, onSuccess }: Props) {
|
||||
>
|
||||
<Field label="From username" required>
|
||||
<input
|
||||
ref={firstFieldRef}
|
||||
value={fUsername}
|
||||
onChange={(e) => setFUsername(e.target.value)}
|
||||
disabled={pending}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
className={inputClass}
|
||||
|
||||
@ -9,7 +9,16 @@ export default function Nav() {
|
||||
const isAccounts = !isUsers;
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-10 border-b border-zinc-200/80 bg-white/80 backdrop-blur-md">
|
||||
<header
|
||||
className="sticky top-0 z-10 border-b border-zinc-200/80 bg-white/80 backdrop-blur-md"
|
||||
style={{
|
||||
// Push content below the iPhone notch when the PWA is installed.
|
||||
// No-op on browsers without a notch (env() resolves to 0).
|
||||
paddingTop: "env(safe-area-inset-top)",
|
||||
paddingLeft: "env(safe-area-inset-left)",
|
||||
paddingRight: "env(safe-area-inset-right)",
|
||||
}}
|
||||
>
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-4 sm:px-6">
|
||||
<Link href="/" className="group flex items-center gap-3">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-900 text-[11px] font-semibold tracking-tight text-white">
|
||||
|
||||
@ -39,7 +39,13 @@ export default function Toast({
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="fixed left-1/2 top-4 z-50 -translate-x-1/2 transform px-4"
|
||||
className="fixed left-1/2 z-50 -translate-x-1/2 transform px-4"
|
||||
style={{
|
||||
// Stay below the notch when running as an installed PWA.
|
||||
// calc(safe-area + 1rem) keeps the toast 1rem below the safe-area
|
||||
// edge — and 1rem below the top in browsers without a notch.
|
||||
top: "calc(env(safe-area-inset-top) + 1rem)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-full px-4 py-2 shadow-sm ring-1 ${styles}`}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user