diff --git a/src/__tests__/helpers/customElement.js b/src/__tests__/helpers/customElement.js
new file mode 100644
index 00000000..e3d1c801
--- /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__/type.js b/src/__tests__/type.js
index 71ab097b..8735dfee 100644
--- a/src/__tests__/type.js
+++ b/src/__tests__/type.js
@@ -1,7 +1,8 @@
import React, {Fragment} from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '..'
-import {setup} from './helpers/utils'
+import {setup, addListeners} from './helpers/utils'
+import './helpers/customElement'
it('types text in input', async () => {
const {element, getEventCalls} = setup()
@@ -23,6 +24,31 @@ it('types text in input', async () => {
`)
})
+it('types text inside custom element', async () => {
+ const {
+ container: {firstChild: customElement},
+ } = render()
+ const inputEl = customElement.shadowRoot.querySelector('input')
+ const {getEventCalls} = addListeners(inputEl)
+
+ await userEvent.type(inputEl, 'Sup')
+ 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 b5885d8a..1281bf19 100644
--- a/src/type.js
+++ b/src/type.js
@@ -17,14 +17,23 @@ async function type(...args) {
return result
}
+const getActiveElement = document => {
+ const activeElement = document.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