Skip to content

Commit e4fa91e

Browse files
authored
fix: focus the right field when a form, popover or menu opens (#25)
## Summary Whatever opens now puts keyboard focus where you'd type next. | Opens | Before | Now | | --- | --- | --- | | Edit Display Name (Alt+Shift+N / Welcome menu) | focus stayed on the page | name field focused, prefilled with the current name and selected | | Habit → Edit | stayed on the page | name field focused | | Goal → comments | stayed on the page | comment field focused | | Focus summary → edit session | stayed on the page | title field focused | | Add Tag popover (Alt+Shift+T / click) | stayed on the page | "Create new tag" focused; Esc closes | | Menu (Alt+M / click) | stayed on the button | first item focused; Esc closes and returns focus to the button | Already correct and unchanged: Alt+G (goal), Alt+N on To-Dos/Goals/Habits/Tags/Boost, to-do inline edits, habit notes, new tags, dialogs and panels. ## Testing - Lint, typecheck, unit tests and build pass. - In the browser: Alt+Shift+N → name field focused with "Alex" selected, Esc closes; Alt+Shift+T → "Create new tag" focused, Esc closes; Alt+M → first menu item focused, Esc closes; Alt+G → goal field focused.
1 parent 10b0d17 commit e4fa91e

6 files changed

Lines changed: 43 additions & 7 deletions

File tree

‎src/components/BoostApp.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ function AppShell() {
7777
const goBack = () => setTab(previousTab);
7878

7979
useNavigationHotkeys(navigateFromCurrent);
80-
useHotkey("app.displayName", () => setIsEditingName(true));
80+
const openNameEditor = () => {
81+
setNewName(displayName);
82+
setIsEditingName(true);
83+
};
84+
useHotkey("app.displayName", openNameEditor);
8185

8286
const saveName = () => {
8387
const trimmed = newName.trim();
@@ -156,6 +160,8 @@ function AppShell() {
156160
type="text"
157161
value={newName}
158162
onChange={(event) => setNewName(event.target.value)}
163+
autoFocus
164+
onFocus={(event) => event.currentTarget.select()}
159165
placeholder="Enter display name"
160166
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 mb-4"
161167
onKeyDown={(event) => {
@@ -215,7 +221,7 @@ function AppShell() {
215221
<TopBar
216222
displayName={displayName}
217223
displayTime={formatTodayMinutes(todayMinutes)}
218-
onEditDisplayName={() => setIsEditingName(true)}
224+
onEditDisplayName={openNameEditor}
219225
onOpenHistory={() => setTab("history")}
220226
/>
221227
<BottomNav activeTab={tab} onTabChange={setTab} />

‎src/components/focus/GoalForm.tsx‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,23 @@ function TagSelector({
112112
const [allTags, setAllTags] = useState<Tag[]>([]);
113113
const [isOpen, setIsOpen] = useState(false);
114114
const [isCreating, setIsCreating] = useState(false);
115-
const ref = useOutsideDismiss(isOpen, () => {
115+
const createButtonRef = useRef<HTMLButtonElement>(null);
116+
const close = () => {
116117
setIsOpen(false);
117118
setIsCreating(false);
118-
});
119+
};
120+
const ref = useOutsideDismiss(isOpen, close);
119121
const addTagCombo = useHotkeyLabel("timer.addTag");
120122
// The Tags row stays mounted but hidden below the `wide` breakpoint.
121123
useHotkey("timer.addTag", () => {
122124
if (ref.current?.getClientRects().length) setIsOpen(true);
123125
}, isActive);
124126

127+
// Opening (by click or Alt+Shift+T) puts focus on the first action so the keyboard can carry on.
128+
useEffect(() => {
129+
if (isOpen && !isCreating) createButtonRef.current?.focus();
130+
}, [isOpen, isCreating]);
131+
125132
useEffect(() => {
126133
if (isOpen) databaseService.getTags().then(setAllTags).catch((error: unknown) => console.error("Failed to load tags:", error));
127134
}, [isOpen]);
@@ -170,14 +177,20 @@ function TagSelector({
170177
) : null}
171178
</div>
172179
{isOpen ? (
173-
<div className="absolute top-full left-0 mt-2 w-64 bg-gray-800 border border-gray-700 rounded-lg shadow-xl z-50 max-h-80 overflow-y-auto">
180+
<div
181+
className="absolute top-full left-0 mt-2 w-64 bg-gray-800 border border-gray-700 rounded-lg shadow-xl z-50 max-h-80 overflow-y-auto"
182+
onKeyDown={(event) => {
183+
if (event.key === "Escape" && !isCreating) close();
184+
}}
185+
>
174186
<div className="p-2">
175187
{isCreating ? (
176188
<NewTagForm onCreate={create} onCancel={() => setIsCreating(false)} />
177189
) : (
178190
<>
179191
<div className="mb-2 flex gap-1">
180192
<button
193+
ref={createButtonRef}
181194
onClick={() => setIsCreating(true)}
182195
className="flex-1 flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gray-700 rounded transition-colors"
183196
>

‎src/components/pages/HistoryPage.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ export function HistoryPage({ userId }: { userId: string }) {
592592
type="text"
593593
value={editGoal}
594594
onChange={(e) => setEditGoal(e.target.value)}
595+
autoFocus
595596
className="flex-1 bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-blue-500"
596597
placeholder="Session title"
597598
/>

‎src/components/pages/goals/GoalCard.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export function GoalCard(props: GoalCardProps) {
167167
type="text"
168168
value={draft}
169169
onChange={(e) => setDraft(e.target.value)}
170+
autoFocus
170171
onKeyDown={(e) => {
171172
if (e.key === "Enter" && !e.shiftKey) {
172173
e.preventDefault();

‎src/components/pages/habits/HabitCard.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function EditForm({ habit, onSave, onCancel }: { habit: Habit; onSave(edit: Habi
4848
<div className="mb-4 space-y-4">
4949
<div>
5050
<FieldLabel>Habit Name</FieldLabel>
51-
<input type="text" value={name} onChange={(e) => setName(e.target.value)} className={FIELD_INPUT} />
51+
<input type="text" value={name} onChange={(e) => setName(e.target.value)} autoFocus className={FIELD_INPUT} />
5252
</div>
5353
<div>
5454
<FieldLabel>Description (Optional)</FieldLabel>

‎src/components/shell/StartMenu.tsx‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export function StartMenu({ onNavigate }: { onNavigate(tab: Tab): void }) {
5656
const [isSettingsOpen, setIsSettingsOpen] = useState(true);
5757
const [isSoundsOpen, setIsSoundsOpen] = useState(false);
5858
const menuRef = useRef<HTMLDivElement>(null);
59+
const panelRef = useRef<HTMLDivElement>(null);
60+
const toggleRef = useRef<HTMLButtonElement>(null);
5961
const isMobile = useMediaQuery(MOBILE_QUERY);
6062
// In the 400x600 popup a floating bottom-left button would cover the timer controls,
6163
// so the menu moves into the top bar and opens downward.
@@ -65,6 +67,10 @@ export function StartMenu({ onNavigate }: { onNavigate(tab: Tab): void }) {
6567
const fullPageCombo = useHotkeyLabel("app.fullPage");
6668
useHotkey("app.fullPage", () => void openFullPage(), isPopup);
6769

70+
useEffect(() => {
71+
if (isOpen) panelRef.current?.querySelector("button")?.focus();
72+
}, [isOpen]);
73+
6874
useEffect(() => {
6975
if (!isOpen) return;
7076
const closeOnOutsideClick = (event: MouseEvent) => {
@@ -92,7 +98,15 @@ export function StartMenu({ onNavigate }: { onNavigate(tab: Tab): void }) {
9298
isPopup ? "top-full right-0 mt-2" : "bottom-full left-0 mb-2"
9399
}`}
94100
>
95-
<div className="py-2 max-h-[70vh] overflow-y-auto force-scrollbar">
101+
<div
102+
ref={panelRef}
103+
className="py-2 max-h-[70vh] overflow-y-auto force-scrollbar"
104+
onKeyDown={(event) => {
105+
if (event.key !== "Escape") return;
106+
setIsOpen(false);
107+
toggleRef.current?.focus();
108+
}}
109+
>
96110
{isPopup ? (
97111
<button onClick={() => void openFullPage()} className={TOP_ITEM}>
98112
<Expand className="w-5 h-5" />
@@ -132,6 +146,7 @@ export function StartMenu({ onNavigate }: { onNavigate(tab: Tab): void }) {
132146
</div>
133147
) : null}
134148
<button
149+
ref={toggleRef}
135150
onClick={() => setIsOpen(!isOpen)}
136151
aria-label="Menu"
137152
title={menuCombo ? `Menu (${menuCombo})` : "Menu"}

0 commit comments

Comments
 (0)