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

Feature: Implement cursor navigation with navigation keys #404

Merged
merged 4 commits into from
Nov 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/__tests__/type.js
Expand Up @@ -1022,3 +1022,47 @@ test('should not type inside a contenteditable=false div', () => {
div - click: Left (0)
`)
})

test('navigation key: {arrowleft} and {arrowright} moves the cursor', () => {
const {element, getEventSnapshot} = setup('<input />')
userEvent.type(element, 'b{arrowleft}a{arrowright}c')
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="abc"]

input[value=""] - pointerover
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried this in the playground and here's what I got for the events (Note, I ignored mouse/pointer move events because they're too noisy in the playground):

textarea#textarea[value=""] - pointerdown
textarea#textarea[value=""] - mousedown
textarea#textarea[value=""] - focusin
textarea#textarea[value=""] - pointerup
textarea#textarea[value=""] - mouseup
textarea#textarea[value=""] - click
textarea#textarea[value=""] - keydown
textarea#textarea[value=""] - keypress
textarea#textarea[value="b"] - input
textarea#textarea[value="b"] - keyup
textarea#textarea[value="b"] - keydown
textarea#textarea[value="b"] - keypress
textarea#textarea[value="br"] - input
textarea#textarea[value="br"] - keyup
textarea#textarea[value="br"] - keydown
textarea#textarea[value="br"] - keypress
textarea#textarea[value="bre"] - input
textarea#textarea[value="bre"] - keyup
textarea#textarea[value="bre"] - keydown
textarea#textarea[value="bre"] - keypress
textarea#textarea[value="brea"] - input
textarea#textarea[value="brea"] - keyup
textarea#textarea[value="brea"] - keydown // <- here's the arrow down to go left
textarea#textarea[value="brea"] - keyup
textarea#textarea[value="brea"] - keydown
textarea#textarea[value="brea"] - keypress
textarea#textarea[value="breka"] - input
textarea#textarea[value="breka"] - keyup
textarea#textarea[value="breka"] - keydown // <- here's the arrow down to go right
textarea#textarea[value="breka"] - keyup
textarea#textarea[value="breka"] - keydown
textarea#textarea[value="breka"] - keypress
textarea#textarea[value="brekaf"] - input
textarea#textarea[value="brekaf"] - keyup
textarea#textarea[value="brekaf"] - keydown
textarea#textarea[value="brekaf"] - keypress
textarea#textarea[value="brekafa"] - input
textarea#textarea[value="brekafa"] - keyup
textarea#textarea[value="brekafa"] - keydown
textarea#textarea[value="brekafa"] - keypress
textarea#textarea[value="brekafas"] - input
textarea#textarea[value="brekafas"] - keyup
textarea#textarea[value="brekafas"] - keydown
textarea#textarea[value="brekafas"] - keypress
textarea#textarea[value="brekafast"] - input
textarea#textarea[value="brekafast"] - keyup

Notice that there's no "select" event in there. I'm not sure whether it's possible to avoid that event, but it would be good to avoid if we can.

Also, I think this test could be simplified if we change the input to: b{arrowleft}a{arrowright}c (which would result in abc, and much fewer events for the snapshot).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least from what I could investigate, the [setSelectionRange](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea/input-setselectionrange] will fire a select event, and I can't really seem to find another way to move the cursor.

input[value=""] - pointerenter
input[value=""] - mouseover: Left (0)
input[value=""] - mouseenter: Left (0)
input[value=""] - pointermove
input[value=""] - mousemove: Left (0)
input[value=""] - pointerdown
input[value=""] - mousedown: Left (0)
input[value=""] - focus
input[value=""] - focusin
input[value=""] - pointerup
input[value=""] - mouseup: Left (0)
input[value=""] - click: Left (0)
input[value=""] - keydown: b (98)
input[value=""] - keypress: b (98)
input[value="b"] - input
"{CURSOR}" -> "b{CURSOR}"
input[value="b"] - keyup: b (98)
input[value="b"] - keydown: ArrowLeft (37)
input[value="b"] - select
input[value="b"] - keyup: ArrowLeft (37)
input[value="b"] - keydown: a (97)
input[value="b"] - keypress: a (97)
input[value="ab"] - input
"{CURSOR}b" -> "ab{CURSOR}"
input[value="ab"] - select
input[value="ab"] - keyup: a (97)
input[value="ab"] - keydown: ArrowRight (39)
input[value="ab"] - select
input[value="ab"] - keyup: ArrowRight (39)
input[value="ab"] - keydown: c (99)
input[value="ab"] - keypress: c (99)
input[value="abc"] - input
"ab{CURSOR}" -> "abc{CURSOR}"
input[value="abc"] - keyup: c (99)
`)
})
50 changes: 50 additions & 0 deletions src/keys/navigation-key.js
@@ -0,0 +1,50 @@
import {fireEvent} from '@testing-library/dom'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your opinions on using this kind of code organization? I noticed there is 3 files for typing:

  1. utils
  2. type
  3. type-modifiers

I think navigation keys will end up adding a chunk (but not massive) amount of code. I opted for a new file but I am willing to move things around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably wouldn't have bothered making the new file myself, but I'm ok with it.


import {setSelectionRangeIfNecessary} from '../utils'

const keys = {
ArrowLeft: {
keyCode: 37,
},
ArrowRight: {
keyCode: 39,
},
}

function getSelectionRange(currentElement, key) {
const {selectionStart, selectionEnd} = currentElement()
const cursorChange = Number(key in keys) * (key === 'ArrowLeft' ? -1 : 1)
return {
selectionStart: selectionStart + cursorChange,
selectionEnd: selectionEnd + cursorChange,
}
}

function navigationKey(key) {
const event = {
key,
keyCode: keys[key].keyCode,
which: keys[key].keyCode,
}

return ({currentElement, eventOverrides}) => {
fireEvent.keyDown(currentElement(), {
...event,
...eventOverrides,
})

const range = getSelectionRange(currentElement, key)
setSelectionRangeIfNecessary(
currentElement(),
range.selectionStart,
range.selectionEnd,
)

fireEvent.keyUp(currentElement(), {
...event,
...eventOverrides,
})
}
}

export {navigationKey}
3 changes: 3 additions & 0 deletions src/type.js
Expand Up @@ -15,6 +15,7 @@ import {
isContentEditable,
} from './utils'
import {click} from './click'
import {navigationKey} from './keys/navigation-key'

const modifierCallbackMap = {
...createModifierCallbackEntries({
Expand Down Expand Up @@ -44,6 +45,8 @@ const modifierCallbackMap = {
}

const specialCharCallbackMap = {
'{arrowleft}': navigationKey('ArrowLeft'),
'{arrowright}': navigationKey('ArrowRight'),
'{enter}': handleEnter,
'{esc}': handleEsc,
'{del}': handleDel,
Expand Down