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

userEvent.type on contenteditable #442

Closed
ph-fritsche opened this issue Sep 4, 2020 · 6 comments
Closed

userEvent.type on contenteditable #442

ph-fritsche opened this issue Sep 4, 2020 · 6 comments
Labels
bug Something isn't working released on @alpha
Milestone

Comments

@ph-fritsche
Copy link
Member

userEvent.type() on an element with contenteditable does not honor the cursor position / selection.

I've set up a code example to demonstrate the problem here:
https://codesandbox.io/s/usereventtype-on-contenteditable-obxib?file=/src/app.test.js

The input field with the content foo has an EventListener that just copies InputEvents event.target.textContent on another div.

When you click one of the buttons and start typing bar, you can see your input being inserted as expected.

Trying to emulate the behavior per userEvent.type fails.
When the cursor is in front of foo , it sets textContent to foobar instead of barfoo.
When the cursor is behind foo, it sets textContent to fbaroo instead if foobar.
When the content is selected, it sets textContent to baroo instead of bar.

@marcosvega91
Copy link
Member

Hi @ph-fritsche thanks for raising this :).

I think that it can be a problem with jsdom because it is not playing well with contenteditable div.

@ph-fritsche
Copy link
Member Author

There is a misconception

user-event/src/utils.js

Lines 114 to 118 in a5b3350

if (isContentEditable(element)) {
const range = element.ownerDocument.getSelection().getRangeAt(0)
return {selectionStart: range.startOffset, selectionEnd: range.endOffset}
}

The select/range offsets are not equivalent to the number of chars selected, but the DOM offset.

[ ] mark the selection:
<div>f[oo<span>ba]r</span>baz</div>
startContainer is the first TextNode with startOffset=1
endContainer is the second TextNode with endOffset=2

<div>[foo]</div>
startContainer is either the DIV or the TextNode with startOffset=0
endContainer is either the DIV with endOffset=1 or the TextNode with endOffset=3

The typing should take place on selection.focusNode at selection.focusOffset and replace everything between it and selection.anchorNode at selection.anchorOffset.
If the focus or anchor is outside of contenteditable, it should do nothing.

@marcosvega91
Copy link
Member

That's right. When this part was done the getSelection has different bugs. Behaviour of getSelection is different in jsdom rather than the browser.

For example this issue

@ph-fritsche
Copy link
Member Author

The selection itself seems to be working as expected.
jsdom/jsdom#3019 (comment)
https://codesandbox.io/s/focus-contenteditable-0ce8q?file=/src/app.js

I think we should try to make element.focus() have the same side effects in jsdom as in the browser.

Meanwhile we should note it as a known limitation here and reimplement the selection handling:

  • If there is a selection, replace DomNodes as the browser does
  • If there is no selection, assume a selection with zero length at the start of contenteditable

This should fix all test cases aside those with a previous selection. That would be overridden by element.focus() in the browser but not in Jsdom until a change is made there.

@ph-fritsche ph-fritsche mentioned this issue Dec 14, 2020
8 tasks
@silviubogan
Copy link

First

While experimenting with @testing-library/user-event I reached the following related issue, with jest in terminal:

image

The volto-slate in the screenshot above is the project I am working on, which uses Slate.js and is a Volto addon written in JavaScript with React.

I am using:

I will try to find more information about this issue, but I am not sure I will.

If there is something I can do to help the work on this progress, please let me know.

Second

Another issue which, I think, is related to this issue is that the two buttons in the following example both throw an exception:

https://codesandbox.io/s/usereventtype-on-contenteditable-forked-fqo5d?file=/index.js

This code sandbox uses the latest version of the dependencies:

image

Screenshots

image

image

@ph-fritsche
Copy link
Member Author

Resolved in #779

There seems to be still an issue with codesandbox's [email protected]
https://codesandbox.io/s/userevent-type-on-contenteditable-forked-ru65e?file=/src/app.test.js

But the following tests pass in the current test environment:

import userEvent from "#src"

function app() {
    document.body.innerHTML = ""
    document.body.innerHTML = `
  <div id="input" contenteditable>foo</div>
  <div id="output"></div>
  <button id="select">Select</button>
  <button id="start">Start</button>
  <button id="end">End</button>
  `

    const input = document.getElementById("input")
    const output = document.getElementById("output")
    const buttonSelect = document.getElementById("select")
    const buttonStart = document.getElementById("start")
    const buttonEnd = document.getElementById("end")

    function selectInput() {
        const selection = document.getSelection()

        input.focus()
        selection.removeAllRanges()
        selection.selectAllChildren(input)

        return selection
    }

    buttonSelect.addEventListener("click", () => selectInput())
    buttonStart.addEventListener("click", () => selectInput().collapseToStart())
    buttonEnd.addEventListener("click", () => selectInput().collapseToEnd())

    input.addEventListener("input", (event) => {
        output.innerHTML = event.target.textContent
    })

    return { input, output, buttonSelect, buttonStart, buttonEnd }
}

test("Type over content", () => {
    const { input, output, buttonSelect } = app()

    expect(input.textContent).toBe("foo")
    expect(output.textContent).toBe("")

    userEvent.click(buttonSelect)
    userEvent.keyboard("bar")

    expect(output.textContent).toBe("bar")
})

test("Type before content", () => {
    const { input, output, buttonStart } = app()

    expect(input.textContent).toBe("foo")
    expect(output.textContent).toBe("")

    userEvent.click(buttonStart)
    userEvent.keyboard("bar")

    expect(output.textContent).toBe("barfoo")
})
test("Type after content", () => {
    const { input, output, buttonEnd } = app()

    expect(input.textContent).toBe("foo")
    expect(output.textContent).toBe("")

    userEvent.click(buttonEnd)
    userEvent.keyboard("bar")

    expect(output.textContent).toBe("foobar")
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working released on @alpha
Projects
None yet
Development

No branches or pull requests

3 participants