Fullscreen on Safari, and a fallback where there is none
Safari on a Mac wants the webkit prefix; Safari on iOS has no fullscreen for an ordinary element at all, only for a <video>, so requestFullscreen was simply undefined and the tab threw. It now uses whichever spelling the browser has, and where there is none the frame fills the viewport in-page instead — the same result minus the browser's own chrome — with a Done button and Escape to come back. The button reads Expand rather than Fullscreen when that is what it will do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e64b0a1727
commit
d8bf5df89f
+47
-9
@@ -22,11 +22,35 @@ import { Btn, Loading, askYes, toast, useEscape } from './ui'
|
||||
type KeyboardLock = { lock?: (keys?: string[]) => Promise<void>; unlock?: () => void }
|
||||
const keyboardAPI = (): KeyboardLock | undefined => (navigator as unknown as { keyboard?: KeyboardLock }).keyboard
|
||||
|
||||
// Fullscreen, across the three ways browsers spell it. Safari on a Mac wants the webkit
|
||||
// prefix; Safari on iOS has no fullscreen for an ordinary element at all — only for a
|
||||
// <video> — so there the page expands inside the viewport instead, which is the same thing
|
||||
// minus the browser's own chrome.
|
||||
type FSElement = HTMLElement & { webkitRequestFullscreen?: () => Promise<void> | void }
|
||||
type FSDocument = Document & { webkitFullscreenElement?: Element | null; webkitExitFullscreen?: () => Promise<void> | void }
|
||||
const nativeFullscreen = () => {
|
||||
if (typeof document === 'undefined') return false
|
||||
const el = document.documentElement as FSElement
|
||||
return typeof el.requestFullscreen === 'function' || typeof el.webkitRequestFullscreen === 'function'
|
||||
}
|
||||
const fullscreenElement = () => (document as FSDocument).fullscreenElement ?? (document as FSDocument).webkitFullscreenElement ?? null
|
||||
async function enterFullscreen(el: FSElement) {
|
||||
if (typeof el.requestFullscreen === 'function') return el.requestFullscreen()
|
||||
if (typeof el.webkitRequestFullscreen === 'function') return el.webkitRequestFullscreen()
|
||||
throw new Error('this browser has no fullscreen for a page element')
|
||||
}
|
||||
function exitFullscreen() {
|
||||
const d = document as FSDocument
|
||||
if (typeof d.exitFullscreen === 'function') return d.exitFullscreen()
|
||||
if (typeof d.webkitExitFullscreen === 'function') return d.webkitExitFullscreen()
|
||||
}
|
||||
|
||||
export default function KVM({ machine, title }: { machine: string | null; title: string }) {
|
||||
const [kvm, setKvm] = useState<Device | null | undefined>(undefined)
|
||||
const [adding, setAdding] = useState(false)
|
||||
const [err, setErr] = useState('')
|
||||
const [full, setFull] = useState(false)
|
||||
const [full, setFull] = useState(false) // native fullscreen is on
|
||||
const [expanded, setExpanded] = useState(false) // the fallback: filling the viewport in-page
|
||||
const box = useRef<HTMLDivElement>(null)
|
||||
const frame = useRef<HTMLIFrameElement>(null)
|
||||
const key = machine ?? '' // the hub is a machine too, and its key is the empty one
|
||||
@@ -41,21 +65,33 @@ export default function KVM({ machine, title }: { machine: string | null; title:
|
||||
// than the browser. Both are undone together when fullscreen ends, however it ends.
|
||||
useEffect(() => {
|
||||
const onChange = () => {
|
||||
const on = !!document.fullscreenElement
|
||||
const on = !!fullscreenElement()
|
||||
setFull(on)
|
||||
if (!on) keyboardAPI()?.unlock?.()
|
||||
}
|
||||
document.addEventListener('fullscreenchange', onChange)
|
||||
return () => { document.removeEventListener('fullscreenchange', onChange); keyboardAPI()?.unlock?.() }
|
||||
document.addEventListener('webkitfullscreenchange', onChange)
|
||||
return () => {
|
||||
document.removeEventListener('fullscreenchange', onChange)
|
||||
document.removeEventListener('webkitfullscreenchange', onChange)
|
||||
keyboardAPI()?.unlock?.()
|
||||
}
|
||||
}, [])
|
||||
// Expanding is in-page, so Escape is ours to handle.
|
||||
useEscape(() => setExpanded(false), expanded)
|
||||
|
||||
const goFullscreen = async () => {
|
||||
const goBig = async () => {
|
||||
if (!nativeFullscreen()) { setExpanded(true); frame.current?.focus(); return }
|
||||
try {
|
||||
await box.current?.requestFullscreen()
|
||||
if (box.current) await enterFullscreen(box.current)
|
||||
await keyboardAPI()?.lock?.()
|
||||
frame.current?.focus()
|
||||
} catch (e) { toast(e instanceof Error ? e.message : 'fullscreen refused', 'err') }
|
||||
} catch {
|
||||
setExpanded(true) // refused (an iframe policy, a permissions policy): fill the page instead
|
||||
frame.current?.focus()
|
||||
}
|
||||
}
|
||||
const goSmall = () => { setExpanded(false); if (fullscreenElement()) exitFullscreen() }
|
||||
|
||||
const remove = async () => {
|
||||
if (!kvm || !await askYes(`Forget ${title}'s KVM? The device itself is not touched — this only removes its address.`, 'Remove')) return
|
||||
@@ -66,6 +102,7 @@ export default function KVM({ machine, title }: { machine: string | null; title:
|
||||
if (kvm === undefined) return <div className="dash"><Loading label="Looking…" /></div>
|
||||
|
||||
const locks = !!keyboardAPI()?.lock
|
||||
const big = full || expanded
|
||||
|
||||
return (
|
||||
<div className="kvm">
|
||||
@@ -76,7 +113,7 @@ export default function KVM({ machine, title }: { machine: string | null; title:
|
||||
: <span className="muted small">no KVM</span>}
|
||||
<span className="spacer" />
|
||||
{kvm && <>
|
||||
<Btn className="btn" onClick={goFullscreen} title={locks ? 'fullscreen, and the browser stops keeping Escape, Tab and Ctrl-W for itself' : 'fullscreen — this browser will still keep some keys for itself'}>Fullscreen</Btn>
|
||||
<Btn className="btn" onClick={goBig} title={!nativeFullscreen() ? 'fill the screen — this browser has no true fullscreen for a page element' : locks ? 'fullscreen, and the browser stops keeping Escape, Tab and Ctrl-W for itself' : 'fullscreen — this browser will still keep some keys for itself'}>{nativeFullscreen() ? 'Fullscreen' : 'Expand'}</Btn>
|
||||
<a className="btn" href={kvm.url} target="_blank" rel="noreferrer" title="the device directly, outside Ham Lab — only works on the LAN">Open ↗</a>
|
||||
<Btn className="btn" onClick={() => setAdding(true)}>Change</Btn>
|
||||
<Btn className="btn danger" onClick={remove}>Remove</Btn>
|
||||
@@ -85,9 +122,10 @@ export default function KVM({ machine, title }: { machine: string | null; title:
|
||||
</div>
|
||||
|
||||
{kvm ? (
|
||||
<div className={'kvm-frame' + (full ? ' full' : '')} ref={box}>
|
||||
<div className={'kvm-frame' + (full ? ' full' : '') + (expanded ? ' expanded' : '')} ref={box}>
|
||||
<iframe ref={frame} key={kvm.id} src={`/kvm/${kvm.id}/`} title={kvm.name}
|
||||
allow="fullscreen; clipboard-read; clipboard-write" />
|
||||
{expanded && <button className="btn kvm-shrink" onClick={goSmall} title="back to the console">Done</button>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="svc-empty muted kvm-none">
|
||||
@@ -98,7 +136,7 @@ export default function KVM({ machine, title }: { machine: string | null; title:
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{kvm && !full && (
|
||||
{kvm && !big && (
|
||||
<div className="kvm-hint muted small">
|
||||
Click the picture, then type — the keys go to {title}.
|
||||
{locks ? ' Fullscreen also takes Escape, Tab and Ctrl-W back from the browser.' : ' This browser keeps Escape, Tab and Ctrl-W for itself; use the device’s own on-screen keys for those, or a Chromium browser.'}
|
||||
|
||||
Reference in New Issue
Block a user