Task detail
StableEverything about one task: an editable title and description, and each property as a picker button.
TaskDetail edits one task through the task actions. The title and description save when you leave the field (⌘Z undoes them), and each property opens the shared picker. As a panel it has a header with the id, its place in the list, previous and next, a menu and close; as a sheet it fits inside a dialog. It needs a PickerHost above it.
Updated View source
Side panel
import { useRef, useState } from 'react'import { labelIndex, StorePickerHost, taskStore, TaskDetail, useTaskActions, useTaskState } from '@rchursin/product/tasks'import { Button } from '@rchursin/product/ui'
/** * The side panel the Tasks app shows next to a wide list, on the shared task store. * Edit the title or description (they save when you leave the field), change a property, * or step through tasks with the arrows in its header. Esc closes it. */export default function TaskDetailPanel() { return ( <StorePickerHost> <Panel /> </StorePickerHost> )}
function Panel() { const { tasks, labels: custom } = useTaskState() // The sample labels plus any the visitor createdUsage
import { StorePickerHost, TaskDetail, useTaskActions } from '@rchursin/product/tasks'<StorePickerHost> <TaskDetail variant="panel" task={task} now={new Date()} labels={labels} actions={actions} onClose={close} onDelete={(id) => actions.remove([id])} /></StorePickerHost>Examples
In a dialog
The narrow layout. The Tasks app switches to it when its body is under 860 pixels wide.
import { useRef, useState } from 'react'import { labelIndex, StatusIcon, StorePickerHost, TaskDetail, useTaskActions, useTaskState } from '@rchursin/product/tasks'import { Button, Dialog } from '@rchursin/product/ui'
/** * The narrow layout: the same detail as a sheet inside a modal dialog. The picker host sits * inside the dialog too, because everything outside a modal dialog is inert. The dialog is * removed when it closes, so focus is put back on the button by hand. */export default function TaskDetailSheet() { const { tasks, labels: custom } = useTaskState() // The sample labels plus any the visitor created const labels = labelIndex(custom).all const actions = useTaskActions() const [openId, setOpenId] = useState<string | null>(null) const task = tasks.find((t) => t.id === openId) ?? null const first = tasks.find((t) => t.status === 'in_review') ?? tasks[0] const opener = useRef<HTMLButtonElement>(null)
function close() {API
TaskDetail props
| Prop | Type | Default | Description |
|---|---|---|---|
taskrequired | Task | – | The task to edit. |
nowrequired | Date | – | For the due date (“Tomorrow”, “Overdue”). |
labelsrequired | Label[] | – | Every label, sample and custom: labelIndex(custom).all. |
actionsrequired | TaskActions | – | Edits go through actions.edit, so ⌘Z undoes them. |
variantrequired | 'panel' | 'sheet' | – | panel: a region with its own header that Esc closes. sheet: just the fields, for a dialog that has its own title and close button. |
onCloserequired | () => void | – | The close button or Esc (panel). |
onDeleterequired | (id: string) => void | – | Delete task in the panel’s “…” menu. |
onStep | (delta: 1 | -1) => void | – | Shows previous and next buttons in the panel header. |
position | string | – | “3 of 12”, shown next to the id. |
ref | Ref<HTMLDivElement> | – | The panel root (tabIndex -1), so an app can move focus into it. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves through the header buttons, the title, the description and the six property buttons. |
| Enter | In the title: keep it and move on to the description. |
| EnterSpace | On a property: open its picker. |
| Esc | Close the panel. In a dialog, Esc closes the dialog. |
- As a panel it is a region named “RC-128 details” and can take focus, so an app can move focus into it when it opens.
- The title is a one-line textarea labelled “Title”. Emptying it sets aria-invalid and shows an error tied to it with aria-describedby; leaving the field keeps the old title.
- Properties are a description list; each value is a button named like “Assignee: Alex Rivera (you)”, with aria-haspopup="dialog".
- Edits save on blur and again when the panel unmounts, so closing it or stepping to another task never loses typing.
Guidelines
Do
- Put a PickerHost above it. In a modal dialog, put the host inside the dialog: the page behind a modal is inert.
- Use the panel when there is room next to the list and the sheet in a dialog when there isn’t, as TasksApp does at 860px.
Don’t
- Don’t pass only the labels from useTaskState(); they are just the custom ones. Merge them with labelIndex(labels).all.
- Don’t add a Save button: each field saves as you leave it, and ⌘Z undoes.
In use
Related
- TasksThe whole task tracker: a keyboard-first list and board, a detail view, bulk edits, and undo for every change.
- Task listThe task list as a data grid: one Tab stop, arrows to move, pickers in the cells, and groups that collapse.
- Property pickersThe Tasks tool’s pickers for status, priority, assignee, labels and estimate, as a chip, a row or an icon.
- DialogA modal on the native <dialog> element: the page behind goes inert, Esc closes it, and focus goes back to what opened it.
- TextareaA multi-line field that grows with what is typed, up to a maximum, then scrolls.
Source
The component’s own files, exactly as they run on this site.
/** * Task detail: editable title and description, and every property as a picker button. * On a wide window it is a side panel next to the list (J / K or the arrows step through tasks); * on a narrow one it sits in a dialog. Edits save when you leave the field; ⌘Z undoes them. */import { useEffect, useId, useRef, useState, type KeyboardEvent, type MouseEvent, type ReactNode, type Ref,} from "react"import { cx } from "../../components/cx"import { personById, priorityById, statusById, type Task } from "../data/sample"import { Icon } from "../icons/Icon"import { fromISODate, formatShort } from "../lib/dates"import { IconButton } from "../ui/button"