Skip to content

Commit d5cf873

Browse files
committed
feat(web): enhance onboarding experience with new steps and improved detection
- Introduced new onboarding steps for CmdPaletteGuide, including steps 5 and 6, to guide users through using the command palette and navigating to the week view. - Updated related hooks and components to manage the new steps, ensuring a cohesive onboarding experience. - Enhanced utility functions for onboarding progress tracking, incorporating new steps into the existing structure. - Added comprehensive tests for the new steps and detection logic, validating correct functionality and user guidance throughout the onboarding process. - Refactored existing tests to accommodate the changes and ensure robust coverage of the onboarding experience.
1 parent e5b122c commit d5cf873

17 files changed

+467
-133
lines changed

packages/web/src/views/Calendar/components/Header/Reminder/Reminder.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ export const Reminder = forwardRef(
253253
const handleReminderKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
254254
if (e.key === "Enter") {
255255
e.preventDefault();
256+
e.stopPropagation(); // Prevent the Enter key from bubbling up to global shortcuts
257+
// Blur the input immediately to prevent global Enter handlers from firing
258+
e.currentTarget.blur();
256259
setIsEditing(false);
257260
// Save to localStorage on ENTER
258261
const latestValue = e.currentTarget.textContent || "";

packages/web/src/views/Day/hooks/onboarding/useAuthPrompt.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ interface UseAuthPromptProps {
1111
tasks: Array<{ id: string }>;
1212
hasNavigatedDates: boolean;
1313
showOnboardingOverlay: boolean;
14-
showCmdPaletteTutorial: boolean;
1514
}
1615

1716
interface UseAuthPromptReturn {
@@ -28,7 +27,6 @@ export function useAuthPrompt({
2827
tasks,
2928
hasNavigatedDates,
3029
showOnboardingOverlay,
31-
showCmdPaletteTutorial,
3230
}: UseAuthPromptProps): UseAuthPromptReturn {
3331
const { authenticated } = useSession();
3432
const isCmdPaletteOpen = useAppSelector(selectIsCmdPaletteOpen);
@@ -48,7 +46,7 @@ export function useAuthPrompt({
4846
const shouldShow =
4947
tasks.length >= 2 || hasNavigatedDates || isCmdPaletteOpen;
5048

51-
if (shouldShow && !showOnboardingOverlay && !showCmdPaletteTutorial) {
49+
if (shouldShow && !showOnboardingOverlay) {
5250
// Delay showing auth prompt to avoid overwhelming user
5351
const timer = setTimeout(() => {
5452
setShowAuthPrompt(true);
@@ -62,7 +60,6 @@ export function useAuthPrompt({
6260
hasNavigatedDates,
6361
isCmdPaletteOpen,
6462
showOnboardingOverlay,
65-
showCmdPaletteTutorial,
6663
]);
6764

6865
const dismissAuthPrompt = () => {

packages/web/src/views/Day/hooks/onboarding/useOnboardingOverlays.test.tsx

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -72,51 +72,6 @@ describe("useOnboardingOverlays", () => {
7272
expect(result.current.showOnboardingOverlay).toBe(false);
7373
});
7474

75-
it("should show cmd palette tutorial after onboarding overlay is dismissed", async () => {
76-
updateOnboardingProgress({ isCompleted: true });
77-
const store = createTestStore();
78-
79-
const { result } = renderHook(
80-
() =>
81-
useOnboardingOverlays({
82-
tasks: [],
83-
hasNavigatedDates: false,
84-
}),
85-
{
86-
wrapper: ({ children }) => (
87-
<Provider store={store}>{children}</Provider>
88-
),
89-
},
90-
);
91-
92-
await waitFor(
93-
() => {
94-
expect(result.current.showCmdPaletteTutorial).toBe(true);
95-
},
96-
{ timeout: 2000 },
97-
);
98-
});
99-
100-
it("should not show cmd palette tutorial if already seen", () => {
101-
updateOnboardingProgress({ isSeen: true, isCompleted: true });
102-
const store = createTestStore();
103-
104-
const { result } = renderHook(
105-
() =>
106-
useOnboardingOverlays({
107-
tasks: [],
108-
hasNavigatedDates: false,
109-
}),
110-
{
111-
wrapper: ({ children }) => (
112-
<Provider store={store}>{children}</Provider>
113-
),
114-
},
115-
);
116-
117-
expect(result.current.showCmdPaletteTutorial).toBe(false);
118-
});
119-
12075
it("should show auth prompt after user creates 2+ tasks", async () => {
12176
updateOnboardingProgress({ isCompleted: true, isSeen: true });
12277
const store = createTestStore();
@@ -191,34 +146,6 @@ describe("useOnboardingOverlays", () => {
191146
expect(result.current.showAuthPrompt).toBe(false);
192147
});
193148

194-
it("should mark cmd palette as used when opened", async () => {
195-
updateOnboardingProgress({ isCompleted: true });
196-
const store = createTestStore(true); // cmd palette is open
197-
198-
const { result } = renderHook(
199-
() =>
200-
useOnboardingOverlays({
201-
tasks: [],
202-
hasNavigatedDates: false,
203-
}),
204-
{
205-
wrapper: ({ children }) => (
206-
<Provider store={store}>{children}</Provider>
207-
),
208-
},
209-
);
210-
211-
// Wait for the effect to run and mark tutorial as seen
212-
await waitFor(
213-
() => {
214-
const progress = getOnboardingProgress();
215-
expect(progress.isSeen).toBe(true);
216-
expect(result.current.showCmdPaletteTutorial).toBe(false);
217-
},
218-
{ timeout: 3000 },
219-
);
220-
});
221-
222149
it("should dismiss onboarding overlay and skip guide", async () => {
223150
const store = createTestStore();
224151

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { OnboardingStepName } from "@web/views/Onboarding/constants/onboarding.constants";
22
import { useAuthPrompt } from "./useAuthPrompt";
3-
import { useCmdPaletteTutorial } from "./useCmdPaletteTutorial";
43
import { useOnboardingOverlay } from "./useOnboardingOverlay";
54

65
interface UseOnboardingOverlaysProps {
@@ -11,17 +10,14 @@ interface UseOnboardingOverlaysProps {
1110
interface UseOnboardingOverlaysReturn {
1211
showOnboardingOverlay: boolean;
1312
currentStep: OnboardingStepName | null;
14-
showCmdPaletteTutorial: boolean;
1513
showAuthPrompt: boolean;
1614
dismissOnboardingOverlay: () => void;
17-
dismissCmdPaletteTutorial: () => void;
1815
dismissAuthPrompt: () => void;
19-
markCmdPaletteUsed: () => void;
2016
}
2117

2218
/**
2319
* Composes multiple onboarding overlay hooks into a single hook
24-
* Manages the display logic for onboarding overlay, cmd palette tutorial, and auth prompt
20+
* Manages the display logic for onboarding overlay and auth prompt
2521
*/
2622
export function useOnboardingOverlays({
2723
tasks,
@@ -30,29 +26,17 @@ export function useOnboardingOverlays({
3026
const { showOnboardingOverlay, currentStep, dismissOnboardingOverlay } =
3127
useOnboardingOverlay();
3228

33-
const {
34-
showCmdPaletteTutorial,
35-
dismissCmdPaletteTutorial,
36-
markCmdPaletteUsed,
37-
} = useCmdPaletteTutorial({
38-
showOnboardingOverlay,
39-
});
40-
4129
const { showAuthPrompt, dismissAuthPrompt } = useAuthPrompt({
4230
tasks,
4331
hasNavigatedDates,
4432
showOnboardingOverlay,
45-
showCmdPaletteTutorial,
4633
});
4734

4835
return {
4936
showOnboardingOverlay,
5037
currentStep,
51-
showCmdPaletteTutorial,
5238
showAuthPrompt,
5339
dismissOnboardingOverlay,
54-
dismissCmdPaletteTutorial,
5540
dismissAuthPrompt,
56-
markCmdPaletteUsed,
5741
};
5842
}

packages/web/src/views/Day/view/DayViewContent.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { useRefetch } from "@web/views/Calendar/hooks/useRefetch";
2020
import { StyledCalendar } from "@web/views/Calendar/styled";
2121
import { Agenda } from "@web/views/Day/components/Agenda/Agenda";
2222
import { AuthPrompt } from "@web/views/Day/components/AuthPrompt/AuthPrompt";
23-
import { CmdPaletteTutorial } from "@web/views/Day/components/CmdPaletteTutorial/CmdPaletteTutorial";
2423
import { DayCmdPalette } from "@web/views/Day/components/DayCmdPalette";
2524
import { Header } from "@web/views/Day/components/Header/Header";
2625
import { StorageInfoModal } from "@web/views/Day/components/StorageInfoModal/StorageInfoModal";
@@ -77,13 +76,7 @@ export const DayViewContent = memo(() => {
7776
}, [dateInView]);
7877

7978
// Onboarding overlays
80-
const {
81-
showCmdPaletteTutorial,
82-
showAuthPrompt,
83-
dismissCmdPaletteTutorial,
84-
dismissAuthPrompt,
85-
markCmdPaletteUsed,
86-
} = useOnboardingOverlays({
79+
const { showAuthPrompt, dismissAuthPrompt } = useOnboardingOverlays({
8780
tasks,
8881
hasNavigatedDates,
8982
});
@@ -184,14 +177,6 @@ export const DayViewContent = memo(() => {
184177

185178
{/* Onboarding overlays */}
186179
<CmdPaletteGuide />
187-
{showCmdPaletteTutorial && (
188-
<CmdPaletteTutorial
189-
onDismiss={() => {
190-
dismissCmdPaletteTutorial();
191-
markCmdPaletteUsed();
192-
}}
193-
/>
194-
)}
195180
{showAuthPrompt && <AuthPrompt onDismiss={dismissAuthPrompt} />}
196181

197182
<ShortcutsOverlay

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useCallback } from "react";
22
import { useNavigate } from "react-router-dom";
33
import { ROOT_ROUTES } from "@web/common/constants/routes";
4+
import { ID_REMINDER_INPUT } from "@web/common/constants/web.constants";
45
import {
56
useKeyDownEvent,
67
useKeyUpEvent,
@@ -69,10 +70,19 @@ export function useNowShortcuts(props?: Props) {
6970
deps: [handleTaskNavigation, onNextTask],
7071
});
7172

73+
const handleEnterKey = useCallback(() => {
74+
// Don't trigger if the reminder input is focused
75+
const activeElement = document.activeElement as HTMLElement | null;
76+
if (activeElement?.id === ID_REMINDER_INPUT) {
77+
return;
78+
}
79+
handleTaskNavigation(onCompleteTask)?.();
80+
}, [handleTaskNavigation, onCompleteTask]);
81+
7282
useKeyUpEvent({
7383
combination: ["Enter"],
74-
handler: handleTaskNavigation(onCompleteTask),
75-
deps: [handleTaskNavigation, onCompleteTask],
84+
handler: handleEnterKey,
85+
deps: [handleEnterKey],
7686
});
7787

7888
useKeyDownEvent({

packages/web/src/views/Onboarding/components/CmdPaletteGuide.test.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe("CmdPaletteGuide", () => {
152152
screen.getByText(/Press.*to go to the \/now view/i),
153153
).toBeInTheDocument();
154154
expect(screen.getByText("1")).toBeInTheDocument(); // The kbd element
155-
expect(screen.getByText("Step 2 of 4")).toBeInTheDocument();
155+
expect(screen.getByText("Step 2 of 6")).toBeInTheDocument();
156156
});
157157

158158
it("should show step 1 instructions on Now view when step 1 is not completed", () => {
@@ -173,7 +173,7 @@ describe("CmdPaletteGuide", () => {
173173
expect(screen.getByText("Welcome to the Now View")).toBeInTheDocument();
174174
expect(screen.getByText(/Type.*to create a task/i)).toBeInTheDocument();
175175
expect(screen.getByText("c")).toBeInTheDocument();
176-
expect(screen.getByText("Step 1 of 4")).toBeInTheDocument();
176+
expect(screen.getByText("Step 1 of 6")).toBeInTheDocument();
177177
});
178178

179179
it("should render step 2 instructions on Day view when step 1 is completed", () => {
@@ -222,7 +222,7 @@ describe("CmdPaletteGuide", () => {
222222
expect(screen.getByText("Welcome to the Day View")).toBeInTheDocument();
223223
expect(screen.getByText(/Type.*to create a task/i)).toBeInTheDocument();
224224
expect(screen.getByText("c")).toBeInTheDocument();
225-
expect(screen.getByText("Step 1 of 4")).toBeInTheDocument();
225+
expect(screen.getByText("Step 1 of 6")).toBeInTheDocument();
226226
});
227227

228228
it("should render step 3 instructions on Now view", () => {
@@ -242,7 +242,7 @@ describe("CmdPaletteGuide", () => {
242242
screen.getByText(/Press.*to edit the description/i),
243243
).toBeInTheDocument();
244244
expect(screen.getByText("d")).toBeInTheDocument(); // The kbd element
245-
expect(screen.getByText("Step 3 of 4")).toBeInTheDocument();
245+
expect(screen.getByText("Step 3 of 6")).toBeInTheDocument();
246246
});
247247

248248
it("should render step 4 instructions on Now view", () => {
@@ -265,7 +265,30 @@ describe("CmdPaletteGuide", () => {
265265
screen.getByText(/Press.*to edit the reminder/i),
266266
).toBeInTheDocument();
267267
expect(screen.getByText("r")).toBeInTheDocument(); // The kbd element
268-
expect(screen.getByText("Step 4 of 4")).toBeInTheDocument();
268+
expect(screen.getByText("Step 4 of 6")).toBeInTheDocument();
269+
});
270+
271+
it("should render step 5 instructions on Now view", () => {
272+
mockUseLocation.mockReturnValue({ pathname: "/now" } as any);
273+
markStepCompleted(ONBOARDING_STEPS.CREATE_TASK);
274+
markStepCompleted(ONBOARDING_STEPS.NAVIGATE_TO_NOW);
275+
markStepCompleted(ONBOARDING_STEPS.EDIT_DESCRIPTION);
276+
markStepCompleted(ONBOARDING_STEPS.EDIT_REMINDER);
277+
mockUseCmdPaletteGuide.mockReturnValue({
278+
currentStep: ONBOARDING_STEPS.CMD_PALETTE_INFO,
279+
isGuideActive: true,
280+
completeStep: jest.fn(),
281+
skipGuide: jest.fn(),
282+
completeGuide: jest.fn(),
283+
});
284+
285+
render(<CmdPaletteGuide />);
286+
287+
expect(screen.getByText("Welcome to the Now View")).toBeInTheDocument();
288+
expect(
289+
screen.getByText(/If you ever forget a shortcut/i),
290+
).toBeInTheDocument();
291+
expect(screen.getByText("Step 5 of 6")).toBeInTheDocument();
269292
});
270293

271294
it("should not render step 3 on Day view", () => {
@@ -385,9 +408,9 @@ describe("CmdPaletteGuide", () => {
385408

386409
// Check that progress dots are rendered
387410
const progressDots = screen
388-
.getByText("Step 2 of 4")
411+
.getByText("Step 2 of 6")
389412
.parentElement?.querySelectorAll("div[class*='rounded-full']");
390-
expect(progressDots).toHaveLength(4);
413+
expect(progressDots).toHaveLength(6);
391414
});
392415

393416
it("should show progress indicators on Day view", () => {
@@ -402,12 +425,12 @@ describe("CmdPaletteGuide", () => {
402425

403426
render(<CmdPaletteGuide />);
404427

405-
expect(screen.getByText("Step 1 of 4")).toBeInTheDocument();
428+
expect(screen.getByText("Step 1 of 6")).toBeInTheDocument();
406429
// Check that progress dots are rendered
407430
const progressDots = screen
408-
.getByText("Step 1 of 4")
431+
.getByText("Step 1 of 6")
409432
.parentElement?.querySelectorAll("div[class*='rounded-full']");
410-
expect(progressDots).toHaveLength(4);
433+
expect(progressDots).toHaveLength(6);
411434
});
412435

413436
it("should not render step 4 on Day view", () => {

0 commit comments

Comments
 (0)