Panels dialog: opens without focusing a field, a panel title saves on blur, Enter or Escape instead of per keystroke, and a new panel's name previews its URL. Docs

This commit is contained in:
2026-10-16 09:25:34 +02:00
parent 07ecbaab3a
commit d06804b145
2 changed files with 76 additions and 43 deletions
+3 -1
View File
@@ -447,7 +447,9 @@ For a dedicated screen, pair a panel.
As a superuser:
1. Open **Dashboards → Panels**, add a panel and select its dashboards.
1. Open **Dashboards → Panels**, add a panel and select its dashboards. The
name you add it under sets its address, `/panel/{id}`, and a later change of
title keeps it.
2. Enable **Touch friendly** for larger controls.
3. Open the displayed pairing link on the device. It shows a six-character code.
4. Enter that code for the panel, check the device description, then press
@@ -139,7 +139,7 @@ export function PanelsDialog({ focus }: { focus?: string } = {}) {
write({ panels: panels.map((p) => (p.id === id ? panel : p)) })
// Closing saves every new port that is ready, in one write. Its port field
// only commits on blur, and Escape closes the dialog without one.
// only commits on blur, and closing does not always blur it.
const saveDrafts = () => {
const done = Object.keys(drafts).filter((id) => ready(drafts[id]))
if (!done.length) return
@@ -167,7 +167,17 @@ export function PanelsDialog({ focus }: { focus?: string } = {}) {
return (
<DialogContent
className="max-h-[80vh] overflow-y-auto sm:max-w-lg"
className="max-h-[80vh] overflow-y-auto outline-none sm:max-w-lg"
// Open on the dialog itself: its first field is a panel's title, which
// would open selected as if to be retyped. Focus still lands inside,
// so Tab starts in the dialog rather than the page behind it.
onOpenAutoFocus={(event) => {
event.preventDefault()
const content = event.currentTarget as HTMLElement
content.focus()
}}
// Escape closes without a blur, and the fields here save on blur.
onEscapeKeyDown={() => (document.activeElement as HTMLElement).blur()}
onCloseAutoFocus={saveDrafts}
>
<DialogHeader>
@@ -211,43 +221,55 @@ export function PanelsDialog({ focus }: { focus?: string } = {}) {
<>
<Separator />
<form
className="flex items-end gap-2"
onSubmit={(event) => {
event.preventDefault()
if (!newId || taken) return
write({
panels: [...panels, { id: newId, title: name.trim() }],
})
setName("")
}}
>
<div className="grid flex-1 gap-1">
<label className="text-sm" htmlFor="new-panel">
New panel
</label>
<Input
id="new-panel"
value={name}
placeholder="hallway"
autoComplete="off"
data-testid="new-panel-name"
onChange={(event) => setName(event.target.value)}
/>
</div>
<Button
type="submit"
disabled={!newId || taken}
data-testid="add-panel"
<div className="grid gap-2">
<form
className="flex items-end gap-2"
onSubmit={(event) => {
event.preventDefault()
if (!newId || taken) return
write({
panels: [...panels, { id: newId, title: name.trim() }],
})
setName("")
}}
>
Add panel
</Button>
</form>
{taken ? (
<p className="text-sm text-destructive">
There is already a panel called {newId}.
</p>
) : null}
<div className="grid flex-1 gap-1">
<label className="text-sm" htmlFor="new-panel">
New panel
</label>
<Input
id="new-panel"
value={name}
placeholder="hallway"
autoComplete="off"
data-testid="new-panel-name"
onChange={(event) => setName(event.target.value)}
/>
</div>
<Button
type="submit"
disabled={!newId || taken}
data-testid="add-panel"
>
Add panel
</Button>
</form>
{taken ? (
<p className="text-sm text-destructive">
There is already a panel called {newId}.
</p>
) : null}
{/* The name becomes the panel's address and stays its address,
while the title can change any time. */}
{newId && host && !taken ? (
<p
className="break-all text-xs text-muted-foreground"
data-testid="new-panel-url"
>
Opens at {host}/panel/{newId}
</p>
) : null}
</div>
</>
) : null}
</div>
@@ -352,13 +374,22 @@ function PanelRow({
>
<div className="flex items-center gap-2">
<Input
value={panel.title}
// Saved on blur or Enter, not per keystroke: each save is a PUT of
// every panel, and the field would snap back to the stored title
// until it lands. Remounted with whatever was saved.
key={panel.title}
defaultValue={panel.title}
placeholder={panel.id}
aria-label={`Title of ${panel.id}`}
disabled={!canEdit}
onChange={(event) =>
onChange({ ...panel, title: event.target.value })
}
onKeyDown={(event) => {
if (event.key === "Enter") event.currentTarget.blur()
}}
onBlur={(event) => {
const title = event.currentTarget.value.trim()
event.currentTarget.value = title
if (title !== panel.title) onChange({ ...panel, title })
}}
/>
<span className="shrink-0 text-sm text-muted-foreground">
{panel.id}