@@ -4,9 +4,11 @@ import type {Meta, StoryObj} from "@storybook/react";
44
55import magnifyingGlass from "@phosphor-icons/core/regular/magnifying-glass.svg" ;
66import caretRight from "@phosphor-icons/core/regular/caret-right.svg" ;
7+ import clock from "@phosphor-icons/core/regular/clock.svg" ;
78
89import { action } from "@storybook/addon-actions" ;
910import { View } from "@khanacademy/wonder-blocks-core" ;
11+ import { BodyText } from "@khanacademy/wonder-blocks-typography" ;
1012import Button , { ActivityButton } from "@khanacademy/wonder-blocks-button" ;
1113
1214import 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+ } ;
0 commit comments