From bdfac065390a3a184e8c2f6c2a17f6110e26f4a2 Mon Sep 17 00:00:00 2001 From: thamaraiselvam Date: Sun, 11 Oct 2026 14:41:28 +0800 Subject: [PATCH] fix: focus and commit the tab rename field in settings The rename field never received focus, so it looked like plain text and swallowed typing. Focus it once it appears, commit on focus loss, cancel on Escape, and make the whole Rename button clickable. Co-Authored-By: Claude Sonnet 5.5 --- .../Features/Tabs/TabManagementView.swift | 17 ++++++++++- .../fix-tab-rename-focus/.openspec.yaml | 2 ++ .../changes/fix-tab-rename-focus/design.md | 20 +++++++++++++ .../changes/fix-tab-rename-focus/proposal.md | 20 +++++++++++++ .../specs/tab-management/spec.md | 29 +++++++++++++++++++ .../changes/fix-tab-rename-focus/tasks.md | 11 +++++++ 6 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/fix-tab-rename-focus/.openspec.yaml create mode 100644 openspec/changes/fix-tab-rename-focus/design.md create mode 100644 openspec/changes/fix-tab-rename-focus/proposal.md create mode 100644 openspec/changes/fix-tab-rename-focus/specs/tab-management/spec.md create mode 100644 openspec/changes/fix-tab-rename-focus/tasks.md diff --git a/app/Sources/TaskNxt/Features/Tabs/TabManagementView.swift b/app/Sources/TaskNxt/Features/Tabs/TabManagementView.swift index ec6e3d4..de22826 100644 --- a/app/Sources/TaskNxt/Features/Tabs/TabManagementView.swift +++ b/app/Sources/TaskNxt/Features/Tabs/TabManagementView.swift @@ -36,18 +36,33 @@ private struct TabRow: View { @Binding var errorMessage: String? @State private var name: String = "" @State private var isEditing = false + @FocusState private var isNameFocused: Bool var body: some View { HStack { if isEditing { TextField("Name", text: $name, onCommit: commitRename) .textFieldStyle(.roundedBorder) + .focused($isNameFocused) + .onExitCommand { isEditing = false } + // Focus is dropped if requested in the same update that + // inserts the field (same fix as TaskRowView). + .onAppear { DispatchQueue.main.async { isNameFocused = true } } + // Clicking elsewhere commits, like pressing Enter. + .onChange(of: isNameFocused) { focused in + if !focused && isEditing { commitRename() } + } } else { Text(tab.name) Spacer() - Button("Rename") { + Button { name = tab.name isEditing = true + } label: { + Text("Rename") + .padding(.horizontal, 6) + .padding(.vertical, 2) + .contentShape(Rectangle()) } .buttonStyle(.plain) .foregroundStyle(.secondary) diff --git a/openspec/changes/fix-tab-rename-focus/.openspec.yaml b/openspec/changes/fix-tab-rename-focus/.openspec.yaml new file mode 100644 index 0000000..a369219 --- /dev/null +++ b/openspec/changes/fix-tab-rename-focus/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-11 diff --git a/openspec/changes/fix-tab-rename-focus/design.md b/openspec/changes/fix-tab-rename-focus/design.md new file mode 100644 index 0000000..fe092b8 --- /dev/null +++ b/openspec/changes/fix-tab-rename-focus/design.md @@ -0,0 +1,20 @@ +## Context + +`TabRow` in `TabManagementView.swift` toggles between `Text` + Rename button and a `TextField` using `isEditing`. `TaskRowView` solved the identical problem in `31b0bc3`: focus requested in the same update that inserts the field is dropped, so focus must be set after the field appears. See proposal.md. + +## Goals / Non-Goals + +**Goals:** make rename work reliably using the pattern already in `TaskRowView`. + +**Non-Goals:** rename from the popover tab bar; a shared inline-edit component; changes to `AppState.renameTab`. + +## Decisions + +- **Mirror `TaskRowView`**: `@FocusState`, `.onAppear { DispatchQueue.main.async { focused = true } }`, `.onChange(of: focused)` to commit on blur, `.onExitCommand` to cancel. Consistent with existing code and proven for the same bug. Alternative: extract a shared view — rejected as more code for two call sites. +- **Keep empty-name handling** in `commitRename` (falls back to current name); `renameTab` unchanged. +- **Guard against double commit**: Enter sets `isEditing = false` before the blur `onChange` fires, and the blur handler only commits while `isEditing` is true. Escape also clears `isEditing` first, so the resulting blur does not commit the draft. +- **Hit area**: `.contentShape(Rectangle())` plus small padding on the Rename button label, as done in `ef50c31`. + +## Risks / Trade-offs + +- Focus timing is only verifiable in the running app → manual check task. The SwiftUI target can't be built in some sandboxes; verify with a real build. diff --git a/openspec/changes/fix-tab-rename-focus/proposal.md b/openspec/changes/fix-tab-rename-focus/proposal.md new file mode 100644 index 0000000..3b2028e --- /dev/null +++ b/openspec/changes/fix-tab-rename-focus/proposal.md @@ -0,0 +1,20 @@ +## Why + +In Settings → Tabs, clicking **Rename** swaps the label for a text field that never receives keyboard focus, so it looks like plain text and swallows typing. Renaming a tab is effectively broken. Task rows had the same bug, fixed in `31b0bc3`; the tab row was missed. + +## What Changes + +- Focus the rename field as soon as it appears. +- Commit the rename when the field loses focus (like Enter); Escape cancels and keeps the old name. +- Make the whole **Rename** button hit-testable, not just its text glyphs. + +## Capabilities + +### New Capabilities + +### Modified Capabilities +- `tab-management`: "Rename a tab" gains requirements on how the inline rename interaction behaves (focus, commit, cancel). + +## Impact + +- `app/Sources/TaskNxt/Features/Tabs/TabManagementView.swift` (`TabRow`) only. No model, persistence, or `AppState` changes. diff --git a/openspec/changes/fix-tab-rename-focus/specs/tab-management/spec.md b/openspec/changes/fix-tab-rename-focus/specs/tab-management/spec.md new file mode 100644 index 0000000..c1a6688 --- /dev/null +++ b/openspec/changes/fix-tab-rename-focus/specs/tab-management/spec.md @@ -0,0 +1,29 @@ +## MODIFIED Requirements + +### Requirement: Rename a tab +The user SHALL be able to rename any existing tab. Renaming SHALL NOT affect the tab's tasks. When the user starts a rename, the name field SHALL be immediately focused and ready for typing. The rename SHALL be committed when the user presses Enter or moves focus away from the field, and SHALL be cancelled, leaving the name unchanged, when the user presses Escape. An empty name SHALL NOT replace the existing name. The entire Rename control SHALL be clickable, not only its label text. + +#### Scenario: Rename preserves tasks +- **WHEN** a tab named "Work" contains tasks +- **AND** the user renames it to "Projects" +- **THEN** the tab is now labeled "Projects" and all of its tasks remain unchanged + +#### Scenario: Field is focused on rename +- **WHEN** the user activates Rename on a tab +- **THEN** the name field appears with keyboard focus and typing edits the name without any extra click + +#### Scenario: Focus loss commits +- **WHEN** the user edits the name and clicks elsewhere +- **THEN** the new name is saved and the row leaves edit mode + +#### Scenario: Escape cancels +- **WHEN** the user edits the name and presses Escape +- **THEN** the tab keeps its previous name and the row leaves edit mode + +#### Scenario: Empty name rejected +- **WHEN** the user clears the name and commits +- **THEN** the tab keeps its previous name + +#### Scenario: Whole Rename control is clickable +- **WHEN** the user clicks the padding around the Rename label +- **THEN** rename starts diff --git a/openspec/changes/fix-tab-rename-focus/tasks.md b/openspec/changes/fix-tab-rename-focus/tasks.md new file mode 100644 index 0000000..861ff25 --- /dev/null +++ b/openspec/changes/fix-tab-rename-focus/tasks.md @@ -0,0 +1,11 @@ +## 1. Fix TabRow rename + +- [x] 1.1 Add `@FocusState` to `TabRow` and focus the field in `.onAppear` via `DispatchQueue.main.async` +- [x] 1.2 Commit on focus loss (`.onChange` of focus, only while `isEditing`) +- [x] 1.3 Cancel on Escape (`.onExitCommand`) without committing the draft +- [x] 1.4 Give the Rename button a full-area hit target (`.contentShape` + padding) + +## 2. Verification + +- [x] 2.1 Build the `TaskNxt` target — builds cleanly with full Xcode (`DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift build`) +- [x] 2.2 Manually verify in Settings → Tabs: focus on Rename, Enter commits, click-away commits, Escape cancels, empty name keeps old name