# Task Form Dialog — keyboard + header polish — Design

**Date:** 2026-05-14
**Scope:** adiuvAI renderer (`src/renderer/components/tasks/TaskFormDialog.tsx`) + supporting libs
**Status:** Approved by user (mockup at `docs/mockups/2026-05-14-task-form-dialog-mockup.html`), ready for implementation plan

## Goal

Port three UX features shipped in the timeline batch-add `AddEventDialog` (`docs/2026-05-08-task-ux-evolution-design.md` § timeline batch) into `TaskFormDialog`:

1. **Header style** — `DialogTitle` + `DialogDescription` (no separator border), matching `AddEventDialog`.
2. **Full keyboard navigation** — Tab/Shift-Tab between fields & pills, arrow keys within pills row, Enter to open focused pill, arrow keys inside list popovers + calendar, Esc to close popover.
3. **Date + time via keyboard** — replace the Calendar + 2× hour/minute `Select` triplet with a typeable `DateField` that supports an optional `HH:MM` suffix and respects `FormatPrefs.dateFormat`.

## Non-goals

- Migrating `TaskFormDialog` to a Sheet (deferred — see `docs/2026-05-08-task-ux-evolution-plan.md`).
- Touching `NewTaskDialog` / `EditTaskDialog` wrappers (no behavior change).
- Changes to other property popovers' rendering beyond keyboard handling.
- Inline project creation flow (`InlineProjectForm`) — unchanged.

## 1. Header

Replace the current minimal header:

```tsx
<DialogHeader className="px-5 py-3 border-b border-border/40">
  <DialogTitle className="text-sm font-medium">{...}</DialogTitle>
</DialogHeader>
```

with the `AddEventDialog` style:

```tsx
<DialogHeader>
  <DialogTitle>{mode === 'create' ? t('tasks.newTask') : t('tasks.editTask')}</DialogTitle>
  <DialogDescription>
    {mode === 'create' ? t('tasks.newTaskDescription') : t('tasks.editTaskDescription')}
  </DialogDescription>
</DialogHeader>
```

**No border-bottom** under the header — body flows directly under it. Keep the existing `bg-card/92 backdrop-blur-xl` overlay on `DialogContent`.

New i18n keys (all 5 languages): `tasks.newTaskDescription`, `tasks.editTaskDescription`.

## 2. Keyboard navigation

### Pills row — roving focus + arrow movement

The property pills (`Project · Priority · Status · Due · Assignees`) become a roving-tabindex group:

- Only one pill at a time has `tabindex={0}`; the rest have `tabindex={-1}`. Default focused pill = first (Project).
- `Tab` / `Shift+Tab` enters/exits the group as a single stop. Inside the group, `Tab` exits forward to the footer; on entry from the footer-side, focus restores to the last-focused pill.
- `ArrowRight` / `ArrowDown` → next pill (clamped at end).
- `ArrowLeft` / `ArrowUp` → previous pill (clamped at start).
- `Home` / `End` → first / last pill.
- `Enter` or `Space` on focused pill → open its popover.

Implementation: a small hook `useRovingFocus(ref, count)` returning `(index) => { tabIndex, onKeyDown, onFocus }`. Pills consume it inside `PropertyPill` (kept presentational) via a wrapping `<button>`.

`PropertyPill` is already a `<button>`-ish trigger via `<span>`. To support real focus rings + key events, change the trigger element rendered by `PopoverTrigger asChild` from `<span>` to a `<button type="button">`. Visible focus ring matches `--ring` via `focus-visible:ring-2 ring-ring/30`.

### List popovers — Project, Priority, Status, Assignees

shadcn `Popover` does not provide list semantics. Inside each popover content:

- Items render with `role="option"` (or `menuitem`) and roving `tabIndex` (active item = `0`, rest `-1`).
- When popover opens, focus moves to the currently-selected item (or first item).
- `ArrowDown` / `ArrowUp` move the active item; `Home`/`End` jump to ends.
- `Enter` / `Space` selects the active item.
- For single-select popovers (Project, Priority, Status) selection closes the popover and returns focus to the originating pill.
- For multi-select (Assignees) selection toggles; popover stays open. `Esc` closes and returns focus to the pill.
- `Tab` inside a popover closes the popover (focus returns to pill, then the next Tab advances normally).

Implementation: a single shared hook `useListboxKeys(items, opts)` consumed by each popover content. Items are sourced from existing data (`projectsList`, `knownAssignees`, hard-coded priority/status arrays).

### Calendar — keyboard

The shadcn `Calendar` already supports arrow-key day navigation and `Enter` to select (via react-day-picker). We need only to confirm that focus lands on the calendar grid when the Due popover opens. The new `DateField` (§3) replaces the current Popover+Calendar+Selects assembly and embeds the calendar.

### Description — Enter

Keep existing behavior: `Enter` inserts a newline in the description textarea. The form-level `⌘/Ctrl+Enter` submit handler already lives on the `<form>` element and continues to work; the footer's "⌘+Enter to create" hint is removed from the UI (the shortcut still works).

## 3. Date + time via keyboard

### Strategy — extend existing `DateField`

Reuse `src/renderer/components/ui/date-field.tsx` (already typeable, format-aware via `useFormatPrefs`, with embedded Calendar). Add **optional time** support behind a new prop `withTime?: boolean`.

When `withTime` is on:
- The text input accepts either a bare date (`30/04/2026`, `Apr 30`, `+3d`, `tomorrow`, …) or date-with-time suffix (`30/04/2026 14:30`).
- The Popover content gains a small `Time` row under the Calendar — two `Select`s (hour 00–23, minute in 5-min steps) identical to the current TaskFormDialog implementation. They edit the time portion of the committed `Date`.
- Display value after commit: `<date in FormatPrefs.dateFormat> HH:MM` when time component is non-midnight, otherwise just the date.

