mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): add hotkey system with coordinator, actions, and ARIA support (#1238)
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
/** Resolve the deepest event target, preferring composedPath for shadow DOM. */
|
||||
export function resolveEventTarget(event: Event): EventTarget | null {
|
||||
const path = event.composedPath();
|
||||
return path.length > 0 ? path[0]! : event.target;
|
||||
}
|
||||
|
||||
export interface OnEventOptions extends AddEventListenerOptions {
|
||||
/**
|
||||
* An AbortSignal to cancel waiting for the event.
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
export { animationFrame } from './animation-frame';
|
||||
export { namedNodeMapToObject } from './attributes';
|
||||
export { isRTL } from './direction';
|
||||
export { type OnEventOptions, onEvent } from './event';
|
||||
export { type OnEventOptions, onEvent, resolveEventTarget } from './event';
|
||||
export { idleCallback } from './idle-callback';
|
||||
export { listen } from './listen';
|
||||
export { isMacOS } from './platform';
|
||||
export { tryHidePopover, tryShowPopover } from './popover';
|
||||
export { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from './predicates';
|
||||
export {
|
||||
isEditableElement,
|
||||
isEditableTarget,
|
||||
isHTMLAudioElement,
|
||||
isHTMLMediaElement,
|
||||
isHTMLVideoElement,
|
||||
} from './predicates';
|
||||
export { type RafThrottled, rafThrottle } from './raf-throttle';
|
||||
export {
|
||||
applyShadowStyles,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isMacOS(): boolean {
|
||||
return typeof navigator !== 'undefined' && /mac/i.test(navigator.userAgent);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { resolveEventTarget } from './event';
|
||||
|
||||
export function isHTMLVideoElement(value: unknown): value is HTMLVideoElement {
|
||||
return value instanceof HTMLVideoElement;
|
||||
}
|
||||
@@ -9,3 +11,25 @@ export function isHTMLAudioElement(value: unknown): value is HTMLAudioElement {
|
||||
export function isHTMLMediaElement(value: unknown): value is HTMLMediaElement {
|
||||
return value instanceof HTMLMediaElement;
|
||||
}
|
||||
|
||||
const EDITABLE_INPUT_TYPES = new Set(['text', 'search', 'url', 'tel', 'email', 'password', 'number']);
|
||||
|
||||
export function isEditableElement(el: Element): boolean {
|
||||
if (el instanceof HTMLTextAreaElement) return true;
|
||||
if (el instanceof HTMLSelectElement) return true;
|
||||
|
||||
if (el instanceof HTMLInputElement) {
|
||||
return EDITABLE_INPUT_TYPES.has(el.type.toLowerCase());
|
||||
}
|
||||
|
||||
if (!(el instanceof HTMLElement)) return false;
|
||||
|
||||
const editable = el.getAttribute('contenteditable');
|
||||
return editable !== null && editable !== 'false';
|
||||
}
|
||||
|
||||
/** Whether the keyboard event target is an editable element (input, textarea, etc). */
|
||||
export function isEditableTarget(event: KeyboardEvent): boolean {
|
||||
const target = resolveEventTarget(event);
|
||||
return target instanceof Element && isEditableElement(target);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from '../predicates';
|
||||
import { isEditableTarget, isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from '../predicates';
|
||||
|
||||
function keydown(target: EventTarget, options?: KeyboardEventInit): KeyboardEvent {
|
||||
const event = new KeyboardEvent('keydown', { key: 'k', bubbles: true, ...options });
|
||||
target.dispatchEvent(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
describe('DOM predicates', () => {
|
||||
describe('isHTMLVideoElement', () => {
|
||||
@@ -75,3 +81,111 @@ describe('DOM predicates', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEditableTarget', () => {
|
||||
it('returns true for text input', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
document.body.appendChild(input);
|
||||
|
||||
const event = keydown(input);
|
||||
expect(isEditableTarget(event)).toBe(true);
|
||||
|
||||
input.remove();
|
||||
});
|
||||
|
||||
it('returns true for textarea', () => {
|
||||
const textarea = document.createElement('textarea');
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
const event = keydown(textarea);
|
||||
expect(isEditableTarget(event)).toBe(true);
|
||||
|
||||
textarea.remove();
|
||||
});
|
||||
|
||||
it('returns true for select', () => {
|
||||
const select = document.createElement('select');
|
||||
document.body.appendChild(select);
|
||||
|
||||
const event = keydown(select);
|
||||
expect(isEditableTarget(event)).toBe(true);
|
||||
|
||||
select.remove();
|
||||
});
|
||||
|
||||
it('returns true for contenteditable element', () => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('contenteditable', 'true');
|
||||
document.body.appendChild(div);
|
||||
|
||||
const event = keydown(div);
|
||||
expect(isEditableTarget(event)).toBe(true);
|
||||
|
||||
div.remove();
|
||||
});
|
||||
|
||||
it('returns false for button', () => {
|
||||
const button = document.createElement('button');
|
||||
document.body.appendChild(button);
|
||||
|
||||
const event = keydown(button);
|
||||
expect(isEditableTarget(event)).toBe(false);
|
||||
|
||||
button.remove();
|
||||
});
|
||||
|
||||
it('returns false for range input', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'range';
|
||||
document.body.appendChild(input);
|
||||
|
||||
const event = keydown(input);
|
||||
expect(isEditableTarget(event)).toBe(false);
|
||||
|
||||
input.remove();
|
||||
});
|
||||
|
||||
it('returns false for plain div', () => {
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
|
||||
const event = keydown(div);
|
||||
expect(isEditableTarget(event)).toBe(false);
|
||||
|
||||
div.remove();
|
||||
});
|
||||
|
||||
it('returns true for email input', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'email';
|
||||
document.body.appendChild(input);
|
||||
|
||||
const event = keydown(input);
|
||||
expect(isEditableTarget(event)).toBe(true);
|
||||
|
||||
input.remove();
|
||||
});
|
||||
|
||||
it('returns true for search input', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'search';
|
||||
document.body.appendChild(input);
|
||||
|
||||
const event = keydown(input);
|
||||
expect(isEditableTarget(event)).toBe(true);
|
||||
|
||||
input.remove();
|
||||
});
|
||||
|
||||
it('returns false for checkbox input', () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
document.body.appendChild(input);
|
||||
|
||||
const event = keydown(input);
|
||||
expect(isEditableTarget(event)).toBe(false);
|
||||
|
||||
input.remove();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user