Essential utility functions and React hooks for building accessible React Aria UI components
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Detection and handling of virtual events from assistive technology, keyboard navigation, and platform-specific input methods.
Functions for detecting clicks that originate from assistive technology or keyboard activation.
/**
* Detects clicks from keyboard or assistive technology
* @param event - MouseEvent or PointerEvent to check
* @returns true if click is from keyboard/AT, false for actual mouse clicks
*/
function isVirtualClick(event: MouseEvent | PointerEvent): boolean;
/**
* Detects pointer events from assistive technology
* @param event - PointerEvent to check
* @returns true if pointer event is from assistive technology
*/
function isVirtualPointerEvent(event: PointerEvent): boolean;Usage Examples:
import { isVirtualClick, isVirtualPointerEvent } from "@react-aria/utils";
function AccessibleButton({ onClick, children, ...props }) {
const handleClick = (e: MouseEvent) => {
const isVirtual = isVirtualClick(e);
console.log(isVirtual ? 'Keyboard/AT activation' : 'Mouse click');
// Different behavior for virtual vs real clicks
if (isVirtual) {
// Keyboard activation - provide more feedback
announceToScreenReader('Button activated');
}
onClick?.(e);
};
const handlePointerDown = (e: PointerEvent) => {
if (isVirtualPointerEvent(e)) {
// This is from assistive technology
console.log('AT pointer event');
e.preventDefault(); // Prevent default AT behavior if needed
}
};
return (
<button
onClick={handleClick}
onPointerDown={handlePointerDown}
{...props}
>
{children}
</button>
);
}
// Link component with virtual click handling
function SmartLink({ href, onClick, children, ...props }) {
const handleClick = (e: MouseEvent) => {
const isVirtual = isVirtualClick(e);
if (isVirtual) {
// Keyboard activation of link
// Don't show loading states that depend on hover
console.log('Link activated via keyboard');
} else {
// Actual mouse click
// Safe to show hover-dependent UI
console.log('Link clicked with mouse');
}
onClick?.(e);
};
return (
<a href={href} onClick={handleClick} {...props}>
{children}
</a>
);
}
// Dropdown menu with virtual click awareness
function DropdownMenu({ trigger, items, onSelect }) {
const [isOpen, setIsOpen] = useState(false);
const handleTriggerClick = (e: MouseEvent) => {
const isVirtual = isVirtualClick(e);
if (isVirtual) {
// Keyboard activation - always open menu
setIsOpen(true);
} else {
// Mouse click - toggle menu
setIsOpen(!isOpen);
}
};
const handleItemClick = (item: any, e: MouseEvent) => {
const isVirtual = isVirtualClick(e);
if (isVirtual) {
// Keyboard selection - provide confirmation
announceToScreenReader(`Selected ${item.name}`);
}
onSelect(item);
setIsOpen(false);
};
return (
<div className="dropdown">
<button onClick={handleTriggerClick}>
{trigger}
</button>
{isOpen && (
<ul className="dropdown-menu">
{items.map(item => (
<li key={item.id}>
<button onClick={(e) => handleItemClick(item, e)}>
{item.name}
</button>
</li>
))}
</ul>
)}
</div>
);
}Function for detecting Ctrl/Cmd key presses in a cross-platform manner.
/**
* Cross-platform Ctrl/Cmd key detection
* @param e - Event with modifier key properties
* @returns true if the platform's primary modifier key is pressed
*/
function isCtrlKeyPressed(e: KeyboardEvent | MouseEvent | PointerEvent): boolean;Usage Examples:
import { isCtrlKeyPressed } from "@react-aria/utils";
function TextEditor({ content, onChange }) {
const handleKeyDown = (e: KeyboardEvent) => {
const isCtrlCmd = isCtrlKeyPressed(e);
if (isCtrlCmd) {
switch (e.key.toLowerCase()) {
case 's':
e.preventDefault();
saveDocument();
break;
case 'z':
e.preventDefault();
if (e.shiftKey) {
redo();
} else {
undo();
}
break;
case 'c':
// Let default copy behavior work
console.log('Copy command');
break;
case 'v':
// Let default paste behavior work
console.log('Paste command');
break;
}
}
};
return (
<textarea
value={content}
onChange={(e) => onChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type here... Use Ctrl/Cmd+S to save, Ctrl/Cmd+Z to undo"
/>
);
}
// Context menu with keyboard shortcuts
function ContextMenu({ x, y, onClose, onAction }) {
const menuItems = [
{ id: 'copy', label: 'Copy', shortcut: 'Ctrl+C', action: 'copy' },
{ id: 'paste', label: 'Paste', shortcut: 'Ctrl+V', action: 'paste' },
{ id: 'delete', label: 'Delete', shortcut: 'Del', action: 'delete' }
];
const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
const isCtrlCmd = isCtrlKeyPressed(e);
// Handle shortcuts when menu is open
if (isCtrlCmd) {
let actionToTrigger = null;
switch (e.key.toLowerCase()) {
case 'c':
actionToTrigger = 'copy';
break;
case 'v':
actionToTrigger = 'paste';
break;
}
if (actionToTrigger) {
e.preventDefault();
onAction(actionToTrigger);
onClose();
}
}
if (e.key === 'Escape') {
onClose();
}
}, [onAction, onClose]);
useEffect(() => {
document.addEventListener('keydown', handleGlobalKeyDown);
return () => document.removeEventListener('keydown', handleGlobalKeyDown);
}, [handleGlobalKeyDown]);
return (
<div
className="context-menu"
style={{ position: 'absolute', left: x, top: y }}
>
{menuItems.map(item => (
<button
key={item.id}
onClick={() => {
onAction(item.action);
onClose();
}}
>
{item.label}
<span className="shortcut">{item.shortcut}</span>
</button>
))}
</div>
);
}
// File manager with cross-platform shortcuts
function FileManager({ files, onFileAction }) {
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const handleKeyDown = (e: KeyboardEvent) => {
const isCtrlCmd = isCtrlKeyPressed(e);
if (isCtrlCmd) {
switch (e.key.toLowerCase()) {
case 'a':
e.preventDefault();
setSelectedFiles(files.map(f => f.id));
break;
case 'c':
e.preventDefault();
copyFilesToClipboard(selectedFiles);
break;
case 'x':
e.preventDefault();
cutFilesToClipboard(selectedFiles);
break;
case 'v':
e.preventDefault();
pasteFilesFromClipboard();
break;
}
} else if (e.key === 'Delete') {
e.preventDefault();
onFileAction('delete', selectedFiles);
}
};
const handleClick = (e: MouseEvent, fileId: string) => {
const isCtrlCmd = isCtrlKeyPressed(e);
if (isCtrlCmd) {
// Multi-select with Ctrl/Cmd+click
setSelectedFiles(prev =>
prev.includes(fileId)
? prev.filter(id => id !== fileId)
: [...prev, fileId]
);
} else {
// Single select
setSelectedFiles([fileId]);
}
};
return (
<div className="file-manager" onKeyDown={handleKeyDown} tabIndex={0}>
{files.map(file => (
<div
key={file.id}
className={`file ${selectedFiles.includes(file.id) ? 'selected' : ''}`}
onClick={(e) => handleClick(e, file.id)}
>
{file.name}
</div>
))}
</div>
);
}Complex scenarios combining virtual event detection with accessibility features:
import { isVirtualClick, isVirtualPointerEvent, isCtrlKeyPressed } from "@react-aria/utils";
// Accessible drag and drop with virtual event support
function DragDropItem({ item, onDragStart, onDrop }) {
const [isDragging, setIsDragging] = useState(false);
const [keyboardDragMode, setKeyboardDragMode] = useState(false);
const elementRef = useRef<HTMLDivElement>(null);
const handleMouseDown = (e: MouseEvent) => {
if (!isVirtualClick(e)) {
// Real mouse interaction
setIsDragging(true);
onDragStart(item);
}
};
const handleKeyDown = (e: KeyboardEvent) => {
const isCtrlCmd = isCtrlKeyPressed(e);
if (e.key === ' ' && isCtrlCmd) {
// Ctrl/Cmd+Space starts keyboard drag mode
e.preventDefault();
setKeyboardDragMode(true);
announceToScreenReader('Drag mode activated. Use arrow keys to move, Space to drop.');
} else if (keyboardDragMode) {
switch (e.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
e.preventDefault();
moveItemInDirection(e.key);
break;
case ' ':
e.preventDefault();
setKeyboardDragMode(false);
onDrop(item);
announceToScreenReader('Item dropped.');
break;
case 'Escape':
e.preventDefault();
setKeyboardDragMode(false);
announceToScreenReader('Drag cancelled.');
break;
}
}
};
const handleClick = (e: MouseEvent) => {
if (isVirtualClick(e)) {
// Keyboard activation
const isCtrlCmd = isCtrlKeyPressed(e);
if (isCtrlCmd) {
// Ctrl/Cmd+Enter activates keyboard drag
setKeyboardDragMode(true);
} else {
// Regular activation
onItemActivate(item);
}
}
};
return (
<div
ref={elementRef}
className={`drag-item ${isDragging ? 'dragging' : ''} ${keyboardDragMode ? 'keyboard-drag' : ''}`}
draggable
tabIndex={0}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
onClick={handleClick}
role="button"
aria-describedby="drag-instructions"
>
{item.name}
<div id="drag-instructions" className="sr-only">
Press Ctrl+Space to start keyboard drag mode
</div>
</div>
);
}
// Touch-friendly button with virtual event awareness
function TouchFriendlyButton({ onPress, children, ...props }) {
const [isPressed, setIsPressed] = useState(false);
const [pressStartTime, setPressStartTime] = useState(0);
const handlePointerDown = (e: PointerEvent) => {
if (isVirtualPointerEvent(e)) {
// AT-generated pointer event
console.log('Assistive technology interaction');
return;
}
setIsPressed(true);
setPressStartTime(Date.now());
};
const handlePointerUp = (e: PointerEvent) => {
if (isVirtualPointerEvent(e)) {
return;
}
setIsPressed(false);
const pressDuration = Date.now() - pressStartTime;
// Different feedback for long vs short presses
if (pressDuration > 500) {
console.log('Long press detected');
onLongPress?.(e);
} else {
onPress?.(e);
}
};
const handleClick = (e: MouseEvent) => {
if (isVirtualClick(e)) {
// Virtual click from keyboard or AT
onPress?.(e);
}
// Mouse clicks are handled by pointer events
};
return (
<button
className={`touch-button ${isPressed ? 'pressed' : ''}`}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onClick={handleClick}
{...props}
>
{children}
</button>
);
}
// Game controller with keyboard and virtual input support
function GameController({ onAction }) {
const handleKeyDown = (e: KeyboardEvent) => {
const isCtrlCmd = isCtrlKeyPressed(e);
// Standard game controls
switch (e.key) {
case 'ArrowUp':
case 'w':
case 'W':
onAction('move-up');
break;
case 'ArrowDown':
case 's':
case 'S':
onAction('move-down');
break;
case 'ArrowLeft':
case 'a':
case 'A':
onAction('move-left');
break;
case 'ArrowRight':
case 'd':
case 'D':
onAction('move-right');
break;
case ' ':
onAction('action');
break;
case 'Enter':
onAction('confirm');
break;
}
// Special combinations with Ctrl/Cmd
if (isCtrlCmd) {
switch (e.key.toLowerCase()) {
case 'r':
e.preventDefault();
onAction('restart');
break;
case 'p':
e.preventDefault();
onAction('pause');
break;
}
}
};
const handleClick = (e: MouseEvent, action: string) => {
if (isVirtualClick(e)) {
// Keyboard/AT activation of button
announceToScreenReader(`${action} activated`);
}
onAction(action);
};
return (
<div className="game-controller" onKeyDown={handleKeyDown} tabIndex={0}>
<div className="dpad">
<button onClick={(e) => handleClick(e, 'move-up')}>↑</button>
<button onClick={(e) => handleClick(e, 'move-left')}>←</button>
<button onClick={(e) => handleClick(e, 'move-right')}>→</button>
<button onClick={(e) => handleClick(e, 'move-down')}>↓</button>
</div>
<div className="action-buttons">
<button onClick={(e) => handleClick(e, 'action')}>Action</button>
<button onClick={(e) => handleClick(e, 'confirm')}>Confirm</button>
</div>
</div>
);
}Virtual event detection works consistently across all modern browsers:
When working with virtual events:
interface MouseEvent extends UIEvent {
metaKey: boolean;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
// ... other MouseEvent properties
}
interface PointerEvent extends MouseEvent {
pointerId: number;
pointerType: string;
// ... other PointerEvent properties
}
interface KeyboardEvent extends UIEvent {
key: string;
metaKey: boolean;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
// ... other KeyboardEvent properties
}Install with Tessl CLI
npx tessl i tessl/npm-react-aria--utils