### Parser extension (`lib/parseDate.ts`)

`parseDate(input, prefs, keywords)` adopts optional trailing time:

- Regex split: `RE_TIME = /\s+(\d{1,2}):(\d{2})\s*$/`.
- If matched, parse `HH`/`MM` (`0–23` / `0–59`), strip the suffix, parse remaining string with the existing logic, then set `hours` and `minutes` on the result.
- If time match is invalid (e.g. `25:99`), whole input is invalid.

Unit-test cases (existing tests if any get extended; otherwise small new file):

| Input | Format pref | Expected |
|---|---|---|
| `30/04/2026 14:30` | `dd/MM/yyyy` | 2026-04-30 14:30 local |
| `04/30/2026 09:00` | `MM/dd/yyyy` | 2026-04-30 09:00 |
| `2026-04-30 23:59` | `yyyy-MM-dd` | 2026-04-30 23:59 |
| `tomorrow 08:15` | any | next-day 08:15 |
| `30/04/2026 25:00` | any | invalid |
| `30/04/2026` | dd/MM | 2026-04-30 00:00 (date only, time unchanged) |

### Caller change in `TaskFormDialog`

The whole Due Popover block (Calendar + hour/minute Selects + clear button) is replaced by:

```tsx
<DateField
  withTime
  value={values.dueDate ? new Date(values.dueDate) : undefined}
  onChange={(d) => setValues((v) => ({ ...v, dueDate: d ? d.getTime() : null }))}
  placeholder={t('tasks.colDue')}
  aria-label={t('tasks.colDue')}
/>
```

The pill itself remains for display when the field is collapsed. Two arrangements considered:

- **(A) Pill opens a popover containing the `DateField`** — keeps visual parity with the other pills. The `DateField` *inside* the popover is just an `Input` + Calendar, no nested Popover. Recommended.
- **(B) `DateField` replaces the pill inline in the row** — visually breaks the pill row.

Going with **(A)**. To avoid a nested-popover (`Popover` inside `PopoverContent`), `DateField` gains a `flat?: boolean` prop. When `flat` is set, it renders:

- the typeable `Input`,
- the `Calendar` inline (no internal `Popover` wrapper),
- the Time row (when `withTime`).

The Due pill's `PopoverContent` renders `<DateField withTime flat />`. Outside the task dialog, existing callers (e.g. `AddEventDialog`) keep using the default (non-flat) DateField with its own popover trigger.

The Due popover content:

```
┌─ Due popover ───────────────────┐
│ [📅 30/04/2026 14:30          ] │   ← typeable Input (parses date + time)
│  Calendar grid (kbd nav)        │
│  ── ── ── ── ── ── ── ── ── ── │
│  Time: [HH ⌄] : [MM ⌄] [Clear]  │   ← shown only when withTime
└─────────────────────────────────┘
```

(The mockup illustrated standalone segments; that was a sketch — the real impl reuses `DateField`'s single-input typeable parser, which is already keyboard-driven via `parseDate`.)

## 4. Files

**Modified:**

```
src/renderer/components/tasks/TaskFormDialog.tsx   — new header; roving focus on pills row; replace Due popover with <DateField withTime />; drop the "⌘+Enter" hint
src/renderer/components/ui/date-field.tsx          — new props withTime + flat; Time Selects; expanded onCommit/text-display logic
src/renderer/lib/parseDate.ts                       — accept optional trailing " HH:MM"
src/renderer/locales/{en,it,es,fr,de}/translation.json
                                                    — add tasks.newTaskDescription, tasks.editTaskDescription
```

**New (small, kept local to features):**

```
src/renderer/hooks/useRovingFocus.ts                — generic roving-tabindex hook
src/renderer/hooks/useListboxKeys.ts                — popover-list arrow/enter/esc handler
```

If a unit-test setup is later introduced for `parseDate`, add cases there. Not blocking.

## 5. Accessibility

- Pills row: `role="toolbar"` with `aria-label={t('tasks.properties')}`; pills are `<button>` with descriptive `aria-label` (e.g. `Project: Acme · Communications`).
- Listbox popovers: container `role="listbox"`, items `role="option"`, `aria-selected` on the chosen one. Single-select popovers also set `aria-activedescendant` on the listbox when convenient; otherwise rely on `.focus()`.
- Multi-select Assignees uses `aria-multiselectable="true"`.
- `DateField` keeps existing `aria-invalid` + `aria-describedby` semantics.

## 6. Out-of-scope follow-ups

- Project popover inline-create flow keyboard polish (currently a sub-form inside the popover — separate effort).
- `DateField` natural-language time keywords (e.g. `tomorrow 9am`) — only `HH:MM` accepted.
- Migrating `TaskFormDialog` shell to a Sheet — already deferred.

## 7. Implementation order (suggested)

1. `useRovingFocus` + `useListboxKeys` hooks (no UI changes).
2. `parseDate` time-suffix support; refresh existing parseDate tests.
3. `DateField` `withTime` prop + time Selects in Popover.
4. `TaskFormDialog`:
   - Header swap (Title + Description, no border).
   - Pills row wired to `useRovingFocus`; pill trigger element switched to `<button>`.
   - Each list popover wired to `useListboxKeys`.
   - Due popover content replaced by `<DateField withTime />`.
   - Remove footer `⌘+Enter` hint.
5. i18n strings in all five languages.
