81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
import { describe, it, expect, afterEach } from 'vitest';
|
|
|
|
import { isEditableEventTarget } from '../keyboard';
|
|
|
|
// Regression tests for #654: global single-key shortcuts fired while the user
|
|
// was editing inside the QuotedHtml shadow-DOM island, because the shadow
|
|
// boundary retargets both document.activeElement and event.target to the
|
|
// plain-div host. isEditableEventTarget must rely on composedPath instead.
|
|
|
|
// Evaluate from a window-level listener DURING dispatch — composedPath() is
|
|
// only populated while the event is being dispatched, matching how the real
|
|
// shortcut handlers run.
|
|
function dispatchAndCheck(el: HTMLElement): { editable: boolean; target: EventTarget | null } {
|
|
let result: { editable: boolean; target: EventTarget | null } | null = null;
|
|
const listener = (e: Event) => {
|
|
result = { editable: isEditableEventTarget(e), target: e.target };
|
|
};
|
|
window.addEventListener('keydown', listener);
|
|
el.dispatchEvent(
|
|
new KeyboardEvent('keydown', { key: 'Backspace', bubbles: true, composed: true })
|
|
);
|
|
window.removeEventListener('keydown', listener);
|
|
if (!result) throw new Error('keydown never reached window');
|
|
return result;
|
|
}
|
|
|
|
describe('isEditableEventTarget', () => {
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
});
|
|
|
|
it('detects plain inputs and textareas', () => {
|
|
for (const tag of ['input', 'textarea', 'select'] as const) {
|
|
const el = document.createElement(tag);
|
|
document.body.appendChild(el);
|
|
expect(dispatchAndCheck(el).editable).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('detects a contentEditable element', () => {
|
|
const el = document.createElement('div');
|
|
el.setAttribute('contenteditable', 'true');
|
|
document.body.appendChild(el);
|
|
expect(dispatchAndCheck(el).editable).toBe(true);
|
|
});
|
|
|
|
it('returns false for a non-editable element', () => {
|
|
const el = document.createElement('div');
|
|
document.body.appendChild(el);
|
|
expect(dispatchAndCheck(el).editable).toBe(false);
|
|
});
|
|
|
|
it('sees through a shadow boundary to an inner contentEditable (QuotedHtml island)', () => {
|
|
// Mirror the structure quoted-html.ts builds: plain-div host, open shadow
|
|
// root, inner contentEditable div.
|
|
const host = document.createElement('div');
|
|
host.className = 'quoted-html-island';
|
|
document.body.appendChild(host);
|
|
const shadow = host.attachShadow({ mode: 'open' });
|
|
const inner = document.createElement('div');
|
|
inner.setAttribute('contenteditable', 'true');
|
|
shadow.appendChild(inner);
|
|
|
|
const { editable, target } = dispatchAndCheck(inner);
|
|
// Sanity: the shadow boundary retargets the event — the outside listener
|
|
// sees the host, which is exactly why a target/activeElement check fails.
|
|
expect(target).toBe(host);
|
|
expect(editable).toBe(true);
|
|
});
|
|
|
|
it('still returns false for a non-editable shadow tree', () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const shadow = host.attachShadow({ mode: 'open' });
|
|
const inner = document.createElement('div');
|
|
shadow.appendChild(inner);
|
|
|
|
expect(dispatchAndCheck(inner).editable).toBe(false);
|
|
});
|
|
});
|