From b768c29f98043a41c6272683e240bf1948238042 Mon Sep 17 00:00:00 2001 From: rahim Date: Tue, 14 Apr 2026 02:11:01 -0700 Subject: [PATCH] fix(packages): ignore gestures on interactive child elements (#1327) --- .../src/dom/gesture/tests/gesture.test.ts | 80 +++++++++ packages/core/src/dom/hotkey/coordinator.ts | 16 +- packages/core/src/dom/ui/event.ts | 1 + .../src/dom/ui/popover/tests/popover.test.ts | 6 +- packages/core/src/dom/ui/slider.ts | 6 + packages/core/src/dom/ui/tests/slider.test.ts | 2 + .../core/src/dom/ui/tests/wheel-step.test.ts | 1 + .../src/dom/ui/tooltip/tests/tooltip.test.ts | 16 +- packages/utils/src/dom/index.ts | 17 +- packages/utils/src/dom/interactive.ts | 47 +++++ packages/utils/src/dom/predicates.ts | 33 ---- .../utils/src/dom/tests/interactive.test.ts | 170 ++++++++++++++++++ .../utils/src/dom/tests/predicates.test.ts | 116 +----------- 13 files changed, 332 insertions(+), 179 deletions(-) create mode 100644 packages/utils/src/dom/interactive.ts create mode 100644 packages/utils/src/dom/tests/interactive.test.ts diff --git a/packages/core/src/dom/gesture/tests/gesture.test.ts b/packages/core/src/dom/gesture/tests/gesture.test.ts index 9aa3076d..2b108033 100644 --- a/packages/core/src/dom/gesture/tests/gesture.test.ts +++ b/packages/core/src/dom/gesture/tests/gesture.test.ts @@ -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 // --------------------------------------------------------------------------- diff --git a/packages/core/src/dom/hotkey/coordinator.ts b/packages/core/src/dom/hotkey/coordinator.ts index 943dc241..d3ca7988 100644 --- a/packages/core/src/dom/hotkey/coordinator.ts +++ b/packages/core/src/dom/hotkey/coordinator.ts @@ -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; diff --git a/packages/core/src/dom/ui/event.ts b/packages/core/src/dom/ui/event.ts index 33a6bc9e..0b8d9c1c 100644 --- a/packages/core/src/dom/ui/event.ts +++ b/packages/core/src/dom/ui/event.ts @@ -1,5 +1,6 @@ export interface UIEvent { preventDefault(): void; + stopPropagation(): void; } export interface UIKeyboardEvent extends UIEvent { diff --git a/packages/core/src/dom/ui/popover/tests/popover.test.ts b/packages/core/src/dom/ui/popover/tests/popover.test.ts index c42ae530..e11f4cf3 100644 --- a/packages/core/src/dom/ui/popover/tests/popover.test.ts +++ b/packages/core/src/dom/ui/popover/tests/popover.test.ts @@ -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' }); diff --git a/packages/core/src/dom/ui/slider.ts b/packages/core/src/dom/ui/slider.ts index 179d53dd..063fb7dd 100644 --- a/packages/core/src/dom/ui/slider.ts +++ b/packages/core/src/dom/ui/slider.ts @@ -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. diff --git a/packages/core/src/dom/ui/tests/slider.test.ts b/packages/core/src/dom/ui/tests/slider.test.ts index 39b214fc..62fdee00 100644 --- a/packages/core/src/dom/ui/tests/slider.test.ts +++ b/packages/core/src/dom/ui/tests/slider.test.ts @@ -53,6 +53,7 @@ function pointerEvent(overrides: Partial = {}): UIPointerEvent { pointerType: 'mouse', buttons: 1, preventDefault: vi.fn(), + stopPropagation: vi.fn(), ...overrides, }; } @@ -69,6 +70,7 @@ function keyboardEvent(key: string, overrides: Partial = {}): U target: node, currentTarget: node, preventDefault: vi.fn(), + stopPropagation: vi.fn(), ...overrides, }; } diff --git a/packages/core/src/dom/ui/tests/wheel-step.test.ts b/packages/core/src/dom/ui/tests/wheel-step.test.ts index 323ab1a3..fa9dcbac 100644 --- a/packages/core/src/dom/ui/tests/wheel-step.test.ts +++ b/packages/core/src/dom/ui/tests/wheel-step.test.ts @@ -7,6 +7,7 @@ function wheelEvent(deltaY: number): UIWheelEvent { return { deltaY, preventDefault: vi.fn(), + stopPropagation: vi.fn(), }; } diff --git a/packages/core/src/dom/ui/tooltip/tests/tooltip.test.ts b/packages/core/src/dom/ui/tooltip/tests/tooltip.test.ts index aa4b17f3..9cef6ff3 100644 --- a/packages/core/src/dom/ui/tooltip/tests/tooltip.test.ts +++ b/packages/core/src/dom/ui/tooltip/tests/tooltip.test.ts @@ -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(), }); }); }); diff --git a/packages/utils/src/dom/index.ts b/packages/utils/src/dom/index.ts index 46c9bf3d..f7589a3a 100644 --- a/packages/utils/src/dom/index.ts +++ b/packages/utils/src/dom/index.ts @@ -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, diff --git a/packages/utils/src/dom/interactive.ts b/packages/utils/src/dom/interactive.ts new file mode 100644 index 00000000..9f718387 --- /dev/null +++ b/packages/utils/src/dom/interactive.ts @@ -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); +} diff --git a/packages/utils/src/dom/predicates.ts b/packages/utils/src/dom/predicates.ts index 0f18301a..ddb003af 100644 --- a/packages/utils/src/dom/predicates.ts +++ b/packages/utils/src/dom/predicates.ts @@ -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; -} diff --git a/packages/utils/src/dom/tests/interactive.test.ts b/packages/utils/src/dom/tests/interactive.test.ts new file mode 100644 index 00000000..942480e3 --- /dev/null +++ b/packages/utils/src/dom/tests/interactive.test.ts @@ -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(); + }); +}); diff --git a/packages/utils/src/dom/tests/predicates.test.ts b/packages/utils/src/dom/tests/predicates.test.ts index a603d220..33f73375 100644 --- a/packages/utils/src/dom/tests/predicates.test.ts +++ b/packages/utils/src/dom/tests/predicates.test.ts @@ -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(); - }); -});