0
# Event System
1
2
Custom event system with propagation control, pointer type awareness, and comprehensive interaction support including press, hover, focus, keyboard, and move events.
3
4
## Capabilities
5
6
### Base Event System
7
8
Enhanced event types with propagation control and pointer type awareness.
9
10
```typescript { .api }
11
/**
12
* Base event type with propagation control
13
* @template T The underlying synthetic event type
14
*/
15
type BaseEvent<T extends SyntheticEvent> = T & {
16
/** @deprecated Use continuePropagation */
17
stopPropagation(): void;
18
/** Allow event to continue propagating to parent elements */
19
continuePropagation(): void;
20
};
21
22
/** Enhanced keyboard event with propagation control */
23
type KeyboardEvent = BaseEvent<ReactKeyboardEvent<any>>;
24
25
/** Pointer interaction types */
26
type PointerType = "mouse" | "pen" | "touch" | "keyboard" | "virtual";
27
```
28
29
### Press Events
30
31
Press events provide a unified interaction model across different input methods.
32
33
```typescript { .api }
34
/**
35
* Press event details
36
*/
37
interface PressEvent {
38
/** The type of press event being fired */
39
type: "pressstart" | "pressend" | "pressup" | "press";
40
/** The pointer type that triggered the press event */
41
pointerType: PointerType;
42
/** The target element of the press event */
43
target: Element;
44
/** Whether the shift keyboard modifier was held during the press event */
45
shiftKey: boolean;
46
/** Whether the ctrl keyboard modifier was held during the press event */
47
ctrlKey: boolean;
48
/** Whether the meta keyboard modifier was held during the press event */
49
metaKey: boolean;
50
/** Whether the alt keyboard modifier was held during the press event */
51
altKey: boolean;
52
/** X position relative to the target */
53
x: number;
54
/** Y position relative to the target */
55
y: number;
56
/**
57
* By default, press events stop propagation to parent elements.
58
* In cases where a handler decides not to handle a specific event,
59
* it can call continuePropagation() to allow a parent to handle it.
60
*/
61
continuePropagation(): void;
62
}
63
64
/**
65
* Long press event (extends PressEvent but omits type and continuePropagation)
66
*/
67
interface LongPressEvent extends Omit<PressEvent, "type" | "continuePropagation"> {
68
/** The type of long press event being fired */
69
type: "longpressstart" | "longpressend" | "longpress";
70
}
71
72
/**
73
* Press event handlers
74
*/
75
interface PressEvents {
76
/** Handler that is called when the press is released over the target */
77
onPress?: (e: PressEvent) => void;
78
/** Handler that is called when a press interaction starts */
79
onPressStart?: (e: PressEvent) => void;
80
/**
81
* Handler that is called when a press interaction ends, either
82
* over the target or when the pointer leaves the target
83
*/
84
onPressEnd?: (e: PressEvent) => void;
85
/** Handler that is called when the press state changes */
86
onPressChange?: (isPressed: boolean) => void;
87
/**
88
* Handler that is called when a press is released over the target, regardless of
89
* whether it started on the target or not
90
*/
91
onPressUp?: (e: PressEvent) => void;
92
/**
93
* Not recommended – use onPress instead. onClick is an alias for onPress
94
* provided for compatibility with other libraries. onPress provides
95
* additional event details for non-mouse interactions.
96
*/
97
onClick?: (e: MouseEvent<FocusableElement>) => void;
98
}
99
```
100
101
### Hover Events
102
103
Hover events for mouse and pen interactions.
104
105
```typescript { .api }
106
/**
107
* Hover event details
108
*/
109
interface HoverEvent {
110
/** The type of hover event being fired */
111
type: "hoverstart" | "hoverend";
112
/** The pointer type that triggered the hover event */
113
pointerType: "mouse" | "pen";
114
/** The target element of the hover event */
115
target: HTMLElement;
116
}
117
118
/**
119
* Hover event handlers
120
*/
121
interface HoverEvents {
122
/** Handler that is called when a hover interaction starts */
123
onHoverStart?: (e: HoverEvent) => void;
124
/** Handler that is called when a hover interaction ends */
125
onHoverEnd?: (e: HoverEvent) => void;
126
/** Handler that is called when the hover state changes */
127
onHoverChange?: (isHovering: boolean) => void;
128
}
129
```
130
131
### Focus Events
132
133
Focus event handlers with focus state tracking.
134
135
```typescript { .api }
136
/**
137
* Focus event handlers
138
* @template Target The type of the target element
139
*/
140
interface FocusEvents<Target = Element> {
141
/** Handler that is called when the element receives focus */
142
onFocus?: (e: FocusEvent<Target>) => void;
143
/** Handler that is called when the element loses focus */
144
onBlur?: (e: FocusEvent<Target>) => void;
145
/** Handler that is called when the element's focus status changes */
146
onFocusChange?: (isFocused: boolean) => void;
147
}
148
149
/**
150
* Properties for focusable elements
151
* @template Target The type of the target element
152
*/
153
interface FocusableProps<Target = Element> extends FocusEvents<Target>, KeyboardEvents {
154
/** Whether the element should receive focus on render */
155
autoFocus?: boolean;
156
}
157
```
158
159
### Keyboard Events
160
161
Keyboard event handlers.
162
163
```typescript { .api }
164
/**
165
* Keyboard event handlers
166
*/
167
interface KeyboardEvents {
168
/** Handler that is called when a key is pressed */
169
onKeyDown?: (e: KeyboardEvent) => void;
170
/** Handler that is called when a key is released */
171
onKeyUp?: (e: KeyboardEvent) => void;
172
}
173
```
174
175
### Move Events
176
177
Move events for drag-like interactions without drag and drop.
178
179
```typescript { .api }
180
/**
181
* Base properties for move events
182
*/
183
interface BaseMoveEvent {
184
/** The pointer type that triggered the move event */
185
pointerType: PointerType;
186
/** Whether the shift keyboard modifier was held during the move event */
187
shiftKey: boolean;
188
/** Whether the ctrl keyboard modifier was held during the move event */
189
ctrlKey: boolean;
190
/** Whether the meta keyboard modifier was held during the move event */
191
metaKey: boolean;
192
/** Whether the alt keyboard modifier was held during the move event */
193
altKey: boolean;
194
}
195
196
/**
197
* Move start event
198
*/
199
interface MoveStartEvent extends BaseMoveEvent {
200
/** The type of move event being fired */
201
type: "movestart";
202
}
203
204
/**
205
* Move event with delta information
206
*/
207
interface MoveMoveEvent extends BaseMoveEvent {
208
/** The type of move event being fired */
209
type: "move";
210
/** The amount moved in the X direction since the last event */
211
deltaX: number;
212
/** The amount moved in the Y direction since the last event */
213
deltaY: number;
214
}
215
216
/**
217
* Move end event
218
*/
219
interface MoveEndEvent extends BaseMoveEvent {
220
/** The type of move event being fired */
221
type: "moveend";
222
}
223
224
/** Union of all move event types */
225
type MoveEvent = MoveStartEvent | MoveMoveEvent | MoveEndEvent;
226
227
/**
228
* Move event handlers
229
*/
230
interface MoveEvents {
231
/** Handler that is called when a move interaction starts */
232
onMoveStart?: (e: MoveStartEvent) => void;
233
/** Handler that is called when the element is moved */
234
onMove?: (e: MoveMoveEvent) => void;
235
/** Handler that is called when a move interaction ends */
236
onMoveEnd?: (e: MoveEndEvent) => void;
237
}
238
```
239
240
### Scroll Events
241
242
Scroll event support with delta information.
243
244
```typescript { .api }
245
/**
246
* Scroll event details
247
*/
248
interface ScrollEvent {
249
/** The amount moved in the X direction since the last event */
250
deltaX: number;
251
/** The amount moved in the Y direction since the last event */
252
deltaY: number;
253
}
254
255
/**
256
* Scroll event handlers
257
*/
258
interface ScrollEvents {
259
/** Handler that is called when the scroll wheel moves */
260
onScroll?: (e: ScrollEvent) => void;
261
}
262
```
263
264
**Usage Examples:**
265
266
```typescript
267
import {
268
PressEvents,
269
HoverEvents,
270
FocusableProps,
271
MoveEvents,
272
PressEvent,
273
HoverEvent,
274
KeyboardEvent
275
} from "@react-types/shared";
276
277
// Interactive button with press and hover support
278
interface InteractiveButtonProps extends PressEvents, HoverEvents, FocusableProps {
279
children: React.ReactNode;
280
isDisabled?: boolean;
281
}
282
283
function InteractiveButton({
284
children,
285
isDisabled,
286
onPress,
287
onPressStart,
288
onPressEnd,
289
onHoverStart,
290
onHoverEnd,
291
onFocus,
292
onBlur,
293
onKeyDown,
294
autoFocus
295
}: InteractiveButtonProps) {
296
const [isPressed, setIsPressed] = useState(false);
297
const [isHovered, setIsHovered] = useState(false);
298
const [isFocused, setIsFocused] = useState(false);
299
300
const handlePress = (e: PressEvent) => {
301
if (isDisabled) return;
302
console.log(`Pressed with ${e.pointerType} at (${e.x}, ${e.y})`);
303
onPress?.(e);
304
};
305
306
const handlePressStart = (e: PressEvent) => {
307
if (isDisabled) return;
308
setIsPressed(true);
309
onPressStart?.(e);
310
};
311
312
const handlePressEnd = (e: PressEvent) => {
313
setIsPressed(false);
314
onPressEnd?.(e);
315
};
316
317
const handleHoverStart = (e: HoverEvent) => {
318
if (isDisabled) return;
319
setIsHovered(true);
320
onHoverStart?.(e);
321
};
322
323
const handleHoverEnd = (e: HoverEvent) => {
324
setIsHovered(false);
325
onHoverEnd?.(e);
326
};
327
328
const handleKeyDown = (e: KeyboardEvent) => {
329
if (isDisabled) return;
330
if (e.key === "Enter" || e.key === " ") {
331
// Simulate press event for keyboard interaction
332
handlePress({
333
type: "press",
334
pointerType: "keyboard",
335
target: e.target as Element,
336
shiftKey: e.shiftKey,
337
ctrlKey: e.ctrlKey,
338
metaKey: e.metaKey,
339
altKey: e.altKey,
340
x: 0,
341
y: 0,
342
continuePropagation: () => {}
343
});
344
}
345
onKeyDown?.(e);
346
};
347
348
return (
349
<button
350
disabled={isDisabled}
351
autoFocus={autoFocus}
352
onMouseDown={(e) => handlePressStart({
353
type: "pressstart",
354
pointerType: "mouse",
355
target: e.target as Element,
356
shiftKey: e.shiftKey,
357
ctrlKey: e.ctrlKey,
358
metaKey: e.metaKey,
359
altKey: e.altKey,
360
x: e.clientX,
361
y: e.clientY,
362
continuePropagation: () => {}
363
})}
364
onMouseUp={(e) => handlePressEnd({
365
type: "pressend",
366
pointerType: "mouse",
367
target: e.target as Element,
368
shiftKey: e.shiftKey,
369
ctrlKey: e.ctrlKey,
370
metaKey: e.metaKey,
371
altKey: e.altKey,
372
x: e.clientX,
373
y: e.clientY,
374
continuePropagation: () => {}
375
})}
376
onClick={(e) => handlePress({
377
type: "press",
378
pointerType: "mouse",
379
target: e.target as Element,
380
shiftKey: e.shiftKey,
381
ctrlKey: e.ctrlKey,
382
metaKey: e.metaKey,
383
altKey: e.altKey,
384
x: e.clientX,
385
y: e.clientY,
386
continuePropagation: () => {}
387
})}
388
onMouseEnter={(e) => handleHoverStart({
389
type: "hoverstart",
390
pointerType: "mouse",
391
target: e.target as HTMLElement
392
})}
393
onMouseLeave={(e) => handleHoverEnd({
394
type: "hoverend",
395
pointerType: "mouse",
396
target: e.target as HTMLElement
397
})}
398
onFocus={(e) => {
399
setIsFocused(true);
400
onFocus?.(e);
401
}}
402
onBlur={(e) => {
403
setIsFocused(false);
404
onBlur?.(e);
405
}}
406
onKeyDown={handleKeyDown}
407
style={{
408
backgroundColor: isPressed ? "#ccc" : isHovered ? "#eee" : "#fff",
409
outline: isFocused ? "2px solid blue" : "none",
410
opacity: isDisabled ? 0.5 : 1
411
}}
412
>
413
{children}
414
</button>
415
);
416
}
417
418
// Draggable element with move events
419
interface DraggableProps extends MoveEvents {
420
children: React.ReactNode;
421
}
422
423
function Draggable({ children, onMoveStart, onMove, onMoveEnd }: DraggableProps) {
424
const [position, setPosition] = useState({ x: 0, y: 0 });
425
const [isDragging, setIsDragging] = useState(false);
426
427
const handleMoveStart = (e: MouseEvent) => {
428
setIsDragging(true);
429
onMoveStart?.({
430
type: "movestart",
431
pointerType: "mouse",
432
shiftKey: e.shiftKey,
433
ctrlKey: e.ctrlKey,
434
metaKey: e.metaKey,
435
altKey: e.altKey
436
});
437
};
438
439
const handleMove = (e: MouseEvent) => {
440
if (!isDragging) return;
441
442
const deltaX = e.movementX;
443
const deltaY = e.movementY;
444
445
setPosition(prev => ({
446
x: prev.x + deltaX,
447
y: prev.y + deltaY
448
}));
449
450
onMove?.({
451
type: "move",
452
pointerType: "mouse",
453
deltaX,
454
deltaY,
455
shiftKey: e.shiftKey,
456
ctrlKey: e.ctrlKey,
457
metaKey: e.metaKey,
458
altKey: e.altKey
459
});
460
};
461
462
const handleMoveEnd = (e: MouseEvent) => {
463
setIsDragging(false);
464
onMoveEnd?.({
465
type: "moveend",
466
pointerType: "mouse",
467
shiftKey: e.shiftKey,
468
ctrlKey: e.ctrlKey,
469
metaKey: e.metaKey,
470
altKey: e.altKey
471
});
472
};
473
474
useEffect(() => {
475
if (isDragging) {
476
document.addEventListener("mousemove", handleMove);
477
document.addEventListener("mouseup", handleMoveEnd);
478
return () => {
479
document.removeEventListener("mousemove", handleMove);
480
document.removeEventListener("mouseup", handleMoveEnd);
481
};
482
}
483
}, [isDragging]);
484
485
return (
486
<div
487
style={{
488
position: "absolute",
489
left: position.x,
490
top: position.y,
491
cursor: isDragging ? "grabbing" : "grab"
492
}}
493
onMouseDown={handleMoveStart}
494
>
495
{children}
496
</div>
497
);
498
}
499
500
// Keyboard navigation component
501
interface KeyboardNavigationProps extends KeyboardEvents, FocusableProps {
502
items: string[];
503
onSelect?: (item: string, index: number) => void;
504
}
505
506
function KeyboardNavigation({
507
items,
508
onSelect,
509
onKeyDown,
510
onFocus,
511
onBlur,
512
autoFocus
513
}: KeyboardNavigationProps) {
514
const [selectedIndex, setSelectedIndex] = useState(0);
515
516
const handleKeyDown = (e: KeyboardEvent) => {
517
switch (e.key) {
518
case "ArrowDown":
519
e.preventDefault();
520
setSelectedIndex(prev => Math.min(prev + 1, items.length - 1));
521
break;
522
case "ArrowUp":
523
e.preventDefault();
524
setSelectedIndex(prev => Math.max(prev - 1, 0));
525
break;
526
case "Enter":
527
e.preventDefault();
528
onSelect?.(items[selectedIndex], selectedIndex);
529
break;
530
case "Home":
531
e.preventDefault();
532
setSelectedIndex(0);
533
break;
534
case "End":
535
e.preventDefault();
536
setSelectedIndex(items.length - 1);
537
break;
538
}
539
onKeyDown?.(e);
540
};
541
542
return (
543
<div
544
tabIndex={0}
545
autoFocus={autoFocus}
546
onKeyDown={handleKeyDown}
547
onFocus={onFocus}
548
onBlur={onBlur}
549
role="listbox"
550
aria-activedescendant={`item-${selectedIndex}`}
551
>
552
{items.map((item, index) => (
553
<div
554
key={index}
555
id={`item-${index}`}
556
role="option"
557
aria-selected={index === selectedIndex}
558
style={{
559
backgroundColor: index === selectedIndex ? "#e0e0e0" : "transparent",
560
padding: "8px"
561
}}
562
onClick={() => {
563
setSelectedIndex(index);
564
onSelect?.(item, index);
565
}}
566
>
567
{item}
568
</div>
569
))}
570
</div>
571
);
572
}
573
```