Skip to content

Commit 0f3feaa

Browse files
authored
Add onMouseDown / onMouseDown / onMouseLeave to ActivityButton and Button (#2763)
## Summary: Adds support for mouse events for advanced animation purposes Issue: WB-2006 ## Test plan: 1. Review new `ActivityButton` story: /?path=/story/packages-button-activitybutton--press-duration-tracking&globals=viewport:desktop 2. Review new `Button` story: `/?path=/story/packages-button-button--press-duration-tracking` 3. Ensure tests pass Author: marcysutton Reviewers: marcysutton, beaesguerra Required Reviewers: Approved By: beaesguerra Checks: ✅ 12 checks were successful, ⏭️ 2 checks have been skipped Pull Request URL: #2763
1 parent 18715e1 commit 0f3feaa

File tree

13 files changed

+977
-55
lines changed

13 files changed

+977
-55
lines changed

.changeset/dirty-toys-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/wonder-blocks-button": minor
3+
---
4+
5+
Add onMouseUp, onMouseDown, and onMouseLeave to ActivityButton and Button to track duration of press before release

__docs__/wonder-blocks-button/activity-button.stories.tsx

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import type {Meta, StoryObj} from "@storybook/react";
44

55
import magnifyingGlass from "@phosphor-icons/core/regular/magnifying-glass.svg";
66
import caretRight from "@phosphor-icons/core/regular/caret-right.svg";
7+
import clock from "@phosphor-icons/core/regular/clock.svg";
78

89
import {action} from "@storybook/addon-actions";
910
import {View} from "@khanacademy/wonder-blocks-core";
11+
import {BodyText} from "@khanacademy/wonder-blocks-typography";
1012
import Button, {ActivityButton} from "@khanacademy/wonder-blocks-button";
1113

1214
import ComponentInfo from "../components/component-info";
@@ -186,3 +188,197 @@ export const ReceivingFocusProgrammatically: Story = {
186188
},
187189
},
188190
};
191+
192+
/**
193+
* This story demonstrates how to use the mouse event handlers (`onMouseDown`,
194+
* `onMouseUp`, and `onMouseLeave`) to track the duration of button presses.
195+
*
196+
* This is useful for analytics, accessibility features, or UI feedback that
197+
* depends on how long a user interacts with a button.
198+
*
199+
* **Use cases:**
200+
* - Measuring engagement time before click completion
201+
* - Detecting accidental clicks vs intentional presses
202+
* - Providing haptic feedback based on press duration
203+
* - Analytics tracking for user interaction patterns
204+
*
205+
* **Try it:** Press and hold the button for different lengths of time, or
206+
* press and drag away from the button to see how the events are tracked.
207+
*/
208+
export const PressDurationTracking: Story = {
209+
render: function Render(args) {
210+
const [pressStartTime, setPressStartTime] = React.useState<
211+
number | null
212+
>(null);
213+
const [pressDuration, setPressDuration] = React.useState<number | null>(
214+
null,
215+
);
216+
const [lastEvent, setLastEvent] = React.useState<string>("none");
217+
const [interactionHistory, setInteractionHistory] = React.useState<
218+
string[]
219+
>([]);
220+
221+
const logEvent = (eventName: string, duration?: number) => {
222+
const timestamp = new Date().toLocaleTimeString();
223+
const message =
224+
duration !== undefined
225+
? `${timestamp}: ${eventName} (${duration}ms)`
226+
: `${timestamp}: ${eventName}`;
227+
228+
setInteractionHistory((prev) => [message, ...prev.slice(0, 4)]); // Keep last 5 events
229+
setLastEvent(eventName);
230+
};
231+
232+
const baseActions = {
233+
onMouseDown: action("onMouseDown"),
234+
onMouseUp: action("onMouseUp"),
235+
onMouseLeave: action("onMouseLeave"),
236+
onClick: action("onClick"),
237+
onMouseEnter: action("onMouseEnter"),
238+
};
239+
240+
const handleMouseDown = (e: React.MouseEvent) => {
241+
const startTime = Date.now();
242+
setPressStartTime(startTime);
243+
setPressDuration(null);
244+
logEvent("Mouse Down - Press Started");
245+
baseActions.onMouseDown?.(e);
246+
};
247+
248+
const handleMouseUp = (e: React.MouseEvent) => {
249+
if (pressStartTime) {
250+
const duration = Date.now() - pressStartTime;
251+
setPressDuration(duration);
252+
logEvent("Mouse Up - Press Completed", duration);
253+
}
254+
baseActions.onMouseUp?.(e);
255+
};
256+
257+
const handleMouseEnter = (e: React.MouseEvent) => {
258+
logEvent("Mouse Enter");
259+
baseActions.onMouseEnter?.(e);
260+
};
261+
262+
const handleMouseLeave = (e: React.MouseEvent) => {
263+
if (pressStartTime) {
264+
const duration = Date.now() - pressStartTime;
265+
setPressDuration(duration);
266+
logEvent("Mouse Leave - Press Abandoned", duration);
267+
}
268+
setPressStartTime(null);
269+
baseActions.onMouseLeave?.(e);
270+
};
271+
272+
const handleClick = (e: React.SyntheticEvent) => {
273+
logEvent("Click - Action Executed");
274+
baseActions.onClick?.(e);
275+
};
276+
277+
const resetTracking = () => {
278+
setPressStartTime(null);
279+
setPressDuration(null);
280+
setLastEvent("none");
281+
setInteractionHistory([]);
282+
};
283+
284+
const isCurrentlyPressed =
285+
pressStartTime !== null &&
286+
lastEvent === "Mouse Down - Press Started";
287+
288+
return (
289+
<View style={{gap: sizing.size_240}}>
290+
<View
291+
style={{
292+
gap: sizing.size_160,
293+
flexDirection: "row",
294+
alignItems: "center",
295+
}}
296+
>
297+
<ActivityButton
298+
{...args}
299+
startIcon={clock}
300+
onMouseEnter={handleMouseEnter}
301+
onMouseDown={handleMouseDown}
302+
onMouseUp={handleMouseUp}
303+
onMouseLeave={handleMouseLeave}
304+
onClick={handleClick}
305+
>
306+
{isCurrentlyPressed
307+
? "Pressed!"
308+
: "Track Press Duration"}
309+
</ActivityButton>
310+
311+
<Button
312+
kind="secondary"
313+
size="small"
314+
onClick={resetTracking}
315+
>
316+
Reset
317+
</Button>
318+
</View>
319+
320+
<View
321+
style={{
322+
gap: sizing.size_120,
323+
padding: sizing.size_160,
324+
backgroundColor:
325+
semanticColor.core.background.neutral.subtle,
326+
borderRadius: sizing.size_080,
327+
minHeight: "120px",
328+
}}
329+
>
330+
<BodyText size="medium" weight="semi">
331+
Press Tracking Information
332+
</BodyText>
333+
334+
<View style={{gap: sizing.size_060}}>
335+
<BodyText>
336+
<strong>Current State:</strong>{" "}
337+
{isCurrentlyPressed
338+
? `Pressed (${pressStartTime ? Math.round((Date.now() - pressStartTime) / 10) * 10 : 0}ms+)`
339+
: "Released"}
340+
</BodyText>
341+
342+
{pressDuration !== null && (
343+
<BodyText>
344+
<strong>Last Press Duration:</strong>{" "}
345+
{pressDuration}ms
346+
</BodyText>
347+
)}
348+
349+
<BodyText>
350+
<strong>Last Event:</strong> {lastEvent}
351+
</BodyText>
352+
</View>
353+
354+
{interactionHistory.length > 0 && (
355+
<View style={{gap: sizing.size_040}}>
356+
<BodyText weight="semi">Recent Events:</BodyText>
357+
{interactionHistory.map((event, index) => (
358+
<BodyText
359+
key={index}
360+
size="small"
361+
style={{
362+
opacity: 1 - index * 0.15,
363+
fontFamily: "monospace",
364+
}}
365+
>
366+
{event}
367+
</BodyText>
368+
))}
369+
</View>
370+
)}
371+
</View>
372+
</View>
373+
);
374+
},
375+
args: {
376+
kind: "primary",
377+
},
378+
parameters: {
379+
chromatic: {
380+
// Disable snapshots since this story is interactive and shows timing
381+
disableSnapshot: true,
382+
},
383+
},
384+
};

__docs__/wonder-blocks-button/button-shared.argtypes.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,42 @@ export default {
4444
},
4545
},
4646
},
47+
onMouseDown: {
48+
action: "onMouseDown",
49+
table: {
50+
category: "Events",
51+
type: {
52+
summary: "(e: React.MouseEvent) => unknown",
53+
},
54+
},
55+
},
56+
onMouseUp: {
57+
action: "onMouseUp",
58+
table: {
59+
category: "Events",
60+
type: {
61+
summary: "(e: React.MouseEvent) => unknown",
62+
},
63+
},
64+
},
65+
onMouseEnter: {
66+
action: "onMouseEnter",
67+
table: {
68+
category: "Events",
69+
type: {
70+
summary: "(e: React.MouseEvent) => unknown",
71+
},
72+
},
73+
},
74+
onMouseLeave: {
75+
action: "onMouseLeave",
76+
table: {
77+
category: "Events",
78+
type: {
79+
summary: "(e: React.MouseEvent) => unknown",
80+
},
81+
},
82+
},
4783
/**
4884
* Navigation
4985
*/

0 commit comments

Comments
 (0)