Skip to content

Commit 6bc8a1e

Browse files
committed
feat(web): enhance keyboard navigation in SomedaySandbox with task validation
1 parent 7030ea9 commit 6bc8a1e

File tree

3 files changed

+167
-12
lines changed

3 files changed

+167
-12
lines changed

packages/web/src/views/Onboarding/OnboardingDemo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
SetReminderSuccess,
1414
SetSomedayEventTwo,
1515
SetSomedayEventsOne,
16-
SetSomedayEventsSuccess,
1716
SignInWithGoogle,
1817
SignInWithGooglePrelude,
1918
WaitlistCheck,
@@ -118,6 +117,7 @@ const OnboardingDemo_: React.FC = () => {
118117
id: "someday-sandbox",
119118
component: (props: OnboardingStepProps) => <SomedaySandbox {...props} />,
120119
preventNavigation: true,
120+
handlesKeyboardEvents: true,
121121
},
122122

123123
{

packages/web/src/views/Onboarding/steps/events/SomedaySandbox.test.tsx

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import React from "react";
22
import "@testing-library/jest-dom";
3-
import { screen } from "@testing-library/react";
3+
import { screen, waitFor } from "@testing-library/react";
44
import userEvent from "@testing-library/user-event";
55
import { render } from "@web/__tests__/__mocks__/mock.render";
66
import { withProvider } from "../../components/OnboardingContext";
77
import { SomedaySandbox } from "./SomedaySandbox";
88

9+
// Mock the createAndSubmitEvents function
10+
jest.mock("./someday-sandbox.util", () => ({
11+
createAndSubmitEvents: jest.fn().mockResolvedValue(undefined),
12+
}));
13+
914
// Wrap the component with OnboardingProvider
1015
const SomedaySandboxWithProvider = withProvider(SomedaySandbox);
1116

@@ -22,6 +27,11 @@ const defaultProps = {
2227
};
2328

2429
describe("SomedaySandbox", () => {
30+
beforeEach(() => {
31+
// Clear all mocks before each test
32+
jest.clearAllMocks();
33+
});
34+
2535
function setup() {
2636
render(<SomedaySandboxWithProvider {...defaultProps} />);
2737
// The first input is for "This Week", the second for "This Month"
@@ -74,4 +84,112 @@ describe("SomedaySandbox", () => {
7484
await userEvent.tab(); // move focus away to trigger blur
7585
expect(screen.getByText("Blur month task")).toBeInTheDocument();
7686
});
87+
88+
it("should call createAndSubmitEvents and onNext when right arrow is pressed and tasks are ready", async () => {
89+
const { createAndSubmitEvents } = require("./someday-sandbox.util");
90+
const mockOnNext = jest.fn();
91+
const props = { ...defaultProps, onNext: mockOnNext };
92+
93+
render(<SomedaySandboxWithProvider {...props} />);
94+
95+
// Add enough tasks to make both checkboxes ready
96+
const weekInput = screen.getAllByPlaceholderText("Add new task...")[0];
97+
const monthInput = screen.getAllByPlaceholderText("Add new task...")[1];
98+
99+
// Add week task
100+
await userEvent.type(weekInput, "Week task{enter}");
101+
102+
// Add month task
103+
await userEvent.type(monthInput, "Month task{enter}");
104+
105+
// Wait for checkboxes to be checked
106+
await waitFor(() => {
107+
expect(screen.getByText("Week task")).toBeInTheDocument();
108+
expect(screen.getByText("Month task")).toBeInTheDocument();
109+
});
110+
111+
// Press right arrow key
112+
await userEvent.keyboard("{ArrowRight}");
113+
114+
// Wait for the async operations to complete
115+
await waitFor(() => {
116+
expect(createAndSubmitEvents).toHaveBeenCalled();
117+
expect(mockOnNext).toHaveBeenCalled();
118+
});
119+
});
120+
121+
it("should call createAndSubmitEvents and onNext when Enter is pressed and tasks are ready", async () => {
122+
const { createAndSubmitEvents } = require("./someday-sandbox.util");
123+
const mockOnNext = jest.fn();
124+
const props = { ...defaultProps, onNext: mockOnNext };
125+
126+
render(<SomedaySandboxWithProvider {...props} />);
127+
128+
// Add enough tasks to make both checkboxes ready
129+
const weekInput = screen.getAllByPlaceholderText("Add new task...")[0];
130+
const monthInput = screen.getAllByPlaceholderText("Add new task...")[1];
131+
132+
// Add week task
133+
await userEvent.type(weekInput, "Week task{enter}");
134+
135+
// Add month task
136+
await userEvent.type(monthInput, "Month task{enter}");
137+
138+
// Wait for checkboxes to be checked
139+
await waitFor(() => {
140+
expect(screen.getByText("Week task")).toBeInTheDocument();
141+
expect(screen.getByText("Month task")).toBeInTheDocument();
142+
});
143+
144+
// Press Enter key (not focused on input)
145+
await userEvent.keyboard("{Enter}");
146+
147+
// Wait for the async operations to complete
148+
await waitFor(() => {
149+
expect(createAndSubmitEvents).toHaveBeenCalled();
150+
expect(mockOnNext).toHaveBeenCalled();
151+
});
152+
});
153+
154+
it("should not navigate when right arrow is pressed with default tasks but checkboxes not ready", async () => {
155+
const { createAndSubmitEvents } = require("./someday-sandbox.util");
156+
const mockOnNext = jest.fn();
157+
const props = { ...defaultProps, onNext: mockOnNext };
158+
159+
render(<SomedaySandboxWithProvider {...props} />);
160+
161+
// Clear the mock to reset any previous calls
162+
createAndSubmitEvents.mockClear();
163+
mockOnNext.mockClear();
164+
165+
// Press right arrow key - the component starts with default tasks but checkboxes are not ready
166+
await userEvent.keyboard("{ArrowRight}");
167+
168+
// Should not call createAndSubmitEvents or onNext because checkboxes are not ready
169+
expect(createAndSubmitEvents).not.toHaveBeenCalled();
170+
expect(mockOnNext).not.toHaveBeenCalled();
171+
});
172+
173+
it("should not navigate when right arrow is pressed with unsaved changes", async () => {
174+
const { createAndSubmitEvents } = require("./someday-sandbox.util");
175+
const mockOnNext = jest.fn();
176+
const props = { ...defaultProps, onNext: mockOnNext };
177+
178+
render(<SomedaySandboxWithProvider {...props} />);
179+
180+
// Clear the mock to reset any previous calls
181+
createAndSubmitEvents.mockClear();
182+
mockOnNext.mockClear();
183+
184+
// Type in an input but don't submit (unsaved changes)
185+
const weekInput = screen.getAllByPlaceholderText("Add new task...")[0];
186+
await userEvent.type(weekInput, "Unsaved task");
187+
188+
// Press right arrow key
189+
await userEvent.keyboard("{ArrowRight}");
190+
191+
// Should not call createAndSubmitEvents or onNext due to unsaved changes
192+
expect(createAndSubmitEvents).not.toHaveBeenCalled();
193+
expect(mockOnNext).not.toHaveBeenCalled();
194+
});
77195
});

packages/web/src/views/Onboarding/steps/events/SomedaySandbox.tsx

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ export const SomedaySandbox: React.FC<OnboardingStepProps> = ({
177177
colorByPriority.relationships,
178178
];
179179

180+
const [weekTasks, setWeekTasks] = useState([
181+
{ text: "💸 File taxes", color: colorByPriority.work },
182+
{ text: "🥗 Get groceries", color: colorByPriority.self },
183+
]);
184+
const [monthTasks, setMonthTasks] = useState([
185+
{ text: "🤖 Start AI course", color: colorByPriority.work },
186+
{ text: "🏠 Book Airbnb", color: colorByPriority.relationships },
187+
{ text: "📚 Return library books", color: colorByPriority.self },
188+
]);
189+
const [newWeekTask, setNewWeekTask] = useState("");
190+
const [newMonthTask, setNewMonthTask] = useState("");
191+
180192
useEffect(() => {
181193
setIsHeaderAnimating(true);
182194
setTimeout(() => setIsHeaderAnimating(false), 2500);
@@ -197,17 +209,42 @@ export const SomedaySandbox: React.FC<OnboardingStepProps> = ({
197209
}
198210
};
199211

200-
const [weekTasks, setWeekTasks] = useState([
201-
{ text: "💸 File taxes", color: colorByPriority.work },
202-
{ text: "🥗 Get groceries", color: colorByPriority.self },
203-
]);
204-
const [monthTasks, setMonthTasks] = useState([
205-
{ text: "🤖 Start AI course", color: colorByPriority.work },
206-
{ text: "🏠 Book Airbnb", color: colorByPriority.relationships },
207-
{ text: "📚 Return library books", color: colorByPriority.self },
212+
// Handle keyboard events for this step
213+
useEffect(() => {
214+
const handleKeyDown = (event: KeyboardEvent) => {
215+
const isRightArrow = event.key === "ArrowRight";
216+
const isEnter = event.key === "Enter";
217+
218+
if (isRightArrow || isEnter) {
219+
// Check if we should prevent navigation
220+
const hasUnsavedChanges =
221+
newWeekTask.trim() !== "" || newMonthTask.trim() !== "";
222+
const checkboxesNotChecked = !isWeekTaskReady || !isMonthTaskReady;
223+
const shouldPrevent = hasUnsavedChanges || checkboxesNotChecked;
224+
225+
if (shouldPrevent) {
226+
event.preventDefault();
227+
event.stopPropagation();
228+
return;
229+
}
230+
231+
// If all conditions are met, call our custom handleNext
232+
event.preventDefault();
233+
event.stopPropagation();
234+
handleNext();
235+
}
236+
};
237+
238+
document.addEventListener("keydown", handleKeyDown);
239+
return () => document.removeEventListener("keydown", handleKeyDown);
240+
}, [
241+
newWeekTask,
242+
newMonthTask,
243+
isWeekTaskReady,
244+
isMonthTaskReady,
245+
weekTasks,
246+
monthTasks,
208247
]);
209-
const [newWeekTask, setNewWeekTask] = useState("");
210-
const [newMonthTask, setNewMonthTask] = useState("");
211248
const monthInputRef = useRef<HTMLInputElement>(null);
212249

213250
// Update navigation prevention based on state

0 commit comments

Comments
 (0)