diff --git a/README.md b/README.md
index 0025ce7cc..09e8efd7f 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,6 @@ change the state of the checkbox.
-
- [Installation](#installation)
- [API](#api)
- [`click(element)`](#clickelement)
@@ -476,6 +475,7 @@ Thanks goes to these people ([emoji key][emojis]):
+
This project follows the [all-contributors][all-contributors] specification.
diff --git a/src/__tests__/helpers/customElement.js b/src/__tests__/helpers/customElement.js
new file mode 100644
index 000000000..e3d1c8015
--- /dev/null
+++ b/src/__tests__/helpers/customElement.js
@@ -0,0 +1,35 @@
+const observed = ['value']
+
+class CustomEl extends HTMLElement {
+ static getObservedAttributes() {
+ return observed
+ }
+
+ constructor() {
+ super()
+ this.attachShadow({mode: 'open'})
+ this.shadowRoot.innerHTML = ``
+ this.$input = this.shadowRoot.querySelector('input')
+ }
+
+ connectedCallback() {
+ observed.forEach(name => {
+ this.render(name, this.getAttribute(name))
+ })
+ }
+
+ attributeChangedCallback(name, oldVal, newVal) {
+ if (oldVal === newVal) return
+ this.render(name, newVal)
+ }
+
+ render(name, value) {
+ if (value == null) {
+ this.$input.removeAttribute(name)
+ } else {
+ this.$input.setAttribute(name, value)
+ }
+ }
+}
+
+customElements.define('custom-el', CustomEl)
diff --git a/src/__tests__/helpers/utils.js b/src/__tests__/helpers/utils.js
index 50f5d68de..a8bf05383 100644
--- a/src/__tests__/helpers/utils.js
+++ b/src/__tests__/helpers/utils.js
@@ -37,14 +37,21 @@ function addEventListener(el, type, listener, options) {
el.addEventListener(type, hijackedListener, options)
}
-function setup(ui) {
- const {
+function setup(ui, {shadowRootSelector} = {}) {
+ let hostElement
+ let {
container: {firstChild: element},
} = render(ui)
+
+ if (shadowRootSelector) {
+ hostElement = element
+ element = element.shadowRoot.querySelector(shadowRootSelector)
+ }
+
element.previousTestData = getTestData(element)
const {getEventCalls, clearEventCalls} = addListeners(element)
- return {element, getEventCalls, clearEventCalls}
+ return {element, hostElement, getEventCalls, clearEventCalls}
}
function addListeners(element) {
diff --git a/src/__tests__/type.js b/src/__tests__/type.js
index 71ab097b9..ea773541a 100644
--- a/src/__tests__/type.js
+++ b/src/__tests__/type.js
@@ -2,6 +2,7 @@ import React, {Fragment} from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '..'
import {setup} from './helpers/utils'
+import './helpers/customElement'
it('types text in input', async () => {
const {element, getEventCalls} = setup()
@@ -23,6 +24,29 @@ it('types text in input', async () => {
`)
})
+it('types text inside custom element', async () => {
+ const {element, hostElement, getEventCalls} = setup(, {
+ shadowRootSelector: 'input',
+ })
+
+ await userEvent.type(element, 'Sup', {hostElement})
+ expect(getEventCalls()).toMatchInlineSnapshot(`
+ focus
+ keydown: S (83)
+ keypress: S (83)
+ input: "{CURSOR}" -> "S"
+ keyup: S (83)
+ keydown: u (117)
+ keypress: u (117)
+ input: "S{CURSOR}" -> "Su"
+ keyup: u (117)
+ keydown: p (112)
+ keypress: p (112)
+ input: "Su{CURSOR}" -> "Sup"
+ keyup: p (112)
+ `)
+})
+
it('types text in textarea', async () => {
const {element, getEventCalls} = setup()
await userEvent.type(element, 'Sup')
diff --git a/src/type.js b/src/type.js
index b5885d8a6..013afad6d 100644
--- a/src/type.js
+++ b/src/type.js
@@ -17,14 +17,23 @@ async function type(...args) {
return result
}
+const getActiveElement = element => {
+ const activeElement = element.activeElement
+ if (activeElement.shadowRoot) {
+ return getActiveElement(activeElement.shadowRoot) || activeElement
+ } else {
+ return activeElement
+ }
+}
+
async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
if (element.disabled) return
element.focus()
// The focused element could change between each event, so get the currently active element each time
- const currentElement = () => element.ownerDocument.activeElement
- const currentValue = () => element.ownerDocument.activeElement.value
+ const currentElement = () => getActiveElement(element.ownerDocument)
+ const currentValue = () => currentElement().value
const setSelectionRange = newSelectionStart => {
// if the actual selection start is different from the one we expected
// then we set it to the end of the input