0
# Virtual Events & Input
1
2
Detection and handling of virtual events from assistive technology, keyboard navigation, and platform-specific input methods.
3
4
## Capabilities
5
6
### Virtual Click Detection
7
8
Functions for detecting clicks that originate from assistive technology or keyboard activation.
9
10
```typescript { .api }
11
/**
12
* Detects clicks from keyboard or assistive technology
13
* @param event - MouseEvent or PointerEvent to check
14
* @returns true if click is from keyboard/AT, false for actual mouse clicks
15
*/
16
function isVirtualClick(event: MouseEvent | PointerEvent): boolean;
17
18
/**
19
* Detects pointer events from assistive technology
20
* @param event - PointerEvent to check
21
* @returns true if pointer event is from assistive technology
22
*/
23
function isVirtualPointerEvent(event: PointerEvent): boolean;
24
```
25
26
**Usage Examples:**
27
28
```typescript
29
import { isVirtualClick, isVirtualPointerEvent } from "@react-aria/utils";
30
31
function AccessibleButton({ onClick, children, ...props }) {
32
const handleClick = (e: MouseEvent) => {
33
const isVirtual = isVirtualClick(e);
34
35
console.log(isVirtual ? 'Keyboard/AT activation' : 'Mouse click');
36
37
// Different behavior for virtual vs real clicks
38
if (isVirtual) {
39
// Keyboard activation - provide more feedback
40
announceToScreenReader('Button activated');
41
}
42
43
onClick?.(e);
44
};
45
46
const handlePointerDown = (e: PointerEvent) => {
47
if (isVirtualPointerEvent(e)) {
48
// This is from assistive technology
49
console.log('AT pointer event');
50
e.preventDefault(); // Prevent default AT behavior if needed
51
}
52
};
53
54
return (
55
<button
56
onClick={handleClick}
57
onPointerDown={handlePointerDown}
58
{...props}
59
>
60
{children}
61
</button>
62
);
63
}
64
65
// Link component with virtual click handling
66
function SmartLink({ href, onClick, children, ...props }) {
67
const handleClick = (e: MouseEvent) => {
68
const isVirtual = isVirtualClick(e);
69
70
if (isVirtual) {
71
// Keyboard activation of link
72
// Don't show loading states that depend on hover
73
console.log('Link activated via keyboard');
74
} else {
75
// Actual mouse click
76
// Safe to show hover-dependent UI
77
console.log('Link clicked with mouse');
78
}
79
80
onClick?.(e);
81
};
82
83
return (
84
<a href={href} onClick={handleClick} {...props}>
85
{children}
86
</a>
87
);
88
}
89
90
// Dropdown menu with virtual click awareness
91
function DropdownMenu({ trigger, items, onSelect }) {
92
const [isOpen, setIsOpen] = useState(false);
93
94
const handleTriggerClick = (e: MouseEvent) => {
95
const isVirtual = isVirtualClick(e);
96
97
if (isVirtual) {
98
// Keyboard activation - always open menu
99
setIsOpen(true);
100
} else {
101
// Mouse click - toggle menu
102
setIsOpen(!isOpen);
103
}
104
};
105
106
const handleItemClick = (item: any, e: MouseEvent) => {
107
const isVirtual = isVirtualClick(e);
108
109
if (isVirtual) {
110
// Keyboard selection - provide confirmation
111
announceToScreenReader(`Selected ${item.name}`);
112
}
113
114
onSelect(item);
115
setIsOpen(false);
116
};
117
118
return (
119
<div className="dropdown">
120
<button onClick={handleTriggerClick}>
121
{trigger}
122
</button>
123
124
{isOpen && (
125
<ul className="dropdown-menu">
126
{items.map(item => (
127
<li key={item.id}>
128
<button onClick={(e) => handleItemClick(item, e)}>
129
{item.name}
130
</button>
131
</li>
132
))}
133
</ul>
134
)}
135
</div>
136
);
137
}
138
```
139
140
### Cross-Platform Key Detection
141
142
Function for detecting Ctrl/Cmd key presses in a cross-platform manner.
143
144
```typescript { .api }
145
/**
146
* Cross-platform Ctrl/Cmd key detection
147
* @param e - Event with modifier key properties
148
* @returns true if the platform's primary modifier key is pressed
149
*/
150
function isCtrlKeyPressed(e: KeyboardEvent | MouseEvent | PointerEvent): boolean;
151
```
152
153
**Usage Examples:**
154
155
```typescript
156
import { isCtrlKeyPressed } from "@react-aria/utils";
157
158
function TextEditor({ content, onChange }) {
159
const handleKeyDown = (e: KeyboardEvent) => {
160
const isCtrlCmd = isCtrlKeyPressed(e);
161
162
if (isCtrlCmd) {
163
switch (e.key.toLowerCase()) {
164
case 's':
165
e.preventDefault();
166
saveDocument();
167
break;
168
169
case 'z':
170
e.preventDefault();
171
if (e.shiftKey) {
172
redo();
173
} else {
174
undo();
175
}
176
break;
177
178
case 'c':
179
// Let default copy behavior work
180
console.log('Copy command');
181
break;
182
183
case 'v':
184
// Let default paste behavior work
185
console.log('Paste command');
186
break;
187
}
188
}
189
};
190
191
return (
192
<textarea
193
value={content}
194
onChange={(e) => onChange(e.target.value)}
195
onKeyDown={handleKeyDown}
196
placeholder="Type here... Use Ctrl/Cmd+S to save, Ctrl/Cmd+Z to undo"
197
/>
198
);
199
}
200
201
// Context menu with keyboard shortcuts
202
function ContextMenu({ x, y, onClose, onAction }) {
203
const menuItems = [
204
{ id: 'copy', label: 'Copy', shortcut: 'Ctrl+C', action: 'copy' },
205
{ id: 'paste', label: 'Paste', shortcut: 'Ctrl+V', action: 'paste' },
206
{ id: 'delete', label: 'Delete', shortcut: 'Del', action: 'delete' }
207
];
208
209
const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
210
const isCtrlCmd = isCtrlKeyPressed(e);
211
212
// Handle shortcuts when menu is open
213
if (isCtrlCmd) {
214
let actionToTrigger = null;
215
216
switch (e.key.toLowerCase()) {
217
case 'c':
218
actionToTrigger = 'copy';
219
break;
220
case 'v':
221
actionToTrigger = 'paste';
222
break;
223
}
224
225
if (actionToTrigger) {
226
e.preventDefault();
227
onAction(actionToTrigger);
228
onClose();
229
}
230
}
231
232
if (e.key === 'Escape') {
233
onClose();
234
}
235
}, [onAction, onClose]);
236
237
useEffect(() => {
238
document.addEventListener('keydown', handleGlobalKeyDown);
239
return () => document.removeEventListener('keydown', handleGlobalKeyDown);
240
}, [handleGlobalKeyDown]);
241
242
return (
243
<div
244
className="context-menu"
245
style={{ position: 'absolute', left: x, top: y }}
246
>
247
{menuItems.map(item => (
248
<button
249
key={item.id}
250
onClick={() => {
251
onAction(item.action);
252
onClose();
253
}}
254
>
255
{item.label}
256
<span className="shortcut">{item.shortcut}</span>
257
</button>
258
))}
259
</div>
260
);
261
}
262
263
// File manager with cross-platform shortcuts
264
function FileManager({ files, onFileAction }) {
265
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
266
267
const handleKeyDown = (e: KeyboardEvent) => {
268
const isCtrlCmd = isCtrlKeyPressed(e);
269
270
if (isCtrlCmd) {
271
switch (e.key.toLowerCase()) {
272
case 'a':
273
e.preventDefault();
274
setSelectedFiles(files.map(f => f.id));
275
break;
276
277
case 'c':
278
e.preventDefault();
279
copyFilesToClipboard(selectedFiles);
280
break;
281
282
case 'x':
283
e.preventDefault();
284
cutFilesToClipboard(selectedFiles);
285
break;
286
287
case 'v':
288
e.preventDefault();
289
pasteFilesFromClipboard();
290
break;
291
}
292
} else if (e.key === 'Delete') {
293
e.preventDefault();
294
onFileAction('delete', selectedFiles);
295
}
296
};
297
298
const handleClick = (e: MouseEvent, fileId: string) => {
299
const isCtrlCmd = isCtrlKeyPressed(e);
300
301
if (isCtrlCmd) {
302
// Multi-select with Ctrl/Cmd+click
303
setSelectedFiles(prev =>
304
prev.includes(fileId)
305
? prev.filter(id => id !== fileId)
306
: [...prev, fileId]
307
);
308
} else {
309
// Single select
310
setSelectedFiles([fileId]);
311
}
312
};
313
314
return (
315
<div className="file-manager" onKeyDown={handleKeyDown} tabIndex={0}>
316
{files.map(file => (
317
<div
318
key={file.id}
319
className={`file ${selectedFiles.includes(file.id) ? 'selected' : ''}`}
320
onClick={(e) => handleClick(e, file.id)}
321
>
322
{file.name}
323
</div>
324
))}
325
</div>
326
);
327
}
328
```
329
330
### Advanced Virtual Event Patterns
331
332
Complex scenarios combining virtual event detection with accessibility features:
333
334
```typescript
335
import { isVirtualClick, isVirtualPointerEvent, isCtrlKeyPressed } from "@react-aria/utils";
336
337
// Accessible drag and drop with virtual event support
338
function DragDropItem({ item, onDragStart, onDrop }) {
339
const [isDragging, setIsDragging] = useState(false);
340
const [keyboardDragMode, setKeyboardDragMode] = useState(false);
341
const elementRef = useRef<HTMLDivElement>(null);
342
343
const handleMouseDown = (e: MouseEvent) => {
344
if (!isVirtualClick(e)) {
345
// Real mouse interaction
346
setIsDragging(true);
347
onDragStart(item);
348
}
349
};
350
351
const handleKeyDown = (e: KeyboardEvent) => {
352
const isCtrlCmd = isCtrlKeyPressed(e);
353
354
if (e.key === ' ' && isCtrlCmd) {
355
// Ctrl/Cmd+Space starts keyboard drag mode
356
e.preventDefault();
357
setKeyboardDragMode(true);
358
announceToScreenReader('Drag mode activated. Use arrow keys to move, Space to drop.');
359
} else if (keyboardDragMode) {
360
switch (e.key) {
361
case 'ArrowUp':
362
case 'ArrowDown':
363
case 'ArrowLeft':
364
case 'ArrowRight':
365
e.preventDefault();
366
moveItemInDirection(e.key);
367
break;
368
369
case ' ':
370
e.preventDefault();
371
setKeyboardDragMode(false);
372
onDrop(item);
373
announceToScreenReader('Item dropped.');
374
break;
375
376
case 'Escape':
377
e.preventDefault();
378
setKeyboardDragMode(false);
379
announceToScreenReader('Drag cancelled.');
380
break;
381
}
382
}
383
};
384
385
const handleClick = (e: MouseEvent) => {
386
if (isVirtualClick(e)) {
387
// Keyboard activation
388
const isCtrlCmd = isCtrlKeyPressed(e);
389
390
if (isCtrlCmd) {
391
// Ctrl/Cmd+Enter activates keyboard drag
392
setKeyboardDragMode(true);
393
} else {
394
// Regular activation
395
onItemActivate(item);
396
}
397
}
398
};
399
400
return (
401
<div
402
ref={elementRef}
403
className={`drag-item ${isDragging ? 'dragging' : ''} ${keyboardDragMode ? 'keyboard-drag' : ''}`}
404
draggable
405
tabIndex={0}
406
onMouseDown={handleMouseDown}
407
onKeyDown={handleKeyDown}
408
onClick={handleClick}
409
role="button"
410
aria-describedby="drag-instructions"
411
>
412
{item.name}
413
<div id="drag-instructions" className="sr-only">
414
Press Ctrl+Space to start keyboard drag mode
415
</div>
416
</div>
417
);
418
}
419
420
// Touch-friendly button with virtual event awareness
421
function TouchFriendlyButton({ onPress, children, ...props }) {
422
const [isPressed, setIsPressed] = useState(false);
423
const [pressStartTime, setPressStartTime] = useState(0);
424
425
const handlePointerDown = (e: PointerEvent) => {
426
if (isVirtualPointerEvent(e)) {
427
// AT-generated pointer event
428
console.log('Assistive technology interaction');
429
return;
430
}
431
432
setIsPressed(true);
433
setPressStartTime(Date.now());
434
};
435
436
const handlePointerUp = (e: PointerEvent) => {
437
if (isVirtualPointerEvent(e)) {
438
return;
439
}
440
441
setIsPressed(false);
442
443
const pressDuration = Date.now() - pressStartTime;
444
445
// Different feedback for long vs short presses
446
if (pressDuration > 500) {
447
console.log('Long press detected');
448
onLongPress?.(e);
449
} else {
450
onPress?.(e);
451
}
452
};
453
454
const handleClick = (e: MouseEvent) => {
455
if (isVirtualClick(e)) {
456
// Virtual click from keyboard or AT
457
onPress?.(e);
458
}
459
// Mouse clicks are handled by pointer events
460
};
461
462
return (
463
<button
464
className={`touch-button ${isPressed ? 'pressed' : ''}`}
465
onPointerDown={handlePointerDown}
466
onPointerUp={handlePointerUp}
467
onClick={handleClick}
468
{...props}
469
>
470
{children}
471
</button>
472
);
473
}
474
475
// Game controller with keyboard and virtual input support
476
function GameController({ onAction }) {
477
const handleKeyDown = (e: KeyboardEvent) => {
478
const isCtrlCmd = isCtrlKeyPressed(e);
479
480
// Standard game controls
481
switch (e.key) {
482
case 'ArrowUp':
483
case 'w':
484
case 'W':
485
onAction('move-up');
486
break;
487
488
case 'ArrowDown':
489
case 's':
490
case 'S':
491
onAction('move-down');
492
break;
493
494
case 'ArrowLeft':
495
case 'a':
496
case 'A':
497
onAction('move-left');
498
break;
499
500
case 'ArrowRight':
501
case 'd':
502
case 'D':
503
onAction('move-right');
504
break;
505
506
case ' ':
507
onAction('action');
508
break;
509
510
case 'Enter':
511
onAction('confirm');
512
break;
513
}
514
515
// Special combinations with Ctrl/Cmd
516
if (isCtrlCmd) {
517
switch (e.key.toLowerCase()) {
518
case 'r':
519
e.preventDefault();
520
onAction('restart');
521
break;
522
523
case 'p':
524
e.preventDefault();
525
onAction('pause');
526
break;
527
}
528
}
529
};
530
531
const handleClick = (e: MouseEvent, action: string) => {
532
if (isVirtualClick(e)) {
533
// Keyboard/AT activation of button
534
announceToScreenReader(`${action} activated`);
535
}
536
537
onAction(action);
538
};
539
540
return (
541
<div className="game-controller" onKeyDown={handleKeyDown} tabIndex={0}>
542
<div className="dpad">
543
<button onClick={(e) => handleClick(e, 'move-up')}>↑</button>
544
<button onClick={(e) => handleClick(e, 'move-left')}>←</button>
545
<button onClick={(e) => handleClick(e, 'move-right')}>→</button>
546
<button onClick={(e) => handleClick(e, 'move-down')}>↓</button>
547
</div>
548
549
<div className="action-buttons">
550
<button onClick={(e) => handleClick(e, 'action')}>Action</button>
551
<button onClick={(e) => handleClick(e, 'confirm')}>Confirm</button>
552
</div>
553
</div>
554
);
555
}
556
```
557
558
## Browser Compatibility
559
560
Virtual event detection works consistently across all modern browsers:
561
562
- **Chrome/Edge**: Full support for pointer events and virtual click detection
563
- **Firefox**: Full support with proper AT integration
564
- **Safari**: Full support including VoiceOver integration
565
- **Mobile browsers**: Handles touch-to-click conversion properly
566
567
## Accessibility Considerations
568
569
When working with virtual events:
570
571
- **Always support keyboard activation**: Virtual clicks often come from Enter/Space key presses
572
- **Provide appropriate feedback**: Screen readers expect different feedback for virtual vs. real clicks
573
- **Don't prevent default behavior unnecessarily**: AT may depend on default behaviors
574
- **Test with real assistive technology**: Virtual event detection helps but isn't a substitute for AT testing
575
576
## Types
577
578
```typescript { .api }
579
interface MouseEvent extends UIEvent {
580
metaKey: boolean;
581
ctrlKey: boolean;
582
altKey: boolean;
583
shiftKey: boolean;
584
// ... other MouseEvent properties
585
}
586
587
interface PointerEvent extends MouseEvent {
588
pointerId: number;
589
pointerType: string;
590
// ... other PointerEvent properties
591
}
592
593
interface KeyboardEvent extends UIEvent {
594
key: string;
595
metaKey: boolean;
596
ctrlKey: boolean;
597
altKey: boolean;
598
shiftKey: boolean;
599
// ... other KeyboardEvent properties
600
}
601
```