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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user