Skip to content

Commit 348c576

Browse files
authored
fix(web): align command palette navigation (#1821)
1 parent 10eefcc commit 348c576

10 files changed

Lines changed: 181 additions & 43 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import dayjs from "@core/util/date/dayjs";
2+
import { getNavigationCommandItems } from "@web/common/constants/navigation.cmd.constants";
3+
import { type ViewName } from "@web/common/constants/shortcuts.constants";
4+
import { describe, expect, it } from "bun:test";
5+
6+
describe("getNavigationCommandItems", () => {
7+
const today = dayjs("2026-05-28");
8+
9+
function getItemLabels(currentView: ViewName) {
10+
return getNavigationCommandItems({
11+
currentView,
12+
onGoToToday: () => {},
13+
onNavigateToView: () => {},
14+
today,
15+
}).map((item) => item.children);
16+
}
17+
18+
it("returns the other views and today for the week palette", () => {
19+
expect(getItemLabels("week")).toEqual([
20+
"Go to Now [n]",
21+
"Go to Day [d]",
22+
"Go to Today (Thursday, May 28) [t]",
23+
]);
24+
});
25+
26+
it("returns the other views and today for the day palette", () => {
27+
expect(getItemLabels("day")).toEqual([
28+
"Go to Now [n]",
29+
"Go to Week [w]",
30+
"Go to Today (Thursday, May 28) [t]",
31+
]);
32+
});
33+
34+
it("returns the other views and today for the now palette", () => {
35+
expect(getItemLabels("now")).toEqual([
36+
"Go to Day [d]",
37+
"Go to Week [w]",
38+
"Go to Today (Thursday, May 28) [t]",
39+
]);
40+
});
41+
42+
it("runs the matching navigation callbacks", () => {
43+
const navigatedViews: ViewName[] = [];
44+
let didGoToToday = false;
45+
const items = getNavigationCommandItems({
46+
currentView: "now",
47+
onGoToToday: () => {
48+
didGoToToday = true;
49+
},
50+
onNavigateToView: (viewName) => {
51+
navigatedViews.push(viewName);
52+
},
53+
today,
54+
});
55+
56+
items.find((item) => item.id === "go-to-day")?.onClick?.({} as never);
57+
items.find((item) => item.id === "go-to-week")?.onClick?.({} as never);
58+
items.find((item) => item.id === "today")?.onClick?.({} as never);
59+
60+
expect(navigatedViews).toEqual(["day", "week"]);
61+
expect(didGoToToday).toBe(true);
62+
});
63+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { type IconName, type JsonStructureItem } from "react-cmdk";
2+
import { type Dayjs } from "@core/util/date/dayjs";
3+
import {
4+
VIEW_SHORTCUTS,
5+
type ViewName,
6+
} from "@web/common/constants/shortcuts.constants";
7+
8+
interface GetNavigationCommandItemsArgs {
9+
currentView: ViewName;
10+
onGoToToday: () => void;
11+
onNavigateToView: (viewName: ViewName) => void;
12+
today: Dayjs;
13+
}
14+
15+
const viewIcons: Record<ViewName, IconName> = {
16+
day: "CalendarDaysIcon",
17+
now: "ClockIcon",
18+
week: "CalendarIcon",
19+
};
20+
21+
const navigationViewOrder: ViewName[] = ["now", "day", "week"];
22+
23+
export const getNavigationCommandItems = ({
24+
currentView,
25+
onGoToToday,
26+
onNavigateToView,
27+
today,
28+
}: GetNavigationCommandItemsArgs): JsonStructureItem[] => [
29+
...navigationViewOrder
30+
.filter((viewName) => viewName !== currentView)
31+
.map((viewName) => ({
32+
id: `go-to-${viewName}`,
33+
children: `Go to ${VIEW_SHORTCUTS[viewName].label} [${VIEW_SHORTCUTS[viewName].key}]`,
34+
icon: viewIcons[viewName],
35+
onClick: () => onNavigateToView(viewName),
36+
})),
37+
{
38+
id: "today",
39+
children: `Go to Today (${today.format("dddd, MMMM D")}) [t]`,
40+
icon: "ArrowUturnDownIcon",
41+
onClick: onGoToToday,
42+
},
43+
];

packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ describe("shortcuts.data", () => {
113113
k: "d",
114114
label: "Day",
115115
});
116-
expect(shortcuts.nowShortcuts).toHaveLength(6);
116+
expect(shortcuts.nowShortcuts).toHaveLength(7);
117117
expect(shortcuts.nowShortcuts[0]).toEqual({
118118
k: "e d",
119119
label: "Edit description",
@@ -135,6 +135,10 @@ describe("shortcuts.data", () => {
135135
k: "Enter",
136136
label: "Mark complete",
137137
});
138+
expect(shortcuts.nowShortcuts[6]).toEqual({
139+
k: "t",
140+
label: "Go to today",
141+
});
138142
expect(shortcuts.nowShortcuts).not.toContainEqual({
139143
k: "Esc",
140144
label: "Back to Today",

packages/web/src/common/utils/shortcut/data/shortcuts.data.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => {
7070
{ k: "j", label: "Previous task" },
7171
{ k: "k", label: "Next task" },
7272
{ k: "Enter", label: "Mark complete" },
73+
{ k: "t", label: "Go to today" },
7374
];
7475
}
7576

packages/web/src/views/CmdPalette/CmdPalette.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { useState } from "react";
22
import _CommandPalette, { filterItems, getItemIndex } from "react-cmdk";
33
import "react-cmdk/dist/cmdk.css";
4+
import { useNavigate } from "react-router-dom";
45
import {
56
SOMEDAY_MONTH_LIMIT_MSG,
67
SOMEDAY_WEEK_LIMIT_MSG,
78
} from "@core/constants/core.constants";
89
import { Categories_Event } from "@core/types/event.types";
910
import { type Dayjs } from "@core/util/date/dayjs";
1011
import { moreCommandPaletteItems } from "@web/common/constants/more.cmd.constants";
12+
import { getNavigationCommandItems } from "@web/common/constants/navigation.cmd.constants";
13+
import { VIEW_SHORTCUTS } from "@web/common/constants/shortcuts.constants";
1114
import { useAuthCmdItems } from "@web/common/hooks/useAuthCmdItems";
1215
import { useGoogleCmdItems } from "@web/common/hooks/useGoogleCmdItems";
1316
import { useLogoutCmdItems } from "@web/common/hooks/useLogoutCmdItems";
@@ -50,6 +53,7 @@ const CmdPalette = ({
5053
scrollUtil,
5154
}: CmdPaletteProps) => {
5255
const dispatch = useAppDispatch();
56+
const navigate = useNavigate();
5357
const isAtMonthlyLimit = useAppSelector(selectIsAtMonthlyLimit);
5458
const isAtWeeklyLimit = useAppSelector(selectIsAtWeeklyLimit);
5559
const open = useAppSelector(selectIsCmdPaletteOpen);
@@ -88,6 +92,22 @@ const CmdPalette = ({
8892

8993
const filteredItems = filterItems(
9094
[
95+
{
96+
heading: "Navigation",
97+
id: "navigation",
98+
items: getNavigationCommandItems({
99+
currentView: "week",
100+
onGoToToday: () => {
101+
scrollUtil.scrollToNow();
102+
_discardDraft();
103+
util.goToToday();
104+
},
105+
onNavigateToView: (viewName) => {
106+
navigate(VIEW_SHORTCUTS[viewName].route);
107+
},
108+
today,
109+
}),
110+
},
91111
{
92112
heading: "Common Tasks",
93113
id: "general",
@@ -134,16 +154,6 @@ const CmdPalette = ({
134154
void handleCreateSomedayDraft(Categories_Event.SOMEDAY_MONTH);
135155
}),
136156
},
137-
{
138-
id: "today",
139-
children: `Go to Today (${today.format("dddd, MMMM D")}) [t]`,
140-
icon: "ArrowUturnDownIcon",
141-
onClick: () => {
142-
scrollUtil.scrollToNow();
143-
_discardDraft();
144-
util.goToToday();
145-
},
146-
},
147157
],
148158
},
149159
{

packages/web/src/views/Day/components/DayCmdPalette.tsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { useState } from "react";
22
import _CommandPalette, { filterItems, getItemIndex } from "react-cmdk";
33
import "react-cmdk/dist/cmdk.css";
4+
import { useNavigate } from "react-router-dom";
45
import dayjs from "@core/util/date/dayjs";
56
import { moreCommandPaletteItems } from "@web/common/constants/more.cmd.constants";
7+
import { getNavigationCommandItems } from "@web/common/constants/navigation.cmd.constants";
68
import { VIEW_SHORTCUTS } from "@web/common/constants/shortcuts.constants";
79
import { useAuthCmdItems } from "@web/common/hooks/useAuthCmdItems";
810
import { useGoogleCmdItems } from "@web/common/hooks/useGoogleCmdItems";
911
import { useLogoutCmdItems } from "@web/common/hooks/useLogoutCmdItems";
10-
import { pressKey } from "@web/common/utils/dom/event-emitter.util";
1112
import { resolveDefaultExport } from "@web/common/utils/resolve-default-export.util";
1213
import { selectIsCmdPaletteOpen } from "@web/ducks/settings/selectors/settings.selectors";
1314
import { settingsSlice } from "@web/ducks/settings/slices/settings.slice";
@@ -25,6 +26,7 @@ interface DayCmdPaletteProps {
2526

2627
export const DayCmdPalette = ({ onGoToToday }: DayCmdPaletteProps) => {
2728
const dispatch = useAppDispatch();
29+
const navigate = useNavigate();
2830
const open = useAppSelector(selectIsCmdPaletteOpen);
2931
const [page] = useState<"root">("root");
3032
const [search, setSearch] = useState("");
@@ -38,19 +40,21 @@ export const DayCmdPalette = ({ onGoToToday }: DayCmdPaletteProps) => {
3840
{
3941
heading: "Navigation",
4042
id: "navigation",
41-
items: [
42-
{
43-
id: "go-to-now",
44-
children: `Go to Now [${VIEW_SHORTCUTS.now.key}]`,
45-
icon: "ClockIcon",
46-
onClick: () => pressKey(VIEW_SHORTCUTS.now.key),
43+
items: getNavigationCommandItems({
44+
currentView: "day",
45+
onGoToToday: () => {
46+
onGoToToday?.();
4747
},
48-
{
49-
id: "go-to-week",
50-
children: `Go to Week [${VIEW_SHORTCUTS.week.key}]`,
51-
icon: "CalendarIcon",
52-
onClick: () => pressKey(VIEW_SHORTCUTS.week.key),
48+
onNavigateToView: (viewName) => {
49+
navigate(VIEW_SHORTCUTS[viewName].route);
5350
},
51+
today,
52+
}),
53+
},
54+
{
55+
heading: "Common Tasks",
56+
id: "general",
57+
items: [
5458
{
5559
id: "create-event",
5660
children: "Create event",
@@ -63,14 +67,6 @@ export const DayCmdPalette = ({ onGoToToday }: DayCmdPaletteProps) => {
6367
icon: "PencilSquareIcon",
6468
onClick: () => queueMicrotask(openEventFormEditEvent),
6569
},
66-
{
67-
id: "today",
68-
children: `Go to Today (${today.format("dddd, MMMM D")}) [t]`,
69-
icon: "ArrowUturnDownIcon",
70-
onClick: () => {
71-
onGoToToday?.();
72-
},
73-
},
7470
],
7571
},
7672
{

packages/web/src/views/Now/components/NowCmdPalette.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { useState } from "react";
22
import _CommandPalette, { filterItems, getItemIndex } from "react-cmdk";
33
import "react-cmdk/dist/cmdk.css";
44
import { useNavigate } from "react-router-dom";
5+
import dayjs from "@core/util/date/dayjs";
56
import { moreCommandPaletteItems } from "@web/common/constants/more.cmd.constants";
7+
import { getNavigationCommandItems } from "@web/common/constants/navigation.cmd.constants";
68
import { VIEW_SHORTCUTS } from "@web/common/constants/shortcuts.constants";
79
import { useAuthCmdItems } from "@web/common/hooks/useAuthCmdItems";
810
import { useGoogleCmdItems } from "@web/common/hooks/useGoogleCmdItems";
@@ -22,6 +24,7 @@ export const NowCmdPalette = () => {
2224
const open = useAppSelector(selectIsCmdPaletteOpen);
2325
const [page] = useState<"root">("root");
2426
const [search, setSearch] = useState("");
27+
const today = dayjs();
2528
const authCmdItems = useAuthCmdItems();
2629
const googleCmdItems = useGoogleCmdItems();
2730
const logoutCmdItems = useLogoutCmdItems();
@@ -31,19 +34,19 @@ export const NowCmdPalette = () => {
3134
{
3235
heading: "Navigation",
3336
id: "navigation",
34-
items: [
35-
{
36-
id: "go-to-day",
37-
children: `Go to Day [${VIEW_SHORTCUTS.day.key}]`,
38-
icon: "CalendarDaysIcon",
39-
onClick: () => navigate(VIEW_SHORTCUTS.day.route),
40-
},
41-
{
42-
id: "go-to-week",
43-
children: `Go to Week [${VIEW_SHORTCUTS.week.key}]`,
44-
icon: "CalendarIcon",
45-
onClick: () => navigate(VIEW_SHORTCUTS.week.route),
37+
items: getNavigationCommandItems({
38+
currentView: "now",
39+
onGoToToday: () => navigate(VIEW_SHORTCUTS.day.route),
40+
onNavigateToView: (viewName) => {
41+
navigate(VIEW_SHORTCUTS[viewName].route);
4642
},
43+
today,
44+
}),
45+
},
46+
{
47+
heading: "Common Tasks",
48+
id: "general",
49+
items: [
4750
{
4851
id: "edit-reminder",
4952
children: `Edit Reminder [r]`,

packages/web/src/views/Now/context/NowViewProvider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export function NowViewProvider({
155155
onToggleSidebar,
156156
onEscape: handleEscape,
157157
onEditReminder: handleEditReminder,
158+
onGoToToday: handleEscape,
158159
});
159160

160161
const value: NowViewContextValue = {

packages/web/src/views/Now/shortcuts/useNowShortcuts.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ describe("useNowShortcuts", () => {
8383
});
8484
});
8585

86+
it("navigates to today when T is pressed", async () => {
87+
const onGoToToday = mock();
88+
renderHook(() => useNowShortcuts({ onGoToToday }), { wrapper });
89+
90+
pressKey("t");
91+
92+
await waitFor(() => {
93+
expect(onGoToToday).toHaveBeenCalledTimes(1);
94+
});
95+
});
96+
8697
it("does not navigate when Escape is pressed inside a textarea", async () => {
8798
const onEscape = mock();
8899
renderHook(() => useNowShortcuts({ onEscape }), { wrapper });

packages/web/src/views/Now/shortcuts/useNowShortcuts.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface Props {
2020
onToggleSidebar?: () => void;
2121
onEscape?: () => void;
2222
onEditReminder?: () => void;
23+
onGoToToday?: () => void;
2324
}
2425

2526
export function useNowShortcuts(props?: Props) {
@@ -32,6 +33,7 @@ export function useNowShortcuts(props?: Props) {
3233
onToggleSidebar,
3334
onEscape,
3435
onEditReminder,
36+
onGoToToday,
3537
} = props || {};
3638

3739
const handleTaskNavigation = useCallback(
@@ -88,6 +90,10 @@ export function useNowShortcuts(props?: Props) {
8890
{ ignoreInputs: true },
8991
);
9092

93+
useAppHotkeyUp("T", () => {
94+
onGoToToday?.();
95+
});
96+
9197
useAppHotkeySequence(["E", "R"], () => {
9298
onEditReminder?.();
9399
});

0 commit comments

Comments
 (0)