mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(packages): ignore gestures on interactive child elements (#1327)
This commit is contained in:
@@ -372,6 +372,86 @@ describe('regions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactive child filtering', () => {
|
||||
beforeEach(() => vi.useFakeTimers());
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it('does not fire when event originates from a child button', () => {
|
||||
const container = setup();
|
||||
const button = document.createElement('button');
|
||||
container.appendChild(button);
|
||||
|
||||
const handler = vi.fn();
|
||||
createTapGesture(container, handler);
|
||||
|
||||
pointerDown(button);
|
||||
vi.advanceTimersByTime(50);
|
||||
pointerUp(button, { pointerType: 'mouse', clientX: 150 });
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not fire when event originates from a child with role="slider"', () => {
|
||||
const container = setup();
|
||||
const slider = document.createElement('div');
|
||||
slider.setAttribute('role', 'slider');
|
||||
container.appendChild(slider);
|
||||
|
||||
const handler = vi.fn();
|
||||
createTapGesture(container, handler);
|
||||
|
||||
pointerDown(slider);
|
||||
vi.advanceTimersByTime(50);
|
||||
pointerUp(slider, { pointerType: 'mouse', clientX: 150 });
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not fire when event originates from a nested child inside an interactive element', () => {
|
||||
const container = setup();
|
||||
const button = document.createElement('button');
|
||||
const icon = document.createElement('span');
|
||||
button.appendChild(icon);
|
||||
container.appendChild(button);
|
||||
|
||||
const handler = vi.fn();
|
||||
createTapGesture(container, handler);
|
||||
|
||||
pointerDown(icon);
|
||||
vi.advanceTimersByTime(50);
|
||||
pointerUp(icon, { pointerType: 'mouse', clientX: 150 });
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fires when event originates from a non-interactive child', () => {
|
||||
const container = setup();
|
||||
const overlay = document.createElement('div');
|
||||
container.appendChild(overlay);
|
||||
|
||||
const handler = vi.fn();
|
||||
createTapGesture(container, handler);
|
||||
|
||||
pointerDown(overlay);
|
||||
vi.advanceTimersByTime(50);
|
||||
pointerUp(overlay, { pointerType: 'mouse', clientX: 150 });
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('fires when event targets the container directly', () => {
|
||||
const container = setup();
|
||||
const handler = vi.fn();
|
||||
createTapGesture(container, handler);
|
||||
|
||||
pointerDown(container);
|
||||
vi.advanceTimersByTime(50);
|
||||
pointerUp(container, { pointerType: 'mouse', clientX: 150 });
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import { isEditableTarget, listen, resolveEventTarget } from '@videojs/utils/dom';
|
||||
import { isEditableTarget, isInteractiveActivation, listen } from '@videojs/utils/dom';
|
||||
|
||||
import { toAriaKeyShortcut } from './aria';
|
||||
import type { HotkeyOptions, ParsedHotkeyBinding } from './hotkey';
|
||||
import { matchesHotkeyEvent, parseHotkeyPattern } from './hotkey';
|
||||
|
||||
const ACTIVATION_KEYS = new Set([' ', 'Enter']);
|
||||
|
||||
/** Whether the event is an activation key on an interactive element (button, slider). */
|
||||
function isInteractiveActivation(event: KeyboardEvent): boolean {
|
||||
if (!ACTIVATION_KEYS.has(event.key)) return false;
|
||||
|
||||
const target = resolveEventTarget(event);
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
if (target instanceof HTMLButtonElement) return true;
|
||||
|
||||
const role = target.getAttribute('role');
|
||||
return role === 'button' || role === 'slider';
|
||||
}
|
||||
|
||||
interface HotkeyBinding {
|
||||
parsed: ParsedHotkeyBinding[];
|
||||
options: HotkeyOptions;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface UIEvent {
|
||||
preventDefault(): void;
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
export interface UIKeyboardEvent extends UIEvent {
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('createPopover', () => {
|
||||
openOnHover: () => true,
|
||||
});
|
||||
|
||||
popover.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
|
||||
popover.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
|
||||
@@ -172,7 +172,7 @@ describe('createPopover', () => {
|
||||
openOnHover: () => true,
|
||||
});
|
||||
|
||||
popover.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
|
||||
popover.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
|
||||
@@ -189,7 +189,7 @@ describe('createPopover', () => {
|
||||
openOnHover: () => true,
|
||||
});
|
||||
|
||||
popover.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
|
||||
popover.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
||||
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true, { reason: 'focus' });
|
||||
|
||||
|
||||
@@ -151,6 +151,10 @@ export function createSlider(options: SliderOptions): SliderApi {
|
||||
onPointerDown(event) {
|
||||
if (options.isDisabled()) return;
|
||||
|
||||
// The slider fully owns pointer interactions — prevent parent gesture
|
||||
// coordinators from misinterpreting slider taps as surface gestures.
|
||||
event.stopPropagation();
|
||||
|
||||
// Prevent the browser's default mousedown focus behavior. Without this,
|
||||
// clicking a non-focusable child (e.g. the track) causes the browser to
|
||||
// move focus away from the thumb after our programmatic `focus()` call,
|
||||
@@ -213,6 +217,8 @@ export function createSlider(options: SliderOptions): SliderApi {
|
||||
onPointerUp(event) {
|
||||
if (isNull(capturedPointerId)) return;
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
const percent = getPercentFromPointerEvent(event, cachedRect!, options.getOrientation(), cachedRTL);
|
||||
|
||||
// Cancel any pending throttled change before the final unthrottled pair.
|
||||
|
||||
@@ -53,6 +53,7 @@ function pointerEvent(overrides: Partial<UIPointerEvent> = {}): UIPointerEvent {
|
||||
pointerType: 'mouse',
|
||||
buttons: 1,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -69,6 +70,7 @@ function keyboardEvent(key: string, overrides: Partial<UIKeyboardEvent> = {}): U
|
||||
target: node,
|
||||
currentTarget: node,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ function wheelEvent(deltaY: number): UIWheelEvent {
|
||||
return {
|
||||
deltaY,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ describe('createTooltip', () => {
|
||||
pointerType: 'mouse',
|
||||
buttons: 0,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
@@ -131,6 +132,7 @@ describe('createTooltip', () => {
|
||||
pointerType: 'touch',
|
||||
buttons: 0,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(600);
|
||||
@@ -148,6 +150,7 @@ describe('createTooltip', () => {
|
||||
pointerType: 'mouse',
|
||||
buttons: 0,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
vi.advanceTimersByTime(600);
|
||||
@@ -164,11 +167,12 @@ describe('createTooltip', () => {
|
||||
pointerType: 'touch' as const,
|
||||
buttons: 0,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
};
|
||||
|
||||
// Simulate tap: pointerdown → focusin (flag consumed and reset)
|
||||
tooltip.triggerProps.onPointerDown(pointerEvent);
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -177,7 +181,7 @@ describe('createTooltip', () => {
|
||||
const { tooltip, onOpenChange } = createTestTooltip();
|
||||
|
||||
// Simulate keyboard Tab: focusin without preceding pointerdown
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
||||
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true, { reason: 'focus' });
|
||||
});
|
||||
@@ -191,15 +195,16 @@ describe('createTooltip', () => {
|
||||
pointerType: 'touch' as const,
|
||||
buttons: 0,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
};
|
||||
|
||||
// Tap: pointerdown → focusin (suppressed, flag consumed)
|
||||
tooltip.triggerProps.onPointerDown(pointerEvent);
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
|
||||
// Later keyboard Tab: flag is clean, focus opens tooltip
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
||||
expect(onOpenChange).toHaveBeenCalledWith(true, { reason: 'focus' });
|
||||
});
|
||||
});
|
||||
@@ -209,7 +214,7 @@ describe('createTooltip', () => {
|
||||
disabled: () => true,
|
||||
});
|
||||
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
|
||||
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn(), stopPropagation: vi.fn() });
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -231,6 +236,7 @@ describe('createTooltip', () => {
|
||||
pointerType: 'mouse',
|
||||
buttons: 0,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,17 +3,18 @@ export { namedNodeMapToObject, serializeAttributes } from './attributes';
|
||||
export { isRTL } from './direction';
|
||||
export { type OnEventOptions, onEvent, resolveEventTarget } from './event';
|
||||
export { idleCallback } from './idle-callback';
|
||||
export {
|
||||
EDITABLE_SELECTOR,
|
||||
INTERACTIVE_SELECTOR,
|
||||
isEditableElement,
|
||||
isEditableTarget,
|
||||
isInteractiveActivation,
|
||||
isInteractiveTarget,
|
||||
} from './interactive';
|
||||
export { listen } from './listen';
|
||||
export { isMacOS } from './platform';
|
||||
export { tryHidePopover, tryShowPopover } from './popover';
|
||||
export {
|
||||
isEditableElement,
|
||||
isEditableTarget,
|
||||
isHTMLAudioElement,
|
||||
isHTMLMediaElement,
|
||||
isHTMLVideoElement,
|
||||
isInteractiveTarget,
|
||||
} from './predicates';
|
||||
export { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from './predicates';
|
||||
export { type RafThrottled, rafThrottle } from './raf-throttle';
|
||||
export {
|
||||
applyShadowStyles,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { resolveEventTarget } from './event';
|
||||
|
||||
export const INTERACTIVE_SELECTOR = 'button,input,select,textarea,a[href],[role="slider"],[role="button"]';
|
||||
|
||||
const EDITABLE_INPUT_TYPES = ['text', 'search', 'url', 'tel', 'email', 'password', 'number'];
|
||||
|
||||
export const EDITABLE_SELECTOR = [
|
||||
'textarea',
|
||||
'select',
|
||||
'input:not([type])',
|
||||
...EDITABLE_INPUT_TYPES.map((type) => `input[type="${type}"]`),
|
||||
'[contenteditable]:not([contenteditable="false"])',
|
||||
].join(',');
|
||||
|
||||
export function isEditableElement(el: Element): boolean {
|
||||
return el.matches(EDITABLE_SELECTOR);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
/** Whether the event originated from an interactive control (button, slider, etc). */
|
||||
export function isInteractiveTarget(event: Event): boolean {
|
||||
const target = resolveEventTarget(event);
|
||||
if (!(target instanceof Element)) return false;
|
||||
return target.closest(INTERACTIVE_SELECTOR) !== null;
|
||||
}
|
||||
|
||||
const ACTIVATION_KEYS = new Set([' ', 'Enter']);
|
||||
|
||||
/**
|
||||
* Selector for elements that use Space/Enter as a native activation key.
|
||||
* Narrower than `INTERACTIVE_SELECTOR` — excludes editable elements like
|
||||
* `input`, `textarea`, `select` where Space/Enter is text input, not activation.
|
||||
*/
|
||||
const ACTIVATABLE_SELECTOR = 'button,a[href],[role="slider"],[role="button"]';
|
||||
|
||||
/** Whether the event is an activation key on an activatable element (button, link, slider). */
|
||||
export function isInteractiveActivation(event: KeyboardEvent): boolean {
|
||||
if (!ACTIVATION_KEYS.has(event.key)) return false;
|
||||
|
||||
const target = resolveEventTarget(event);
|
||||
return target instanceof Element && target.matches(ACTIVATABLE_SELECTOR);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { resolveEventTarget } from './event';
|
||||
|
||||
export function isHTMLVideoElement(value: unknown): value is HTMLVideoElement {
|
||||
return value instanceof HTMLVideoElement;
|
||||
}
|
||||
@@ -11,34 +9,3 @@ 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);
|
||||
}
|
||||
|
||||
const INTERACTIVE_SELECTOR = 'button, input, select, textarea, [role="button"], [role="slider"]';
|
||||
|
||||
/** Whether the event originated from an interactive control (button, slider, etc). */
|
||||
export function isInteractiveTarget(event: Event): boolean {
|
||||
const target = resolveEventTarget(event);
|
||||
if (!(target instanceof Element)) return false;
|
||||
return target.closest(INTERACTIVE_SELECTOR) !== null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isEditableTarget, isInteractiveActivation } from '../interactive';
|
||||
|
||||
function keydown(target: EventTarget, options?: KeyboardEventInit): KeyboardEvent {
|
||||
const event = new KeyboardEvent('keydown', { key: 'k', bubbles: true, ...options });
|
||||
target.dispatchEvent(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInteractiveActivation', () => {
|
||||
it('returns true for Space on a button', () => {
|
||||
const button = document.createElement('button');
|
||||
document.body.appendChild(button);
|
||||
|
||||
const event = keydown(button, { key: ' ' });
|
||||
expect(isInteractiveActivation(event)).toBe(true);
|
||||
|
||||
button.remove();
|
||||
});
|
||||
|
||||
it('returns true for Enter on a button', () => {
|
||||
const button = document.createElement('button');
|
||||
document.body.appendChild(button);
|
||||
|
||||
const event = keydown(button, { key: 'Enter' });
|
||||
expect(isInteractiveActivation(event)).toBe(true);
|
||||
|
||||
button.remove();
|
||||
});
|
||||
|
||||
it('returns false for non-activation key on a button', () => {
|
||||
const button = document.createElement('button');
|
||||
document.body.appendChild(button);
|
||||
|
||||
const event = keydown(button, { key: 'a' });
|
||||
expect(isInteractiveActivation(event)).toBe(false);
|
||||
|
||||
button.remove();
|
||||
});
|
||||
|
||||
it('returns false for Space on a non-interactive element', () => {
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
|
||||
const event = keydown(div, { key: ' ' });
|
||||
expect(isInteractiveActivation(event)).toBe(false);
|
||||
|
||||
div.remove();
|
||||
});
|
||||
|
||||
it('returns true for Space on element with role="slider"', () => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('role', 'slider');
|
||||
document.body.appendChild(div);
|
||||
|
||||
const event = keydown(div, { key: ' ' });
|
||||
expect(isInteractiveActivation(event)).toBe(true);
|
||||
|
||||
div.remove();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
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;
|
||||
}
|
||||
import { isHTMLAudioElement, isHTMLMediaElement, isHTMLVideoElement } from '../predicates';
|
||||
|
||||
describe('DOM predicates', () => {
|
||||
describe('isHTMLVideoElement', () => {
|
||||
@@ -81,111 +75,3 @@ 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