Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(clipboard): support input elements in shadow DOM #1033

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0b858a0
Potentially fix paste
Christian24 Aug 7, 2022
586f2c4
Fixed cut, paste and copy for open shadow dom
Christian24 Aug 8, 2022
42efa62
Update tests/dom/customElement.ts
Christian24 Aug 14, 2022
0446f16
Moved Shadow DOM tests. Added focus and selection support for shadow …
Christian24 Aug 14, 2022
70279f0
Added some tests for getActiveElement
Christian24 Aug 14, 2022
86f94d2
Added some tests for getActiveElement
Christian24 Aug 14, 2022
aedd838
Fix type import
Christian24 Aug 15, 2022
340f73b
Fix type import
Christian24 Aug 15, 2022
0853cd6
Update tests/utils/focus/getActiveElement.ts
Christian24 Aug 15, 2022
83508b5
Add helper to define element. Remove describe block
Christian24 Aug 15, 2022
f170d22
Use getActiveElementOrBody
Christian24 Aug 15, 2022
bed1eb1
Update tests/utils/focus/getActiveElement.ts
Christian24 Aug 15, 2022
28e8995
Make focusing custom elements safer
Christian24 Aug 15, 2022
418093a
Update tests/utils/focus/getActiveElement.ts
Christian24 Aug 16, 2022
494c1bf
Remove test
Christian24 Aug 16, 2022
a5f79d4
Removed types
Christian24 Aug 16, 2022
6526a25
Add focus helper
Christian24 Aug 16, 2022
5d44e0b
Delete test
Christian24 Aug 16, 2022
db15efe
Improve focus handling for setup
Christian24 Aug 16, 2022
eb50b90
Improve focus handling for setup Add test for keyboard input on open …
Christian24 Aug 16, 2022
5460634
Improve focus handling for setup
Christian24 Aug 16, 2022
dea9287
ease setting up tests
ph-fritsche Aug 18, 2022
e36e922
clean up import
ph-fritsche Aug 18, 2022
7f803bf
prevent accidental import of wrong util
ph-fritsche Aug 18, 2022
a342415
Fix build
Christian24 Sep 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/clipboard/copy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {copySelection} from '../document'
import {type Instance} from '../setup'
import {writeDataTransferToClipboard} from '../utils'
import {writeDataTransferToClipboard, getActiveElementOrBody} from '../utils'

export async function copy(this: Instance) {
const doc = this.config.document
const target = doc.activeElement ?? /* istanbul ignore next */ doc.body
const target = getActiveElementOrBody(doc)

const clipboardData = copySelection(target)

Expand Down
4 changes: 2 additions & 2 deletions src/clipboard/cut.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {copySelection} from '../document'
import {type Instance} from '../setup'
import {writeDataTransferToClipboard} from '../utils'
import {writeDataTransferToClipboard, getActiveElementOrBody} from '../utils'

export async function cut(this: Instance) {
const doc = this.config.document
const target = doc.activeElement ?? /* istanbul ignore next */ doc.body
const target = getActiveElementOrBody(doc)

const clipboardData = copySelection(target)

Expand Down
4 changes: 2 additions & 2 deletions src/clipboard/paste.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {type Instance} from '../setup'
import {
createDataTransfer,
getActiveElementOrBody,
getWindow,
readDataTransferFromClipboard,
} from '../utils'
Expand All @@ -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 = getActiveElementOrBody(doc)
const dataTransfer: DataTransfer =
(typeof clipboardData === 'string'
? getClipboardDataFromString(doc, clipboardData)
Expand Down
6 changes: 3 additions & 3 deletions src/event/focus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {findClosest, getActiveElement, isFocusable} from '../utils'
import {findClosest, getActiveElementOrBody, isFocusable} from '../utils'
import {updateSelectionOnFocus} from './selection'
import {wrapEvent} from './wrapEvent'

Expand All @@ -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) {
Expand All @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions src/utils/focus/getActiveElement.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions tests/_helpers/elements/index.ts
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
42 changes: 42 additions & 0 deletions tests/_helpers/elements/shadow-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const observed = ['value']

const template = document.createElement('template')
template.innerHTML = `
<input>
`

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 ?? ''
}
}
16 changes: 16 additions & 0 deletions tests/_helpers/index.ts
Original file line number Diff line number Diff line change
@@ -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((<null | {snapshot?: string}>val)?.snapshot),
})

registerCustomElements()

export type {CustomElements}

export {render, setup} from './setup'
export {addEventListener, addListeners} from './listeners'
23 changes: 17 additions & 6 deletions tests/_helpers/setup.ts
Original file line number Diff line number Diff line change
@@ -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<Elements extends Element | Element[] = HTMLElement>(
Expand All @@ -28,24 +28,22 @@ export function render<Elements extends Element | Element[] = HTMLElement>(
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 = findFocusable(div)
element?.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)
: focusNode
const focusOffset = selection.focusOffset ?? 0
const anchorOffset = selection.anchorOffset ?? focusOffset

if (!focusNode || !anchorNode) {
throw new Error(`missing/invalid selection.focusNode`)
}
setSelection({
focusNode,
anchorNode,
Expand Down Expand Up @@ -105,3 +103,16 @@ export function setup<Elements extends Element | Element[] = HTMLElement>(
...render<Elements>(ui, {eventHandlers, focus, selection}),
}
}
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
}
}
}
return null
}
12 changes: 12 additions & 0 deletions tests/clipboard/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,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('<shadow-input value="test"></shadow-input>', {
selection: {anchorOffset: 0, focusOffset: 4},
})

const data = await user.copy()

expect(data?.getData('text')).toEqual('test')
})
})
17 changes: 16 additions & 1 deletion tests/clipboard/cut.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import userEvent from '#src'
import {render, setup} from '#testHelpers'
import {CustomElements, render, setup} from '#testHelpers'

test('cut selected value', async () => {
const {getEvents, user} = setup<HTMLInputElement>(
Expand Down Expand Up @@ -106,3 +106,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<CustomElements['shadow-input']>(
'<shadow-input value="test"></shadow-input>',
{
selection: {anchorOffset: 0, focusOffset: 4},
},
)

const data = await user.cut()

expect(data?.getData('text')).toEqual('test')
expect(element.value.length).toEqual(0)
})
})
14 changes: 13 additions & 1 deletion tests/clipboard/paste.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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 () => {
Expand Down Expand Up @@ -149,3 +149,15 @@ describe('without Clipboard API', () => {
expect(getEvents()).toHaveLength(0)
})
})

describe('on shadow DOM', () => {
test('paste into an input element', async () => {
const {element, user} = setup<CustomElements['shadow-input']>(
'<shadow-input></shadow-input>',
)

await user.paste('test')

expect(element.value).toEqual('test')
})
})
87 changes: 0 additions & 87 deletions tests/dom/customElement.ts

This file was deleted.

14 changes: 13 additions & 1 deletion tests/keyboard/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import userEvent from '#src'
import {addListeners, render, setup} from '#testHelpers'
import {addListeners, CustomElements, render, setup} from '#testHelpers'

test('type without focus', async () => {
const {element, user} = setup('<input/>', {focus: false})
Expand Down Expand Up @@ -163,3 +163,15 @@ test('disabling activeElement moves action to HTMLBodyElement', async () => {
body - keyup: c
`)
})

describe('on shadow DOM', () => {
test('type into an input element', async () => {
const {element, user} = setup<CustomElements['shadow-input']>(
'<shadow-input></shadow-input>',
)

await user.keyboard('test')

expect(element.value).toEqual('test')
})
})
Loading