From 0b858a079f058bfec5731c464e7346889ace4331 Mon Sep 17 00:00:00 2001 From: Christian24 Date: Sun, 7 Aug 2022 17:34:48 +0000 Subject: [PATCH 01/25] Potentially fix paste --- src/clipboard/paste.ts | 4 ++-- tests/webcomponents/index.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 tests/webcomponents/index.ts diff --git a/src/clipboard/paste.ts b/src/clipboard/paste.ts index 18c70cab..31e23ab5 100644 --- a/src/clipboard/paste.ts +++ b/src/clipboard/paste.ts @@ -1,6 +1,7 @@ import {type Instance} from '../setup' import { createDataTransfer, + getActiveElement, getWindow, readDataTransferFromClipboard, } from '../utils' @@ -10,8 +11,7 @@ export async function paste( clipboardData?: DataTransfer | string, ) { const doc = this.config.document - const target = doc.activeElement ?? /* istanbul ignore next */ doc.body - + const target = getActiveElement(doc) ?? doc.activeElement ?? doc.body const dataTransfer: DataTransfer = (typeof clipboardData === 'string' ? getClipboardDataFromString(doc, clipboardData) diff --git a/tests/webcomponents/index.ts b/tests/webcomponents/index.ts new file mode 100644 index 00000000..b81cab17 --- /dev/null +++ b/tests/webcomponents/index.ts @@ -0,0 +1,36 @@ +import {setup} from '#testHelpers' + +const template = document.createElement('template') +template.innerHTML = ` + +` + +class ShadowInput extends HTMLElement { + constructor() { + super() + + this.attachShadow({mode: 'open', delegatesFocus: true}) + if (this.shadowRoot) { + this.shadowRoot.appendChild(template.content.cloneNode(true)) + } + } + + public get value(): string { + return this.shadowRoot?.querySelector('input')?.value ?? '' + } +} + +test('Render open shadow DOM element', async () => { + window.customElements.define('shadow-input', ShadowInput) + const {element, user} = setup('', { + focus: false, + }) + + const inputElement = element.shadowRoot?.querySelector('input') + if (inputElement) { + await user.click(inputElement) + } + await user.keyboard('test') + await user.paste('test') + expect((element as ShadowInput).value).toEqual('testtest') +}) From 586f2c43fc2948b045be9c81a8f0b91c6ef0fe2e Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Mon, 8 Aug 2022 21:32:15 +0200 Subject: [PATCH 02/25] Fixed cut, paste and copy for open shadow dom --- src/clipboard/copy.ts | 4 +- src/clipboard/cut.ts | 4 +- tests/dom/customElement.ts | 94 +++++++++++++++++++++++++++++++++++- tests/webcomponents/index.ts | 36 -------------- 4 files changed, 97 insertions(+), 41 deletions(-) delete mode 100644 tests/webcomponents/index.ts diff --git a/src/clipboard/copy.ts b/src/clipboard/copy.ts index fac54bb9..7f745bf7 100644 --- a/src/clipboard/copy.ts +++ b/src/clipboard/copy.ts @@ -1,10 +1,10 @@ import {copySelection} from '../document' import {type Instance} from '../setup' -import {writeDataTransferToClipboard} from '../utils' +import {writeDataTransferToClipboard, getActiveElement,} from '../utils' export async function copy(this: Instance) { const doc = this.config.document - const target = doc.activeElement ?? /* istanbul ignore next */ doc.body + const target = getActiveElement(doc) ?? doc.activeElement ?? doc.body const clipboardData = copySelection(target) diff --git a/src/clipboard/cut.ts b/src/clipboard/cut.ts index 7e75e2ee..c6ed844f 100644 --- a/src/clipboard/cut.ts +++ b/src/clipboard/cut.ts @@ -1,10 +1,10 @@ import {copySelection} from '../document' import {type Instance} from '../setup' -import {writeDataTransferToClipboard} from '../utils' +import {writeDataTransferToClipboard, getActiveElement} from '../utils' export async function cut(this: Instance) { const doc = this.config.document - const target = doc.activeElement ?? /* istanbul ignore next */ doc.body + const target = getActiveElement(doc) ?? doc.activeElement ?? doc.body const clipboardData = copySelection(target) diff --git a/tests/dom/customElement.ts b/tests/dom/customElement.ts index beafce69..af743fb8 100644 --- a/tests/dom/customElement.ts +++ b/tests/dom/customElement.ts @@ -1,5 +1,6 @@ import userEvent from '#src' -import {addListeners} from '#testHelpers' +import {addListeners, setup} from '#testHelpers' +import {setUISelection} from '#src/document' // It is unclear which part of our implementation is targeted with this test. // Can this be removed? Is it sufficient? @@ -85,3 +86,94 @@ test('types text inside custom element', async () => { input[value="Sup"] - keyup: p `) }) + +const template = document.createElement('template') +template.innerHTML = ` + +` + +class ShadowInput extends HTMLElement { + constructor() { + super() + + this.attachShadow({mode: 'open', delegatesFocus: true}) + if (this.shadowRoot) { + this.shadowRoot.appendChild(template.content.cloneNode(true)) + } + } + + public get value(): string { + return this.shadowRoot?.querySelector('input')?.value ?? '' + } +} +window.customElements.define('shadow-input', ShadowInput) + +test('Render open shadow DOM element - type', async () => { + const {element, user} = setup('', { + focus: false, + }) + + const inputElement = element.shadowRoot?.querySelector('input') + if (inputElement) { + await user.click(inputElement) + } + await user.keyboard('test') + + expect((element as ShadowInput).value).toEqual('test') +}) + +test('Render open shadow DOM element - copy', async () => { + const {element, user} = setup('', { + focus: false, + }) + + const inputElement = element.shadowRoot?.querySelector('input') + if (inputElement) { + await user.click(inputElement) + } + await user.keyboard('test') + + if (inputElement) { + setUISelection(inputElement, {anchorOffset: 0, focusOffset: 4}) + } + + const data = await user.copy() + + expect(data?.getData('text')).toEqual('test') +}) + +test('Render open shadow DOM element - paste', async () => { + const {element, user} = setup('', { + focus: false, + }) + + const inputElement = element.shadowRoot?.querySelector('input') + if (inputElement) { + await user.click(inputElement) + } + + await user.paste('test') + + expect((element as ShadowInput).value).toEqual('test') +}) + +test('Render open shadow DOM element - cut', async () => { + const {element, user} = setup('', { + focus: false, + }) + + const inputElement = element.shadowRoot?.querySelector('input') + if (inputElement) { + await user.click(inputElement) + } + await user.keyboard('test') + + if (inputElement) { + setUISelection(inputElement, {anchorOffset: 0, focusOffset: 4}) + } + + const data = await user.cut() + + expect(data?.getData('text')).toEqual('test') + expect((element as ShadowInput).value.length).toEqual(0) +}) diff --git a/tests/webcomponents/index.ts b/tests/webcomponents/index.ts deleted file mode 100644 index b81cab17..00000000 --- a/tests/webcomponents/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {setup} from '#testHelpers' - -const template = document.createElement('template') -template.innerHTML = ` - -` - -class ShadowInput extends HTMLElement { - constructor() { - super() - - this.attachShadow({mode: 'open', delegatesFocus: true}) - if (this.shadowRoot) { - this.shadowRoot.appendChild(template.content.cloneNode(true)) - } - } - - public get value(): string { - return this.shadowRoot?.querySelector('input')?.value ?? '' - } -} - -test('Render open shadow DOM element', async () => { - window.customElements.define('shadow-input', ShadowInput) - const {element, user} = setup('', { - focus: false, - }) - - const inputElement = element.shadowRoot?.querySelector('input') - if (inputElement) { - await user.click(inputElement) - } - await user.keyboard('test') - await user.paste('test') - expect((element as ShadowInput).value).toEqual('testtest') -}) From 42efa628d965971bd85e8950f9fff3b8aab9a8d3 Mon Sep 17 00:00:00 2001 From: Christian24 Date: Sun, 14 Aug 2022 13:09:18 +0200 Subject: [PATCH 03/25] Update tests/dom/customElement.ts Co-authored-by: Philipp Fritsche --- tests/dom/customElement.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/dom/customElement.ts b/tests/dom/customElement.ts index af743fb8..9bfd2455 100644 --- a/tests/dom/customElement.ts +++ b/tests/dom/customElement.ts @@ -122,7 +122,8 @@ test('Render open shadow DOM element - type', async () => { expect((element as ShadowInput).value).toEqual('test') }) -test('Render open shadow DOM element - copy', async () => { +describe('on shadow DOM', () => { + test('copy', async () => { const {element, user} = setup('', { focus: false, }) From 0446f16192a99277e5438581d54c92c01e000957 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Sun, 14 Aug 2022 15:50:21 +0200 Subject: [PATCH 04/25] Moved Shadow DOM tests. Added focus and selection support for shadow DOM in setup(). Removed old custom elements test --- src/clipboard/copy.ts | 6 +- src/clipboard/cut.ts | 6 +- src/clipboard/paste.ts | 6 +- tests/_helpers/setup.ts | 14 +-- tests/_helpers/shadow-input.ts | 52 ++++++++++ tests/clipboard/copy.ts | 13 +++ tests/clipboard/cut.ts | 17 ++++ tests/clipboard/paste.ts | 12 +++ tests/dom/customElement.ts | 180 --------------------------------- tests/utility/type.ts | 17 ++++ 10 files changed, 128 insertions(+), 195 deletions(-) create mode 100644 tests/_helpers/shadow-input.ts delete mode 100644 tests/dom/customElement.ts diff --git a/src/clipboard/copy.ts b/src/clipboard/copy.ts index 7f745bf7..cef96c78 100644 --- a/src/clipboard/copy.ts +++ b/src/clipboard/copy.ts @@ -1,10 +1,10 @@ import {copySelection} from '../document' import {type Instance} from '../setup' -import {writeDataTransferToClipboard, getActiveElement,} from '../utils' +import {writeDataTransferToClipboard, getActiveElementOrBody,} from '../utils' export async function copy(this: Instance) { - const doc = this.config.document - const target = getActiveElement(doc) ?? doc.activeElement ?? doc.body + const doc: Document = this.config.document + const target = getActiveElementOrBody(doc) const clipboardData = copySelection(target) diff --git a/src/clipboard/cut.ts b/src/clipboard/cut.ts index c6ed844f..3016911c 100644 --- a/src/clipboard/cut.ts +++ b/src/clipboard/cut.ts @@ -1,10 +1,10 @@ import {copySelection} from '../document' import {type Instance} from '../setup' -import {writeDataTransferToClipboard, getActiveElement} from '../utils' +import {writeDataTransferToClipboard, getActiveElementOrBody} from '../utils' export async function cut(this: Instance) { - const doc = this.config.document - const target = getActiveElement(doc) ?? doc.activeElement ?? doc.body + const doc: Document = this.config.document + const target = getActiveElementOrBody(doc) const clipboardData = copySelection(target) diff --git a/src/clipboard/paste.ts b/src/clipboard/paste.ts index 31e23ab5..03baf7bf 100644 --- a/src/clipboard/paste.ts +++ b/src/clipboard/paste.ts @@ -1,7 +1,7 @@ import {type Instance} from '../setup' import { createDataTransfer, - getActiveElement, + getActiveElementOrBody, getWindow, readDataTransferFromClipboard, } from '../utils' @@ -10,8 +10,8 @@ export async function paste( this: Instance, clipboardData?: DataTransfer | string, ) { - const doc = this.config.document - const target = getActiveElement(doc) ?? doc.activeElement ?? doc.body + const doc: Document = this.config.document + const target = getActiveElementOrBody(doc) const dataTransfer: DataTransfer = (typeof clipboardData === 'string' ? getClipboardDataFromString(doc, clipboardData) diff --git a/tests/_helpers/setup.ts b/tests/_helpers/setup.ts index e9ae6d9c..46aef0b4 100644 --- a/tests/_helpers/setup.ts +++ b/tests/_helpers/setup.ts @@ -1,7 +1,7 @@ import {addListeners, EventHandlers} from './listeners' import userEvent from '#src' import {Options} from '#src/options' -import {FOCUSABLE_SELECTOR} from '#src/utils' +import {FOCUSABLE_SELECTOR, getActiveElementOrBody} from '#src/utils' import {setSelection} from '#src/event/selection' export function render( @@ -28,14 +28,19 @@ export function render( if (typeof focus === 'string') { ;(assertSingleNodeFromXPath(focus, div) as HTMLElement).focus() } else if (focus !== false) { - ;(div.querySelector(FOCUSABLE_SELECTOR) as HTMLElement | undefined)?.focus() + const element: HTMLElement | null = div.querySelector(FOCUSABLE_SELECTOR) + if (element) { + element.focus() + } else if (div.firstChild) { + ;(div.firstChild as HTMLElement).focus() + } } if (selection) { const focusNode = typeof selection.focusNode === 'string' ? assertSingleNodeFromXPath(selection.focusNode, div) - : document.activeElement + : getActiveElementOrBody(document) const anchorNode = typeof selection.anchorNode === 'string' ? assertSingleNodeFromXPath(selection.anchorNode, div) @@ -43,9 +48,6 @@ export function render( const focusOffset = selection.focusOffset ?? 0 const anchorOffset = selection.anchorOffset ?? focusOffset - if (!focusNode || !anchorNode) { - throw new Error(`missing/invalid selection.focusNode`) - } setSelection({ focusNode, anchorNode, diff --git a/tests/_helpers/shadow-input.ts b/tests/_helpers/shadow-input.ts new file mode 100644 index 00000000..332ef19e --- /dev/null +++ b/tests/_helpers/shadow-input.ts @@ -0,0 +1,52 @@ +const observed = ['value'] + +const template = document.createElement('template') +template.innerHTML = ` + +` + +export class ShadowInput extends HTMLElement { + private $input?: HTMLInputElement + + static getObservedAttributes() { + return observed + } + constructor() { + super() + + this.attachShadow({mode: 'open', delegatesFocus: true}) + if (this.shadowRoot) { + this.shadowRoot.appendChild(template.content.cloneNode(true)) + this.$input = this.shadowRoot.querySelector('input') as HTMLInputElement + } + observed.forEach(name => { + this.render(name, this.getAttribute(name)) + }) + } + attributeChangedCallback(name: string, oldVal: string, newVal: string) { + if (oldVal === newVal) return + this.render(name, newVal) + } + + render(name: string, value: string | null) { + if (value == null) { + this.$input?.removeAttribute(name) + } else { + this.$input?.setAttribute(name, value) + } + } + + public get value(): string { + return this.$input?.value ?? '' + } + + /** + * Overwrite focus handling because delegatesFocus is not implemented in jsdom + * @param options + */ + public override focus(options?: FocusOptions) { + super.focus(options) + this.$input?.focus(options) + } +} +window.customElements.define('shadow-input', ShadowInput) diff --git a/tests/clipboard/copy.ts b/tests/clipboard/copy.ts index 1b0e3b11..ad450605 100644 --- a/tests/clipboard/copy.ts +++ b/tests/clipboard/copy.ts @@ -1,5 +1,6 @@ import userEvent from '#src' import {render, setup} from '#testHelpers' +import '../_helpers/shadow-input' test('copy selected value', async () => { const {getEvents, user} = setup( @@ -98,3 +99,15 @@ describe('without Clipboard API', () => { expect(dt?.getData('text/plain')).toBe('bar') }) }) + +describe('on shadow DOM', () => { + test('copy in an input element', async () => { + const {user} = setup('', { + selection: {anchorOffset: 0, focusOffset: 4}, + }) + + const data = await user.copy() + + expect(data?.getData('text')).toEqual('test') + }) +}) diff --git a/tests/clipboard/cut.ts b/tests/clipboard/cut.ts index b8c2fc45..ce239a02 100644 --- a/tests/clipboard/cut.ts +++ b/tests/clipboard/cut.ts @@ -1,5 +1,7 @@ +import type {ShadowInput} from '../_helpers/shadow-input' import userEvent from '#src' import {render, setup} from '#testHelpers' +import '../_helpers/shadow-input' test('cut selected value', async () => { const {getEvents, user} = setup( @@ -106,3 +108,18 @@ describe('without Clipboard API', () => { expect(dt?.getData('text/plain')).toBe('bar') }) }) +describe('on shadow DOM', () => { + test('cut in an input element', async () => { + const {element, user} = setup( + '', + { + selection: {anchorOffset: 0, focusOffset: 4}, + }, + ) + + const data = await user.cut() + + expect(data?.getData('text')).toEqual('test') + expect((element as ShadowInput).value.length).toEqual(0) + }) +}) diff --git a/tests/clipboard/paste.ts b/tests/clipboard/paste.ts index cd5eb967..f38e6abc 100644 --- a/tests/clipboard/paste.ts +++ b/tests/clipboard/paste.ts @@ -1,6 +1,8 @@ +import type {ShadowInput} from '../_helpers/shadow-input' import userEvent from '#src' import {render, setup} from '#testHelpers' import {createDataTransfer} from '#src/utils' +import '../_helpers/shadow-input' test('paste with empty clipboard', async () => { const {element, getEvents, user} = setup(``) @@ -149,3 +151,13 @@ describe('without Clipboard API', () => { expect(getEvents()).toHaveLength(0) }) }) + +describe('on shadow DOM', () => { + test('paste into an input element', async () => { + const {element, user} = setup('') + + await user.paste('test') + + expect((element as ShadowInput).value).toEqual('test') + }) +}) diff --git a/tests/dom/customElement.ts b/tests/dom/customElement.ts deleted file mode 100644 index 9bfd2455..00000000 --- a/tests/dom/customElement.ts +++ /dev/null @@ -1,180 +0,0 @@ -import userEvent from '#src' -import {addListeners, setup} from '#testHelpers' -import {setUISelection} from '#src/document' - -// It is unclear which part of our implementation is targeted with this test. -// Can this be removed? Is it sufficient? - -const observed = ['value'] -const HTMLElement = window.HTMLElement - -class CustomEl extends HTMLElement { - private $input: HTMLInputElement - - static getObservedAttributes() { - return observed - } - - constructor() { - super() - const shadowRoot = this.attachShadow({mode: 'open'}) - shadowRoot.innerHTML = `` - this.$input = shadowRoot.querySelector('input') as HTMLInputElement - } - - connectedCallback() { - observed.forEach(name => { - this.render(name, this.getAttribute(name)) - }) - } - - attributeChangedCallback(name: string, oldVal: string, newVal: string) { - if (oldVal === newVal) return - this.render(name, newVal) - } - - render(name: string, value: string | null) { - if (value == null) { - this.$input.removeAttribute(name) - } else { - this.$input.setAttribute(name, value) - } - } -} - -customElements.define('custom-el', CustomEl) - -test('types text inside custom element', async () => { - const element = document.createElement('custom-el') - document.body.append(element) - const inputEl = (element.shadowRoot as ShadowRoot).querySelector( - 'input', - ) as HTMLInputElement - const {getEventSnapshot} = addListeners(inputEl) - - await userEvent.type(inputEl, 'Sup') - expect(getEventSnapshot()).toMatchInlineSnapshot(` - Events fired on: input[value="Sup"] - - input[value=""] - pointerover - input[value=""] - pointerenter - input[value=""] - mouseover - input[value=""] - mouseenter - input[value=""] - pointermove - input[value=""] - mousemove - input[value=""] - pointerdown - input[value=""] - mousedown: primary - input[value=""] - focus - input[value=""] - focusin - input[value=""] - pointerup - input[value=""] - mouseup: primary - input[value=""] - click: primary - input[value=""] - keydown: S - input[value=""] - keypress: S - input[value=""] - beforeinput - input[value="S"] - input - input[value="S"] - keyup: S - input[value="S"] - keydown: u - input[value="S"] - keypress: u - input[value="S"] - beforeinput - input[value="Su"] - input - input[value="Su"] - keyup: u - input[value="Su"] - keydown: p - input[value="Su"] - keypress: p - input[value="Su"] - beforeinput - input[value="Sup"] - input - input[value="Sup"] - keyup: p - `) -}) - -const template = document.createElement('template') -template.innerHTML = ` - -` - -class ShadowInput extends HTMLElement { - constructor() { - super() - - this.attachShadow({mode: 'open', delegatesFocus: true}) - if (this.shadowRoot) { - this.shadowRoot.appendChild(template.content.cloneNode(true)) - } - } - - public get value(): string { - return this.shadowRoot?.querySelector('input')?.value ?? '' - } -} -window.customElements.define('shadow-input', ShadowInput) - -test('Render open shadow DOM element - type', async () => { - const {element, user} = setup('', { - focus: false, - }) - - const inputElement = element.shadowRoot?.querySelector('input') - if (inputElement) { - await user.click(inputElement) - } - await user.keyboard('test') - - expect((element as ShadowInput).value).toEqual('test') -}) - -describe('on shadow DOM', () => { - test('copy', async () => { - const {element, user} = setup('', { - focus: false, - }) - - const inputElement = element.shadowRoot?.querySelector('input') - if (inputElement) { - await user.click(inputElement) - } - await user.keyboard('test') - - if (inputElement) { - setUISelection(inputElement, {anchorOffset: 0, focusOffset: 4}) - } - - const data = await user.copy() - - expect(data?.getData('text')).toEqual('test') -}) - -test('Render open shadow DOM element - paste', async () => { - const {element, user} = setup('', { - focus: false, - }) - - const inputElement = element.shadowRoot?.querySelector('input') - if (inputElement) { - await user.click(inputElement) - } - - await user.paste('test') - - expect((element as ShadowInput).value).toEqual('test') -}) - -test('Render open shadow DOM element - cut', async () => { - const {element, user} = setup('', { - focus: false, - }) - - const inputElement = element.shadowRoot?.querySelector('input') - if (inputElement) { - await user.click(inputElement) - } - await user.keyboard('test') - - if (inputElement) { - setUISelection(inputElement, {anchorOffset: 0, focusOffset: 4}) - } - - const data = await user.cut() - - expect(data?.getData('text')).toEqual('test') - expect((element as ShadowInput).value.length).toEqual(0) -}) diff --git a/tests/utility/type.ts b/tests/utility/type.ts index 26e73131..39854d8a 100644 --- a/tests/utility/type.ts +++ b/tests/utility/type.ts @@ -1,4 +1,6 @@ +import type {ShadowInput} from '../_helpers/shadow-input' import {setup} from '#testHelpers' +import '../_helpers/shadow-input' test('type into input', async () => { const {element, getEventSnapshot, user} = setup('', { @@ -90,3 +92,18 @@ test('do nothing on disabled element', async () => { expect(getEvents()).toHaveLength(0) }) + +describe('on shadow DOM', () => { + test('type into an input element', async () => { + const {element, user} = setup('') + + const inputElement = element.shadowRoot?.querySelector('input') + if (inputElement) { + await user.click(inputElement) + } + // Skip click because delegatesFocus is not implemented in jsdom + await user.type(element, 'test', {skipClick: true}) + + expect((element as ShadowInput).value).toEqual('test') + }) +}) From 70279f0472d06fd3ed3ac49967860fefe8ad00ce Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Sun, 14 Aug 2022 16:24:59 +0200 Subject: [PATCH 05/25] Added some tests for getActiveElement --- tests/utility/type.ts | 4 ---- tests/utils/focus/getActiveElement.ts | 29 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 tests/utils/focus/getActiveElement.ts diff --git a/tests/utility/type.ts b/tests/utility/type.ts index 39854d8a..849b3f77 100644 --- a/tests/utility/type.ts +++ b/tests/utility/type.ts @@ -97,10 +97,6 @@ describe('on shadow DOM', () => { test('type into an input element', async () => { const {element, user} = setup('') - const inputElement = element.shadowRoot?.querySelector('input') - if (inputElement) { - await user.click(inputElement) - } // Skip click because delegatesFocus is not implemented in jsdom await user.type(element, 'test', {skipClick: true}) diff --git a/tests/utils/focus/getActiveElement.ts b/tests/utils/focus/getActiveElement.ts new file mode 100644 index 00000000..f13f9bdc --- /dev/null +++ b/tests/utils/focus/getActiveElement.ts @@ -0,0 +1,29 @@ +import '../../_helpers/shadow-input' +import {setup} from '#testHelpers' +import {getActiveElement, getActiveElementOrBody} from '#src/utils' + +test('focus input element', async () => { + const {element} = setup('') + + expect(getActiveElement(document)).toBe(element) +}) + +test('focus should be body', async () => { + setup('', {focus: false}) + + expect(getActiveElementOrBody(document)).toBe(document.body) +}) +describe('on shadow DOM', () => { + test('focus contained input element', async () => { + const {element} = setup('') + + expect(getActiveElement(document)).toBe( + element.shadowRoot?.querySelector('input'), + ) + }) + test('focus just body', async () => { + setup('', {focus: false}) + + expect(getActiveElementOrBody(document)).toBe(document.body) + }) +}) From 86f94d2c5114ee33931d6d0df022fa502ff950bc Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Sun, 14 Aug 2022 16:27:00 +0200 Subject: [PATCH 06/25] Added some tests for getActiveElement --- tests/utils/focus/getActiveElement.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/utils/focus/getActiveElement.ts b/tests/utils/focus/getActiveElement.ts index f13f9bdc..6a1b7b48 100644 --- a/tests/utils/focus/getActiveElement.ts +++ b/tests/utils/focus/getActiveElement.ts @@ -2,17 +2,20 @@ import '../../_helpers/shadow-input' import {setup} from '#testHelpers' import {getActiveElement, getActiveElementOrBody} from '#src/utils' -test('focus input element', async () => { - const {element} = setup('') +describe('focus tests with not shadow root', () => { + test('focus input element', async () => { + const {element} = setup('') - expect(getActiveElement(document)).toBe(element) -}) + expect(getActiveElement(document)).toBe(element) + }) -test('focus should be body', async () => { - setup('', {focus: false}) + test('focus should be body', async () => { + setup('', {focus: false}) - expect(getActiveElementOrBody(document)).toBe(document.body) + expect(getActiveElementOrBody(document)).toBe(document.body) + }) }) + describe('on shadow DOM', () => { test('focus contained input element', async () => { const {element} = setup('') From aedd83864e817ac03b431791ee438513c1d45e07 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Mon, 15 Aug 2022 18:55:56 +0200 Subject: [PATCH 07/25] Fix type import --- src/clipboard/copy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clipboard/copy.ts b/src/clipboard/copy.ts index cef96c78..63be0649 100644 --- a/src/clipboard/copy.ts +++ b/src/clipboard/copy.ts @@ -1,5 +1,5 @@ import {copySelection} from '../document' -import {type Instance} from '../setup' +import type {type Instance} from '../setup' import {writeDataTransferToClipboard, getActiveElementOrBody,} from '../utils' export async function copy(this: Instance) { From 340f73bf20923675ef39166c892a83d427d8b9ad Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Mon, 15 Aug 2022 19:13:14 +0200 Subject: [PATCH 08/25] Fix type import --- tests/_helpers/shadow-input.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/_helpers/shadow-input.ts b/tests/_helpers/shadow-input.ts index 332ef19e..2f2a0b49 100644 --- a/tests/_helpers/shadow-input.ts +++ b/tests/_helpers/shadow-input.ts @@ -14,7 +14,7 @@ export class ShadowInput extends HTMLElement { constructor() { super() - this.attachShadow({mode: 'open', delegatesFocus: true}) + this.attachShadow({mode: 'open', delegatesFocus: false}) if (this.shadowRoot) { this.shadowRoot.appendChild(template.content.cloneNode(true)) this.$input = this.shadowRoot.querySelector('input') as HTMLInputElement @@ -41,7 +41,7 @@ export class ShadowInput extends HTMLElement { } /** - * Overwrite focus handling because delegatesFocus is not implemented in jsdom + * Overwrite focus handling, so that when the component is focused, the focus is delegated to the containing input. * @param options */ public override focus(options?: FocusOptions) { From 0853cd60b9ac78d95699d57c8e40eb3ca9d52baa Mon Sep 17 00:00:00 2001 From: Christian24 Date: Mon, 15 Aug 2022 19:12:30 +0200 Subject: [PATCH 09/25] Update tests/utils/focus/getActiveElement.ts Co-authored-by: Philipp Fritsche --- tests/utils/focus/getActiveElement.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/focus/getActiveElement.ts b/tests/utils/focus/getActiveElement.ts index 6a1b7b48..079959ed 100644 --- a/tests/utils/focus/getActiveElement.ts +++ b/tests/utils/focus/getActiveElement.ts @@ -6,7 +6,7 @@ describe('focus tests with not shadow root', () => { test('focus input element', async () => { const {element} = setup('') - expect(getActiveElement(document)).toBe(element) + expect(getActiveElementOrBody(document)).toBe(element) }) test('focus should be body', async () => { From 83508b5b85c1728f913d626a927effabd661c40a Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Mon, 15 Aug 2022 19:30:20 +0200 Subject: [PATCH 10/25] Add helper to define element. Remove describe block --- tests/_helpers/shadow-input.ts | 11 ++++++++-- tests/clipboard/copy.ts | 3 ++- tests/clipboard/cut.ts | 7 +++--- tests/clipboard/paste.ts | 7 +++--- tests/utility/type.ts | 9 ++++---- tests/utils/focus/getActiveElement.ts | 31 ++++++++++++++++----------- 6 files changed, 42 insertions(+), 26 deletions(-) diff --git a/tests/_helpers/shadow-input.ts b/tests/_helpers/shadow-input.ts index 2f2a0b49..30cdc1d2 100644 --- a/tests/_helpers/shadow-input.ts +++ b/tests/_helpers/shadow-input.ts @@ -5,7 +5,7 @@ template.innerHTML = ` ` -export class ShadowInput extends HTMLElement { +class ShadowInput extends HTMLElement { private $input?: HTMLInputElement static getObservedAttributes() { @@ -49,4 +49,11 @@ export class ShadowInput extends HTMLElement { this.$input?.focus(options) } } -window.customElements.define('shadow-input', ShadowInput) + +export type {ShadowInput} + +export function defineShadowInputCustomElementIfNotDefined() { + if (window.customElements.get('shadow-input') === undefined) { + window.customElements.define('shadow-input', ShadowInput) + } +} diff --git a/tests/clipboard/copy.ts b/tests/clipboard/copy.ts index ad450605..8ae5703b 100644 --- a/tests/clipboard/copy.ts +++ b/tests/clipboard/copy.ts @@ -1,6 +1,6 @@ +import {defineShadowInputCustomElementIfNotDefined} from '../_helpers/shadow-input' import userEvent from '#src' import {render, setup} from '#testHelpers' -import '../_helpers/shadow-input' test('copy selected value', async () => { const {getEvents, user} = setup( @@ -102,6 +102,7 @@ describe('without Clipboard API', () => { describe('on shadow DOM', () => { test('copy in an input element', async () => { + defineShadowInputCustomElementIfNotDefined() const {user} = setup('', { selection: {anchorOffset: 0, focusOffset: 4}, }) diff --git a/tests/clipboard/cut.ts b/tests/clipboard/cut.ts index ce239a02..2c526381 100644 --- a/tests/clipboard/cut.ts +++ b/tests/clipboard/cut.ts @@ -1,7 +1,7 @@ import type {ShadowInput} from '../_helpers/shadow-input' +import {defineShadowInputCustomElementIfNotDefined} from '../_helpers/shadow-input' import userEvent from '#src' import {render, setup} from '#testHelpers' -import '../_helpers/shadow-input' test('cut selected value', async () => { const {getEvents, user} = setup( @@ -110,7 +110,8 @@ describe('without Clipboard API', () => { }) describe('on shadow DOM', () => { test('cut in an input element', async () => { - const {element, user} = setup( + defineShadowInputCustomElementIfNotDefined() + const {element, user} = setup( '', { selection: {anchorOffset: 0, focusOffset: 4}, @@ -120,6 +121,6 @@ describe('on shadow DOM', () => { const data = await user.cut() expect(data?.getData('text')).toEqual('test') - expect((element as ShadowInput).value.length).toEqual(0) + expect(element.value.length).toEqual(0) }) }) diff --git a/tests/clipboard/paste.ts b/tests/clipboard/paste.ts index f38e6abc..7cc03e51 100644 --- a/tests/clipboard/paste.ts +++ b/tests/clipboard/paste.ts @@ -1,8 +1,8 @@ import type {ShadowInput} from '../_helpers/shadow-input' +import {defineShadowInputCustomElementIfNotDefined} from '../_helpers/shadow-input' import userEvent from '#src' import {render, setup} from '#testHelpers' import {createDataTransfer} from '#src/utils' -import '../_helpers/shadow-input' test('paste with empty clipboard', async () => { const {element, getEvents, user} = setup(``) @@ -154,10 +154,11 @@ describe('without Clipboard API', () => { describe('on shadow DOM', () => { test('paste into an input element', async () => { - const {element, user} = setup('') + defineShadowInputCustomElementIfNotDefined() + const {element, user} = setup('') await user.paste('test') - expect((element as ShadowInput).value).toEqual('test') + expect(element.value).toEqual('test') }) }) diff --git a/tests/utility/type.ts b/tests/utility/type.ts index 849b3f77..b8abe23b 100644 --- a/tests/utility/type.ts +++ b/tests/utility/type.ts @@ -1,6 +1,6 @@ import type {ShadowInput} from '../_helpers/shadow-input' +import {defineShadowInputCustomElementIfNotDefined} from '../_helpers/shadow-input' import {setup} from '#testHelpers' -import '../_helpers/shadow-input' test('type into input', async () => { const {element, getEventSnapshot, user} = setup('', { @@ -95,11 +95,12 @@ test('do nothing on disabled element', async () => { describe('on shadow DOM', () => { test('type into an input element', async () => { - const {element, user} = setup('') + defineShadowInputCustomElementIfNotDefined() + const {element, user} = setup('') - // Skip click because delegatesFocus is not implemented in jsdom + // Skip click because the element is already focused await user.type(element, 'test', {skipClick: true}) - expect((element as ShadowInput).value).toEqual('test') + expect(element.value).toEqual('test') }) }) diff --git a/tests/utils/focus/getActiveElement.ts b/tests/utils/focus/getActiveElement.ts index 079959ed..92e0435d 100644 --- a/tests/utils/focus/getActiveElement.ts +++ b/tests/utils/focus/getActiveElement.ts @@ -1,23 +1,25 @@ -import '../../_helpers/shadow-input' +import { + defineShadowInputCustomElementIfNotDefined, + ShadowInput, +} from '../../_helpers/shadow-input' import {setup} from '#testHelpers' import {getActiveElement, getActiveElementOrBody} from '#src/utils' -describe('focus tests with not shadow root', () => { - test('focus input element', async () => { - const {element} = setup('') +test('focused input element', async () => { + const {element} = setup('') - expect(getActiveElementOrBody(document)).toBe(element) - }) - - test('focus should be body', async () => { - setup('', {focus: false}) + expect(getActiveElementOrBody(document)).toBe(element) +}) - expect(getActiveElementOrBody(document)).toBe(document.body) - }) +test('focus should be body', async () => { + const {element} = setup('', {focus: false}) + element.blur() + expect(getActiveElementOrBody(document)).toBe(document.body) }) describe('on shadow DOM', () => { test('focus contained input element', async () => { + defineShadowInputCustomElementIfNotDefined() const {element} = setup('') expect(getActiveElement(document)).toBe( @@ -25,8 +27,11 @@ describe('on shadow DOM', () => { ) }) test('focus just body', async () => { - setup('', {focus: false}) - + defineShadowInputCustomElementIfNotDefined() + const {element} = setup('', { + focus: false, + }) + element.blur() expect(getActiveElementOrBody(document)).toBe(document.body) }) }) From f170d22dee29234df0442d755aec007767357148 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Mon, 15 Aug 2022 19:41:05 +0200 Subject: [PATCH 11/25] Use getActiveElementOrBody --- src/event/focus.ts | 6 +++--- tests/utils/focus/getActiveElement.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/event/focus.ts b/src/event/focus.ts index d21b4e40..e650978d 100644 --- a/src/event/focus.ts +++ b/src/event/focus.ts @@ -1,4 +1,4 @@ -import {findClosest, getActiveElement, isFocusable} from '../utils' +import {findClosest, getActiveElementOrBody, isFocusable} from '../utils' import {updateSelectionOnFocus} from './selection' import {wrapEvent} from './wrapEvent' @@ -8,7 +8,7 @@ import {wrapEvent} from './wrapEvent' export function focusElement(element: Element) { const target = findClosest(element, isFocusable) - const activeElement = getActiveElement(element.ownerDocument) + const activeElement = getActiveElementOrBody(element.ownerDocument) if ((target ?? element.ownerDocument.body) === activeElement) { return } else if (target) { @@ -23,7 +23,7 @@ export function focusElement(element: Element) { export function blurElement(element: Element) { if (!isFocusable(element)) return - const wasActive = getActiveElement(element.ownerDocument) === element + const wasActive = getActiveElementOrBody(element.ownerDocument) === element if (!wasActive) return wrapEvent(() => element.blur(), element) diff --git a/tests/utils/focus/getActiveElement.ts b/tests/utils/focus/getActiveElement.ts index 92e0435d..4037cae3 100644 --- a/tests/utils/focus/getActiveElement.ts +++ b/tests/utils/focus/getActiveElement.ts @@ -3,7 +3,7 @@ import { ShadowInput, } from '../../_helpers/shadow-input' import {setup} from '#testHelpers' -import {getActiveElement, getActiveElementOrBody} from '#src/utils' +import {getActiveElementOrBody} from '#src/utils' test('focused input element', async () => { const {element} = setup('') @@ -22,7 +22,7 @@ describe('on shadow DOM', () => { defineShadowInputCustomElementIfNotDefined() const {element} = setup('') - expect(getActiveElement(document)).toBe( + expect(getActiveElementOrBody(document)).toBe( element.shadowRoot?.querySelector('input'), ) }) From bed1eb1129b086cad8275d7cd09c5cf78de58a05 Mon Sep 17 00:00:00 2001 From: Christian24 Date: Mon, 15 Aug 2022 19:45:50 +0200 Subject: [PATCH 12/25] Update tests/utils/focus/getActiveElement.ts Co-authored-by: Philipp Fritsche --- tests/utils/focus/getActiveElement.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/focus/getActiveElement.ts b/tests/utils/focus/getActiveElement.ts index 4037cae3..50b09e4a 100644 --- a/tests/utils/focus/getActiveElement.ts +++ b/tests/utils/focus/getActiveElement.ts @@ -18,7 +18,7 @@ test('focus should be body', async () => { }) describe('on shadow DOM', () => { - test('focus contained input element', async () => { + test('get focused element inside shadow tree', async () => { defineShadowInputCustomElementIfNotDefined() const {element} = setup('') From 28e89954555f6a40cc6665784f5a79680f11e300 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Mon, 15 Aug 2022 19:53:22 +0200 Subject: [PATCH 13/25] Make focusing custom elements safer --- tests/_helpers/setup.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/_helpers/setup.ts b/tests/_helpers/setup.ts index 46aef0b4..9f249c69 100644 --- a/tests/_helpers/setup.ts +++ b/tests/_helpers/setup.ts @@ -31,8 +31,9 @@ export function render( const element: HTMLElement | null = div.querySelector(FOCUSABLE_SELECTOR) if (element) { element.focus() - } else if (div.firstChild) { - ;(div.firstChild as HTMLElement).focus() + } else if (div.firstElementChild?.tagName.includes('-')) { + // If the element's tag contains - it is a custom element, so it is focusable. + ;(div.firstElementChild as HTMLElement).focus() } } From 418093a19e4e5bfe2e0728b2c2ffdad8061b24f4 Mon Sep 17 00:00:00 2001 From: Christian24 Date: Tue, 16 Aug 2022 19:02:29 +0200 Subject: [PATCH 14/25] Update tests/utils/focus/getActiveElement.ts Co-authored-by: Philipp Fritsche --- tests/utils/focus/getActiveElement.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/focus/getActiveElement.ts b/tests/utils/focus/getActiveElement.ts index 50b09e4a..2a1b792d 100644 --- a/tests/utils/focus/getActiveElement.ts +++ b/tests/utils/focus/getActiveElement.ts @@ -11,7 +11,7 @@ test('focused input element', async () => { expect(getActiveElementOrBody(document)).toBe(element) }) -test('focus should be body', async () => { +test('default to body as active element', async () => { const {element} = setup('', {focus: false}) element.blur() expect(getActiveElementOrBody(document)).toBe(document.body) From 494c1bfe1455d60ea582b8429f38189c9979fff4 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Tue, 16 Aug 2022 19:37:28 +0200 Subject: [PATCH 15/25] Remove test --- tests/utils/focus/getActiveElement.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/utils/focus/getActiveElement.ts b/tests/utils/focus/getActiveElement.ts index 2a1b792d..c303f476 100644 --- a/tests/utils/focus/getActiveElement.ts +++ b/tests/utils/focus/getActiveElement.ts @@ -1,7 +1,4 @@ -import { - defineShadowInputCustomElementIfNotDefined, - ShadowInput, -} from '../../_helpers/shadow-input' +import {defineShadowInputCustomElementIfNotDefined} from '../../_helpers/shadow-input' import {setup} from '#testHelpers' import {getActiveElementOrBody} from '#src/utils' @@ -26,12 +23,4 @@ describe('on shadow DOM', () => { element.shadowRoot?.querySelector('input'), ) }) - test('focus just body', async () => { - defineShadowInputCustomElementIfNotDefined() - const {element} = setup('', { - focus: false, - }) - element.blur() - expect(getActiveElementOrBody(document)).toBe(document.body) - }) }) From a5f79d48d2db0d62fcb70010d3120323d6383669 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Tue, 16 Aug 2022 19:40:46 +0200 Subject: [PATCH 16/25] Removed types --- src/clipboard/copy.ts | 2 +- src/clipboard/cut.ts | 2 +- src/clipboard/paste.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clipboard/copy.ts b/src/clipboard/copy.ts index 63be0649..b38bc3a2 100644 --- a/src/clipboard/copy.ts +++ b/src/clipboard/copy.ts @@ -3,7 +3,7 @@ import type {type Instance} from '../setup' import {writeDataTransferToClipboard, getActiveElementOrBody,} from '../utils' export async function copy(this: Instance) { - const doc: Document = this.config.document + const doc = this.config.document const target = getActiveElementOrBody(doc) const clipboardData = copySelection(target) diff --git a/src/clipboard/cut.ts b/src/clipboard/cut.ts index 3016911c..746e7e26 100644 --- a/src/clipboard/cut.ts +++ b/src/clipboard/cut.ts @@ -3,7 +3,7 @@ import {type Instance} from '../setup' import {writeDataTransferToClipboard, getActiveElementOrBody} from '../utils' export async function cut(this: Instance) { - const doc: Document = this.config.document + const doc = this.config.document const target = getActiveElementOrBody(doc) const clipboardData = copySelection(target) diff --git a/src/clipboard/paste.ts b/src/clipboard/paste.ts index 03baf7bf..a0aedd20 100644 --- a/src/clipboard/paste.ts +++ b/src/clipboard/paste.ts @@ -10,7 +10,7 @@ export async function paste( this: Instance, clipboardData?: DataTransfer | string, ) { - const doc: Document = this.config.document + const doc = this.config.document const target = getActiveElementOrBody(doc) const dataTransfer: DataTransfer = (typeof clipboardData === 'string' From 6526a25bb47c901fb93e4e8630ead66f7072c714 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Tue, 16 Aug 2022 21:12:52 +0200 Subject: [PATCH 17/25] Add focus helper --- tests/_helpers/setup.ts | 38 +++++++++++++++++++++++++++------- tests/_helpers/shadow-input.ts | 11 +--------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/tests/_helpers/setup.ts b/tests/_helpers/setup.ts index 9f249c69..c29f83f2 100644 --- a/tests/_helpers/setup.ts +++ b/tests/_helpers/setup.ts @@ -28,13 +28,8 @@ export function render( if (typeof focus === 'string') { ;(assertSingleNodeFromXPath(focus, div) as HTMLElement).focus() } else if (focus !== false) { - const element: HTMLElement | null = div.querySelector(FOCUSABLE_SELECTOR) - if (element) { - element.focus() - } else if (div.firstElementChild?.tagName.includes('-')) { - // If the element's tag contains - it is a custom element, so it is focusable. - ;(div.firstElementChild as HTMLElement).focus() - } + const element: HTMLElement | null = getFocusableElement(div) + element?.focus() } if (selection) { @@ -108,3 +103,32 @@ export function setup( ...render(ui, {eventHandlers, focus, selection}), } } +export function getFocusableElement( + parent: Element | ShadowRoot, +): HTMLElement | null { + const possibleFocusableElement: HTMLElement | null = + parent.querySelector(FOCUSABLE_SELECTOR) + if (possibleFocusableElement) { + return possibleFocusableElement + } + + const children = Array.from(parent.children) + for (const child of children) { + if ('shadowRoot' in child && child.shadowRoot) { + //JSDOM currently does not support delegatesFocus, find the focusable element ourselves, if delegatesFocus is undefined + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (child.shadowRoot.delegatesFocus === undefined) { + const possibleFocusableChildElement = getFocusableElement( + child.shadowRoot, + ) + if (possibleFocusableChildElement) { + return possibleFocusableChildElement + } + } else { + return child as HTMLElement + } + } + } + return null +} diff --git a/tests/_helpers/shadow-input.ts b/tests/_helpers/shadow-input.ts index 30cdc1d2..404103ba 100644 --- a/tests/_helpers/shadow-input.ts +++ b/tests/_helpers/shadow-input.ts @@ -14,7 +14,7 @@ class ShadowInput extends HTMLElement { constructor() { super() - this.attachShadow({mode: 'open', delegatesFocus: false}) + this.attachShadow({mode: 'open', delegatesFocus: true}) if (this.shadowRoot) { this.shadowRoot.appendChild(template.content.cloneNode(true)) this.$input = this.shadowRoot.querySelector('input') as HTMLInputElement @@ -39,15 +39,6 @@ class ShadowInput extends HTMLElement { public get value(): string { return this.$input?.value ?? '' } - - /** - * Overwrite focus handling, so that when the component is focused, the focus is delegated to the containing input. - * @param options - */ - public override focus(options?: FocusOptions) { - super.focus(options) - this.$input?.focus(options) - } } export type {ShadowInput} From 5d44e0be79f0f13168d2668de0b3fc297b594d06 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Tue, 16 Aug 2022 21:15:12 +0200 Subject: [PATCH 18/25] Delete test --- tests/utility/type.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/utility/type.ts b/tests/utility/type.ts index b8abe23b..26e73131 100644 --- a/tests/utility/type.ts +++ b/tests/utility/type.ts @@ -1,5 +1,3 @@ -import type {ShadowInput} from '../_helpers/shadow-input' -import {defineShadowInputCustomElementIfNotDefined} from '../_helpers/shadow-input' import {setup} from '#testHelpers' test('type into input', async () => { @@ -92,15 +90,3 @@ test('do nothing on disabled element', async () => { expect(getEvents()).toHaveLength(0) }) - -describe('on shadow DOM', () => { - test('type into an input element', async () => { - defineShadowInputCustomElementIfNotDefined() - const {element, user} = setup('') - - // Skip click because the element is already focused - await user.type(element, 'test', {skipClick: true}) - - expect(element.value).toEqual('test') - }) -}) From db15efe8ee4d377d12a41d88720fb1bee89eca77 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Tue, 16 Aug 2022 21:41:24 +0200 Subject: [PATCH 19/25] Improve focus handling for setup --- tests/_helpers/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/_helpers/setup.ts b/tests/_helpers/setup.ts index c29f83f2..2dbd7331 100644 --- a/tests/_helpers/setup.ts +++ b/tests/_helpers/setup.ts @@ -125,7 +125,7 @@ export function getFocusableElement( if (possibleFocusableChildElement) { return possibleFocusableChildElement } - } else { + } else if (child.shadowRoot.delegatesFocus) { return child as HTMLElement } } From eb50b903b1d1ac15a05171cf958e0d71fcd8b904 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Tue, 16 Aug 2022 22:21:28 +0200 Subject: [PATCH 20/25] Improve focus handling for setup Add test for keyboard input on open shadow dom --- tests/_helpers/setup.ts | 17 +++++------------ tests/keyboard/index.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/tests/_helpers/setup.ts b/tests/_helpers/setup.ts index 2dbd7331..4e177992 100644 --- a/tests/_helpers/setup.ts +++ b/tests/_helpers/setup.ts @@ -115,18 +115,11 @@ export function getFocusableElement( const children = Array.from(parent.children) for (const child of children) { if ('shadowRoot' in child && child.shadowRoot) { - //JSDOM currently does not support delegatesFocus, find the focusable element ourselves, if delegatesFocus is undefined - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (child.shadowRoot.delegatesFocus === undefined) { - const possibleFocusableChildElement = getFocusableElement( - child.shadowRoot, - ) - if (possibleFocusableChildElement) { - return possibleFocusableChildElement - } - } else if (child.shadowRoot.delegatesFocus) { - return child as HTMLElement + const possibleFocusableChildElement = getFocusableElement( + child.shadowRoot, + ) + if (possibleFocusableChildElement) { + return possibleFocusableChildElement } } } diff --git a/tests/keyboard/index.ts b/tests/keyboard/index.ts index 32ec1133..0592f5b3 100644 --- a/tests/keyboard/index.ts +++ b/tests/keyboard/index.ts @@ -1,5 +1,9 @@ import userEvent from '#src' import {addListeners, render, setup} from '#testHelpers' +import { + defineShadowInputCustomElementIfNotDefined, + ShadowInput, +} from '../_helpers/shadow-input' test('type without focus', async () => { const {element, user} = setup('', {focus: false}) @@ -163,3 +167,14 @@ test('disabling activeElement moves action to HTMLBodyElement', async () => { body - keyup: c `) }) + +describe('on shadow DOM', () => { + test('type into an input element', async () => { + defineShadowInputCustomElementIfNotDefined() + const {element, user} = setup('') + + await user.keyboard('test') + + expect(element.value).toEqual('test') + }) +}) From 546063467112b70407bbd462a631ad632f594f2a Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Tue, 16 Aug 2022 23:03:43 +0200 Subject: [PATCH 21/25] Improve focus handling for setup --- tests/_helpers/setup.ts | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/_helpers/setup.ts b/tests/_helpers/setup.ts index 4e177992..a87be5b0 100644 --- a/tests/_helpers/setup.ts +++ b/tests/_helpers/setup.ts @@ -28,7 +28,7 @@ export function render( if (typeof focus === 'string') { ;(assertSingleNodeFromXPath(focus, div) as HTMLElement).focus() } else if (focus !== false) { - const element: HTMLElement | null = getFocusableElement(div) + const element: HTMLElement | null = findFocusable(div) element?.focus() } @@ -103,23 +103,14 @@ export function setup( ...render(ui, {eventHandlers, focus, selection}), } } -export function getFocusableElement( - parent: Element | ShadowRoot, -): HTMLElement | null { - const possibleFocusableElement: HTMLElement | null = - parent.querySelector(FOCUSABLE_SELECTOR) - if (possibleFocusableElement) { - return possibleFocusableElement - } - - const children = Array.from(parent.children) - for (const child of children) { - if ('shadowRoot' in child && child.shadowRoot) { - const possibleFocusableChildElement = getFocusableElement( - child.shadowRoot, - ) - if (possibleFocusableChildElement) { - return possibleFocusableChildElement +function findFocusable(container: Element | ShadowRoot): HTMLElement | null { + for (const el of Array.from(container.querySelectorAll('*'))) { + if (el.matches(FOCUSABLE_SELECTOR)) { + return el as HTMLElement + } else if (el.shadowRoot) { + const f = findFocusable(el.shadowRoot) + if (f) { + return f } } } From dea92873e242ccb748a4e3610de492a7e5b28e4e Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Thu, 18 Aug 2022 09:59:27 +0000 Subject: [PATCH 22/25] ease setting up tests --- tests/_helpers/elements/index.ts | 21 +++++++++++++++++++ tests/_helpers/{ => elements}/shadow-input.ts | 10 +-------- tests/_helpers/index.ts | 16 ++++++++++++++ tests/clipboard/copy.ts | 2 -- tests/clipboard/cut.ts | 4 +--- tests/clipboard/paste.ts | 9 ++++---- tests/keyboard/index.ts | 11 ++++------ tests/utils/focus/getActiveElement.ts | 2 -- 8 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 tests/_helpers/elements/index.ts rename tests/_helpers/{ => elements}/shadow-input.ts (78%) diff --git a/tests/_helpers/elements/index.ts b/tests/_helpers/elements/index.ts new file mode 100644 index 00000000..20015454 --- /dev/null +++ b/tests/_helpers/elements/index.ts @@ -0,0 +1,21 @@ +import {ShadowInput} from './shadow-input' + +const customElements = { + 'shadow-input': ShadowInput, +} + +export type CustomElements = { + [k in keyof typeof customElements]: typeof customElements[k] extends { + new (): infer T + } + ? T + : never +} + +export function registerCustomElements() { + Object.entries(customElements).forEach(([name, constructor]) => { + if (!globalThis.customElements.get(name)) { + globalThis.customElements.define(name, constructor) + } + }) +} diff --git a/tests/_helpers/shadow-input.ts b/tests/_helpers/elements/shadow-input.ts similarity index 78% rename from tests/_helpers/shadow-input.ts rename to tests/_helpers/elements/shadow-input.ts index 404103ba..1e454c6e 100644 --- a/tests/_helpers/shadow-input.ts +++ b/tests/_helpers/elements/shadow-input.ts @@ -5,7 +5,7 @@ template.innerHTML = ` ` -class ShadowInput extends HTMLElement { +export class ShadowInput extends HTMLElement { private $input?: HTMLInputElement static getObservedAttributes() { @@ -40,11 +40,3 @@ class ShadowInput extends HTMLElement { return this.$input?.value ?? '' } } - -export type {ShadowInput} - -export function defineShadowInputCustomElementIfNotDefined() { - if (window.customElements.get('shadow-input') === undefined) { - window.customElements.define('shadow-input', ShadowInput) - } -} diff --git a/tests/_helpers/index.ts b/tests/_helpers/index.ts index 13c9b71c..13d293be 100644 --- a/tests/_helpers/index.ts +++ b/tests/_helpers/index.ts @@ -1,5 +1,21 @@ +import {CustomElements, registerCustomElements} from './elements' + // this is pretty helpful: // https://codesandbox.io/s/quizzical-worker-eo909 +expect.addSnapshotSerializer({ + test: (val: unknown) => + Boolean( + typeof val === 'object' + ? Object.prototype.hasOwnProperty.call(val, 'snapshot') + : false, + ), + print: val => String((val)?.snapshot), +}) + +registerCustomElements() + +export type {CustomElements} + export {render, setup} from './setup' export {addEventListener, addListeners} from './listeners' diff --git a/tests/clipboard/copy.ts b/tests/clipboard/copy.ts index 8ae5703b..8d42d2db 100644 --- a/tests/clipboard/copy.ts +++ b/tests/clipboard/copy.ts @@ -1,4 +1,3 @@ -import {defineShadowInputCustomElementIfNotDefined} from '../_helpers/shadow-input' import userEvent from '#src' import {render, setup} from '#testHelpers' @@ -102,7 +101,6 @@ describe('without Clipboard API', () => { describe('on shadow DOM', () => { test('copy in an input element', async () => { - defineShadowInputCustomElementIfNotDefined() const {user} = setup('', { selection: {anchorOffset: 0, focusOffset: 4}, }) diff --git a/tests/clipboard/cut.ts b/tests/clipboard/cut.ts index 2c526381..ccde5b1d 100644 --- a/tests/clipboard/cut.ts +++ b/tests/clipboard/cut.ts @@ -1,5 +1,4 @@ -import type {ShadowInput} from '../_helpers/shadow-input' -import {defineShadowInputCustomElementIfNotDefined} from '../_helpers/shadow-input' +import type {ShadowInput} from '../_helpers/elements/shadow-input' import userEvent from '#src' import {render, setup} from '#testHelpers' @@ -110,7 +109,6 @@ describe('without Clipboard API', () => { }) describe('on shadow DOM', () => { test('cut in an input element', async () => { - defineShadowInputCustomElementIfNotDefined() const {element, user} = setup( '', { diff --git a/tests/clipboard/paste.ts b/tests/clipboard/paste.ts index 7cc03e51..39e0b459 100644 --- a/tests/clipboard/paste.ts +++ b/tests/clipboard/paste.ts @@ -1,7 +1,5 @@ -import type {ShadowInput} from '../_helpers/shadow-input' -import {defineShadowInputCustomElementIfNotDefined} from '../_helpers/shadow-input' import userEvent from '#src' -import {render, setup} from '#testHelpers' +import {CustomElements, render, setup} from '#testHelpers' import {createDataTransfer} from '#src/utils' test('paste with empty clipboard', async () => { @@ -154,8 +152,9 @@ describe('without Clipboard API', () => { describe('on shadow DOM', () => { test('paste into an input element', async () => { - defineShadowInputCustomElementIfNotDefined() - const {element, user} = setup('') + const {element, user} = setup( + '', + ) await user.paste('test') diff --git a/tests/keyboard/index.ts b/tests/keyboard/index.ts index 0592f5b3..275b19ea 100644 --- a/tests/keyboard/index.ts +++ b/tests/keyboard/index.ts @@ -1,9 +1,5 @@ import userEvent from '#src' -import {addListeners, render, setup} from '#testHelpers' -import { - defineShadowInputCustomElementIfNotDefined, - ShadowInput, -} from '../_helpers/shadow-input' +import {addListeners, CustomElements, render, setup} from '#testHelpers' test('type without focus', async () => { const {element, user} = setup('', {focus: false}) @@ -170,8 +166,9 @@ test('disabling activeElement moves action to HTMLBodyElement', async () => { describe('on shadow DOM', () => { test('type into an input element', async () => { - defineShadowInputCustomElementIfNotDefined() - const {element, user} = setup('') + const {element, user} = setup( + '', + ) await user.keyboard('test') diff --git a/tests/utils/focus/getActiveElement.ts b/tests/utils/focus/getActiveElement.ts index c303f476..13d630ad 100644 --- a/tests/utils/focus/getActiveElement.ts +++ b/tests/utils/focus/getActiveElement.ts @@ -1,4 +1,3 @@ -import {defineShadowInputCustomElementIfNotDefined} from '../../_helpers/shadow-input' import {setup} from '#testHelpers' import {getActiveElementOrBody} from '#src/utils' @@ -16,7 +15,6 @@ test('default to body as active element', async () => { describe('on shadow DOM', () => { test('get focused element inside shadow tree', async () => { - defineShadowInputCustomElementIfNotDefined() const {element} = setup('') expect(getActiveElementOrBody(document)).toBe( From e36e922202abaca6d3a0ea1f0b5922e128c74c3a Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Thu, 18 Aug 2022 10:02:31 +0000 Subject: [PATCH 23/25] clean up import --- tests/clipboard/cut.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/clipboard/cut.ts b/tests/clipboard/cut.ts index ccde5b1d..5a887792 100644 --- a/tests/clipboard/cut.ts +++ b/tests/clipboard/cut.ts @@ -1,6 +1,5 @@ -import type {ShadowInput} from '../_helpers/elements/shadow-input' import userEvent from '#src' -import {render, setup} from '#testHelpers' +import {CustomElements, render, setup} from '#testHelpers' test('cut selected value', async () => { const {getEvents, user} = setup( @@ -109,7 +108,7 @@ describe('without Clipboard API', () => { }) describe('on shadow DOM', () => { test('cut in an input element', async () => { - const {element, user} = setup( + const {element, user} = setup( '', { selection: {anchorOffset: 0, focusOffset: 4}, From 7f803bf95b4a72d0ef0ce36bd330097224bb29c4 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Thu, 18 Aug 2022 10:14:34 +0000 Subject: [PATCH 24/25] prevent accidental import of wrong util --- src/utils/focus/getActiveElement.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils/focus/getActiveElement.ts b/src/utils/focus/getActiveElement.ts index 725c2524..5f7b0769 100644 --- a/src/utils/focus/getActiveElement.ts +++ b/src/utils/focus/getActiveElement.ts @@ -1,8 +1,6 @@ import {isDisabled} from '../misc/isDisabled' -export function getActiveElement( - document: Document | ShadowRoot, -): Element | null { +function getActiveElement(document: Document | ShadowRoot): Element | null { const activeElement = document.activeElement if (activeElement?.shadowRoot) { From a34241576969b16520978f9a69bec88d4609b159 Mon Sep 17 00:00:00 2001 From: Christian Siebmanns Date: Fri, 15 Sep 2023 21:37:45 +0200 Subject: [PATCH 25/25] Fix build --- src/clipboard/copy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clipboard/copy.ts b/src/clipboard/copy.ts index b38bc3a2..b51fff60 100644 --- a/src/clipboard/copy.ts +++ b/src/clipboard/copy.ts @@ -1,6 +1,6 @@ import {copySelection} from '../document' -import type {type Instance} from '../setup' -import {writeDataTransferToClipboard, getActiveElementOrBody,} from '../utils' +import {type Instance} from '../setup' +import {writeDataTransferToClipboard, getActiveElementOrBody} from '../utils' export async function copy(this: Instance) { const doc = this.config.document