or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

animation-and-transitions.mdevent-management.mdfocus-and-accessibility.mdid-and-refs.mdindex.mdlinks-and-navigation.mdmiscellaneous-utilities.mdplatform-detection.mdprops-and-events.mdscrolling-and-layout.mdshadow-dom-support.mdstate-and-effects.mdvirtual-events-and-input.md

virtual-events-and-input.mddocs/

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

```