Skip to content

Commit b83b259

Browse files
authoredJan 1, 2022
feat(keyboard): move cursor and delete content in contenteditable (#822)
1 parent ef2f4e5 commit b83b259

File tree

7 files changed

+620
-48
lines changed

7 files changed

+620
-48
lines changed
 

‎src/keyboard/plugins/arrow.ts

+42-16
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,54 @@
44
*/
55

66
import {behaviorPlugin} from '../types'
7-
import {isElementType, setSelection} from '../../utils'
7+
import {getNextCursorPosition, hasOwnSelection, setSelection} from '../../utils'
88
import {getUISelection} from '../../document'
99

1010
export const keydownBehavior: behaviorPlugin[] = [
1111
{
12-
// TODO: implement for contentEditable
13-
matches: (keyDef, element) =>
14-
(keyDef.key === 'ArrowLeft' || keyDef.key === 'ArrowRight') &&
15-
isElementType(element, ['input', 'textarea']),
12+
matches: keyDef =>
13+
keyDef.key === 'ArrowLeft' || keyDef.key === 'ArrowRight',
1614
handle: (keyDef, element) => {
17-
const selection = getUISelection(element as HTMLInputElement)
15+
// TODO: implement shift
1816

19-
// TODO: implement shift/ctrl
20-
setSelection({
21-
focusNode: element,
22-
focusOffset:
23-
selection.startOffset === selection.endOffset
24-
? selection.focusOffset + (keyDef.key === 'ArrowLeft' ? -1 : 1)
25-
: keyDef.key === 'ArrowLeft'
26-
? selection.startOffset
27-
: selection.endOffset,
28-
})
17+
if (hasOwnSelection(element)) {
18+
const selection = getUISelection(element as HTMLInputElement)
19+
20+
setSelection({
21+
focusNode: element,
22+
focusOffset:
23+
selection.startOffset === selection.endOffset
24+
? selection.focusOffset + (keyDef.key === 'ArrowLeft' ? -1 : 1)
25+
: keyDef.key === 'ArrowLeft'
26+
? selection.startOffset
27+
: selection.endOffset,
28+
})
29+
} else {
30+
const selection = element.ownerDocument.getSelection()
31+
32+
/* istanbul ignore if */
33+
if (!selection) {
34+
return
35+
}
36+
37+
if (selection.isCollapsed) {
38+
const nextPosition = getNextCursorPosition(
39+
selection.focusNode as Node,
40+
selection.focusOffset,
41+
keyDef.key === 'ArrowLeft' ? -1 : 1,
42+
)
43+
if (nextPosition) {
44+
setSelection({
45+
focusNode: nextPosition.node,
46+
focusOffset: nextPosition.offset,
47+
})
48+
}
49+
} else {
50+
selection[
51+
keyDef.key === 'ArrowLeft' ? 'collapseToStart' : 'collapseToEnd'
52+
]()
53+
}
54+
}
2955
},
3056
},
3157
]

‎src/utils/edit/prepareInput.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {fireEvent} from '@testing-library/dom'
22
import {calculateNewValue, editInputElement, getInputRange} from '../../utils'
3+
import {getNextCursorPosition} from '../focus/cursor'
34

45
export function prepareInput(
56
data: string,
@@ -16,11 +17,34 @@ export function prepareInput(
1617
if ('startContainer' in inputRange) {
1718
return {
1819
commit: () => {
19-
const del = !inputRange.collapsed
20+
let del: boolean = false
2021

21-
if (del) {
22+
if (!inputRange.collapsed) {
23+
del = true
2224
inputRange.deleteContents()
25+
} else if (
26+
['deleteContentBackward', 'deleteContentForward'].includes(inputType)
27+
) {
28+
const nextPosition = getNextCursorPosition(
29+
inputRange.startContainer,
30+
inputRange.startOffset,
31+
inputType === 'deleteContentBackward' ? -1 : 1,
32+
inputType,
33+
)
34+
if (nextPosition) {
35+
del = true
36+
const delRange = inputRange.cloneRange()
37+
if (
38+
delRange.comparePoint(nextPosition.node, nextPosition.offset) < 0
39+
) {
40+
delRange.setStart(nextPosition.node, nextPosition.offset)
41+
} else {
42+
delRange.setEnd(nextPosition.node, nextPosition.offset)
43+
}
44+
delRange.deleteContents()
45+
}
2346
}
47+
2448
if (data) {
2549
if (inputRange.endContainer.nodeType === 3) {
2650
const offset = inputRange.endOffset

‎src/utils/focus/cursor.ts

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import {isContentEditable, isElementType} from '..'
2+
3+
declare global {
4+
interface Text {
5+
nodeValue: string
6+
}
7+
}
8+
9+
export function getNextCursorPosition(
10+
node: Node,
11+
offset: number,
12+
direction: -1 | 1,
13+
inputType?: string,
14+
):
15+
| {
16+
node: Node
17+
offset: number
18+
}
19+
| undefined {
20+
// The behavior at text node zero offset is inconsistent.
21+
// When walking backwards:
22+
// Firefox always moves to zero offset and jumps over last offset.
23+
// Chrome jumps over zero offset per default but over last offset when Shift is pressed.
24+
// The cursor always moves to zero offset if the focus area (contenteditable or body) ends there.
25+
// When walking foward both ignore zero offset.
26+
// When walking over input elements the cursor moves before or after that element.
27+
// When walking over line breaks the cursor moves inside any following text node.
28+
29+
if (
30+
isTextNode(node) &&
31+
offset + direction >= 0 &&
32+
offset + direction <= node.nodeValue.length
33+
) {
34+
return {node, offset: offset + direction}
35+
}
36+
const nextNode = getNextCharacterContentNode(node, offset, direction)
37+
if (nextNode) {
38+
if (isTextNode(nextNode)) {
39+
return {
40+
node: nextNode,
41+
offset:
42+
direction > 0
43+
? Math.min(1, nextNode.nodeValue.length)
44+
: Math.max(nextNode.nodeValue.length - 1, 0),
45+
}
46+
} else if (isElementType(nextNode, 'br')) {
47+
const nextPlusOne = getNextCharacterContentNode(
48+
nextNode,
49+
undefined,
50+
direction,
51+
)
52+
if (!nextPlusOne) {
53+
// The behavior when there is no possible cursor position beyond the line break is inconsistent.
54+
// In Chrome outside of contenteditable moving before a leading line break is possible.
55+
// A leading line break can still be removed per deleteContentBackward.
56+
// A trailing line break on the other hand is not removed by deleteContentForward.
57+
if (direction < 0 && inputType === 'deleteContentBackward') {
58+
return {
59+
node: nextNode.parentNode as Node,
60+
offset: getOffset(nextNode),
61+
}
62+
}
63+
return undefined
64+
} else if (isTextNode(nextPlusOne)) {
65+
return {
66+
node: nextPlusOne,
67+
offset: direction > 0 ? 0 : nextPlusOne.nodeValue.length,
68+
}
69+
} else if (direction < 0 && isElementType(nextPlusOne, 'br')) {
70+
return {
71+
node: nextNode.parentNode as Node,
72+
offset: getOffset(nextNode),
73+
}
74+
} else {
75+
return {
76+
node: nextPlusOne.parentNode as Node,
77+
offset: getOffset(nextPlusOne) + (direction > 0 ? 0 : 1),
78+
}
79+
}
80+
} else {
81+
return {
82+
node: nextNode.parentNode as Node,
83+
offset: getOffset(nextNode) + (direction > 0 ? 1 : 0),
84+
}
85+
}
86+
}
87+
}
88+
89+
function getNextCharacterContentNode(
90+
node: Node,
91+
offset: number | undefined,
92+
direction: -1 | 1,
93+
) {
94+
const nextOffset = Number(offset) + (direction < 0 ? -1 : 0)
95+
if (
96+
offset !== undefined &&
97+
isElement(node) &&
98+
nextOffset >= 0 &&
99+
nextOffset < node.children.length
100+
) {
101+
node = node.children[nextOffset]
102+
}
103+
return walkNodes(
104+
node,
105+
direction === 1 ? 'next' : 'previous',
106+
isTreatedAsCharacterContent,
107+
)
108+
}
109+
110+
function isTreatedAsCharacterContent(node: Node): node is Text | HTMLElement {
111+
if (isTextNode(node)) {
112+
return true
113+
}
114+
if (isElement(node)) {
115+
if (isElementType(node, ['input', 'textarea'])) {
116+
return (node as HTMLInputElement).type !== 'hidden'
117+
} else if (isElementType(node, 'br')) {
118+
return true
119+
}
120+
}
121+
return false
122+
}
123+
124+
function getOffset(node: Node) {
125+
let i = 0
126+
while (node.previousSibling) {
127+
i++
128+
node = node.previousSibling
129+
}
130+
return i
131+
}
132+
133+
function isElement(node: Node): node is Element {
134+
return node.nodeType === 1
135+
}
136+
137+
function isTextNode(node: Node): node is Text {
138+
return node.nodeType === 3
139+
}
140+
141+
function walkNodes<T extends Node>(
142+
node: Node,
143+
direction: 'previous' | 'next',
144+
callback: (node: Node) => node is T,
145+
) {
146+
for (;;) {
147+
const sibling = node[`${direction}Sibling`]
148+
if (sibling) {
149+
node = getDescendant(sibling, direction === 'next' ? 'first' : 'last')
150+
if (callback(node)) {
151+
return node
152+
}
153+
} else if (
154+
node.parentNode &&
155+
(!isElement(node.parentNode) ||
156+
(!isContentEditable(node.parentNode) &&
157+
node.parentNode !== node.ownerDocument?.body))
158+
) {
159+
node = node.parentNode
160+
} else {
161+
break
162+
}
163+
}
164+
}
165+
166+
function getDescendant(node: Node, direction: 'first' | 'last') {
167+
while (node.hasChildNodes()) {
168+
node = node[`${direction}Child`] as ChildNode
169+
}
170+
return node
171+
}

‎src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from './edit/setFiles'
1919

2020
export * from './focus/blur'
2121
export * from './focus/copySelection'
22+
export * from './focus/cursor'
2223
export * from './focus/focus'
2324
export * from './focus/getActiveElement'
2425
export * from './focus/getTabDestination'

‎tests/keyboard/plugin/arrow.ts

+137-30
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,153 @@
11
import userEvent from '#src'
2+
import {setSelection} from '#src/utils'
23
import {setup} from '#testHelpers/utils'
34

4-
test('collapse selection to the left', async () => {
5-
const {element} = setup<HTMLInputElement>(`<input value="foobar"/>`)
6-
element.focus()
7-
element.setSelectionRange(2, 4)
5+
describe('in text input', () => {
6+
test('collapse selection to the left', async () => {
7+
const {element} = setup<HTMLInputElement>(`<input value="foobar"/>`)
8+
element.focus()
9+
element.setSelectionRange(2, 4)
810

9-
await userEvent.keyboard('[ArrowLeft]')
11+
await userEvent.keyboard('[ArrowLeft]')
1012

11-
expect(element.selectionStart).toBe(2)
12-
expect(element.selectionEnd).toBe(2)
13-
})
13+
expect(element.selectionStart).toBe(2)
14+
expect(element.selectionEnd).toBe(2)
15+
})
1416

15-
test('collapse selection to the right', async () => {
16-
const {element} = setup<HTMLInputElement>(`<input value="foobar"/>`)
17-
element.focus()
18-
element.setSelectionRange(2, 4)
17+
test('collapse selection to the right', async () => {
18+
const {element} = setup<HTMLInputElement>(`<input value="foobar"/>`)
19+
element.focus()
20+
element.setSelectionRange(2, 4)
1921

20-
await userEvent.keyboard('[ArrowRight]')
22+
await userEvent.keyboard('[ArrowRight]')
2123

22-
expect(element.selectionStart).toBe(4)
23-
expect(element.selectionEnd).toBe(4)
24-
})
24+
expect(element.selectionStart).toBe(4)
25+
expect(element.selectionEnd).toBe(4)
26+
})
27+
28+
test('move cursor left', async () => {
29+
const {element} = setup<HTMLInputElement>(`<input value="foobar"/>`)
30+
element.focus()
31+
element.setSelectionRange(2, 2)
32+
33+
await userEvent.keyboard('[ArrowLeft]')
34+
35+
expect(element.selectionStart).toBe(1)
36+
expect(element.selectionEnd).toBe(1)
37+
})
2538

26-
test('move cursor left', async () => {
27-
const {element} = setup<HTMLInputElement>(`<input value="foobar"/>`)
28-
element.focus()
29-
element.setSelectionRange(2, 2)
39+
test('move cursor right', async () => {
40+
const {element} = setup<HTMLInputElement>(`<input value="foobar"/>`)
41+
element.focus()
42+
element.setSelectionRange(2, 2)
3043

31-
await userEvent.keyboard('[ArrowLeft]')
44+
await userEvent.keyboard('[ArrowRight]')
3245

33-
expect(element.selectionStart).toBe(1)
34-
expect(element.selectionEnd).toBe(1)
46+
expect(element.selectionStart).toBe(3)
47+
expect(element.selectionEnd).toBe(3)
48+
})
3549
})
3650

37-
test('move cursor right', async () => {
38-
const {element} = setup<HTMLInputElement>(`<input value="foobar"/>`)
39-
element.focus()
40-
element.setSelectionRange(2, 2)
51+
describe('in contenteditable', () => {
52+
test('collapse selection to the left', async () => {
53+
const {element} = setup(
54+
`<div contenteditable><span>foo</span><span>bar</span></div>`,
55+
)
56+
setSelection({
57+
anchorNode: element.firstChild?.firstChild as Text,
58+
anchorOffset: 2,
59+
focusNode: element.lastChild?.firstChild as Text,
60+
focusOffset: 1,
61+
})
62+
63+
await userEvent.keyboard('[ArrowLeft]')
64+
65+
expect(element.ownerDocument.getSelection()).toHaveProperty(
66+
'focusNode',
67+
element.firstChild?.firstChild,
68+
)
69+
expect(element.ownerDocument.getSelection()).toHaveProperty(
70+
'focusOffset',
71+
2,
72+
)
73+
})
74+
75+
test('collapse selection to the right', async () => {
76+
const {element} = setup(
77+
`<div contenteditable><span>foo</span><span>bar</span></div>`,
78+
)
79+
setSelection({
80+
anchorNode: element.firstChild?.firstChild as Text,
81+
anchorOffset: 2,
82+
focusNode: element.lastChild?.firstChild as Text,
83+
focusOffset: 1,
84+
})
85+
86+
await userEvent.keyboard('[ArrowRight]')
87+
88+
expect(element.ownerDocument.getSelection()).toHaveProperty(
89+
'focusNode',
90+
element.lastChild?.firstChild,
91+
)
92+
expect(element.ownerDocument.getSelection()).toHaveProperty(
93+
'focusOffset',
94+
1,
95+
)
96+
})
97+
98+
test('move cursor to the left', async () => {
99+
const {
100+
elements: [, div],
101+
} = setup(
102+
`<span>abc</span><div contenteditable><span>foo</span><span>bar</span></div><span>def</span>`,
103+
)
104+
setSelection({
105+
focusNode: div.lastChild?.firstChild as Text,
106+
focusOffset: 1,
107+
})
108+
109+
await userEvent.keyboard('[ArrowLeft][ArrowLeft]')
110+
111+
expect(div.ownerDocument.getSelection()).toHaveProperty(
112+
'focusNode',
113+
div.firstChild?.firstChild,
114+
)
115+
expect(div.ownerDocument.getSelection()).toHaveProperty('focusOffset', 2)
116+
117+
await userEvent.keyboard('[ArrowLeft][ArrowLeft][ArrowLeft][ArrowLeft]')
118+
119+
expect(div.ownerDocument.getSelection()).toHaveProperty(
120+
'focusNode',
121+
div.firstChild?.firstChild,
122+
)
123+
expect(div.ownerDocument.getSelection()).toHaveProperty('focusOffset', 0)
124+
})
125+
126+
test('move cursor to the right', async () => {
127+
const {
128+
elements: [, div],
129+
} = setup(
130+
`<span>abc</span><div contenteditable><span>foo</span><span>bar</span></div><span>def</span>`,
131+
)
132+
setSelection({
133+
focusNode: div.firstChild?.firstChild as Text,
134+
focusOffset: 2,
135+
})
136+
137+
await userEvent.keyboard('[ArrowRight][ArrowRight]')
138+
139+
expect(div.ownerDocument.getSelection()).toHaveProperty(
140+
'focusNode',
141+
div.lastChild?.firstChild,
142+
)
143+
expect(div.ownerDocument.getSelection()).toHaveProperty('focusOffset', 1)
41144

42-
await userEvent.keyboard('[ArrowRight]')
145+
await userEvent.keyboard('[ArrowRight][ArrowRight][ArrowRight][ArrowRight]')
43146

44-
expect(element.selectionStart).toBe(3)
45-
expect(element.selectionEnd).toBe(3)
147+
expect(div.ownerDocument.getSelection()).toHaveProperty(
148+
'focusNode',
149+
div.lastChild?.firstChild,
150+
)
151+
expect(div.ownerDocument.getSelection()).toHaveProperty('focusOffset', 3)
152+
})
46153
})

‎tests/keyboard/plugin/functional.ts

+33
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import cases from 'jest-in-case'
22
import userEvent from '#src'
33
import {getUISelection, setUISelection, setUIValue} from '#src/document'
44
import {setup} from '#testHelpers/utils'
5+
import {setSelection} from '#src/utils'
56

67
test('produce extra events for the Control key when AltGraph is pressed', async () => {
78
const {element, getEventSnapshot, clearEventCalls} = setup(`<input/>`)
@@ -205,3 +206,35 @@ test('tab through elements', async () => {
205206
expect(getUISelection(elements[1])).toHaveProperty('startOffset', 0)
206207
expect(getUISelection(elements[1])).toHaveProperty('endOffset', 3)
207208
})
209+
210+
test('delete content in contenteditable', async () => {
211+
const {element, getEvents} = setup(
212+
`<div><span>---</span><div contenteditable><span id="foo">foo</span><input type="checkbox"/><span id="bar">bar</span></div><span>---</span></div>`,
213+
)
214+
element.querySelector('div')?.focus()
215+
setSelection({
216+
focusNode: document.getElementById('foo')?.firstChild as Text,
217+
focusOffset: 2,
218+
})
219+
220+
await userEvent.keyboard('[Backspace][Backspace][Backspace][Backspace]')
221+
222+
expect(getEvents('input')).toHaveLength(2)
223+
expect(element.innerHTML).toMatchInlineSnapshot(
224+
`<span>---</span><div contenteditable=""><span id="foo">o</span><input type="checkbox"><span id="bar">bar</span></div><span>---</span>`,
225+
)
226+
227+
await userEvent.keyboard('[ArrowRight][Delete]')
228+
229+
expect(getEvents('input')).toHaveLength(3)
230+
expect(element.innerHTML).toMatchInlineSnapshot(
231+
`<span>---</span><div contenteditable=""><span id="foo">o</span><span id="bar">bar</span></div><span>---</span>`,
232+
)
233+
234+
await userEvent.keyboard('[Delete]')
235+
236+
expect(getEvents('input')).toHaveLength(4)
237+
expect(element.innerHTML).toMatchInlineSnapshot(
238+
`<span>---</span><div contenteditable=""><span id="foo">o</span><span id="bar">ar</span></div><span>---</span>`,
239+
)
240+
})

‎tests/utils/focus/cursor.ts

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import cases from 'jest-in-case'
2+
import {getNextCursorPosition} from '#src/utils'
3+
import {setup} from '#testHelpers/utils'
4+
5+
cases<{
6+
html: string
7+
nodeSelector: string
8+
offset: number
9+
direction: -1 | 1
10+
inputType?: string
11+
expectedSelector?: string
12+
expectedOffset?: number
13+
}>(
14+
'get next cursor position',
15+
({
16+
html,
17+
nodeSelector,
18+
offset,
19+
direction,
20+
inputType,
21+
expectedSelector,
22+
expectedOffset,
23+
}) => {
24+
const {element} = setup(`<div>${html}</div>`)
25+
const node = document.evaluate(
26+
nodeSelector,
27+
element,
28+
null,
29+
XPathResult.FIRST_ORDERED_NODE_TYPE,
30+
).singleNodeValue
31+
expect(node).toBeTruthy()
32+
const expectedNode = expectedSelector
33+
? document.evaluate(
34+
expectedSelector,
35+
element,
36+
null,
37+
XPathResult.FIRST_ORDERED_NODE_TYPE,
38+
).singleNodeValue
39+
: undefined
40+
expect(expectedNode)[expectedSelector ? 'toBeTruthy' : 'toBeFalsy']()
41+
42+
document.getSelection()?.setPosition(node, offset)
43+
const nextPosition = getNextCursorPosition(
44+
node as Node,
45+
offset,
46+
direction,
47+
inputType,
48+
)
49+
50+
expect(nextPosition?.node).toBe(expectedNode)
51+
if (expectedNode) {
52+
expect(nextPosition).toHaveProperty('offset', expectedOffset)
53+
}
54+
},
55+
{
56+
'in text node forwards': {
57+
html: `foobar`,
58+
nodeSelector: 'text()',
59+
offset: 3,
60+
direction: 1,
61+
expectedSelector: 'text()',
62+
expectedOffset: 4,
63+
},
64+
'in text node backwards': {
65+
html: `foobar`,
66+
nodeSelector: 'text()',
67+
offset: 3,
68+
direction: -1,
69+
expectedSelector: 'text()',
70+
expectedOffset: 2,
71+
},
72+
'across text nodes forwards': {
73+
html: `<span>foo</span><span>bar</span>`,
74+
nodeSelector: '*[1]/text()',
75+
offset: 3,
76+
direction: 1,
77+
expectedSelector: '*[2]/text()',
78+
expectedOffset: 1,
79+
},
80+
'across text nodes backwards': {
81+
html: `<span>foo</span><span>bar</span>`,
82+
nodeSelector: '*[2]/text()',
83+
offset: 0,
84+
direction: -1,
85+
expectedSelector: '*[1]/text()',
86+
expectedOffset: 2,
87+
},
88+
'to start of body backwards': {
89+
html: `foobar`,
90+
nodeSelector: 'text()',
91+
offset: 1,
92+
direction: -1,
93+
expectedSelector: 'text()',
94+
expectedOffset: 0,
95+
},
96+
'to start of contenteditable backwards': {
97+
html: `<span>foo</span><span contenteditable>bar</span>`,
98+
nodeSelector: '*[@contenteditable]/text()',
99+
offset: 1,
100+
direction: -1,
101+
expectedSelector: '*[@contenteditable]/text()',
102+
expectedOffset: 0,
103+
},
104+
'over input forwards': {
105+
html: `<span>foo</span><input type="checkbox"><span>bar</span>`,
106+
nodeSelector: 'span[1]/text()',
107+
offset: 3,
108+
direction: 1,
109+
expectedSelector: '.',
110+
expectedOffset: 2,
111+
},
112+
'over input backwards': {
113+
html: `<span>foo</span><input type="checkbox"><span>bar</span>`,
114+
nodeSelector: 'span[2]/text()',
115+
offset: 0,
116+
direction: -1,
117+
expectedSelector: '.',
118+
expectedOffset: 1,
119+
},
120+
'over line break forwards to text node': {
121+
html: `<span>foo</span><br/><span>bar</span>`,
122+
nodeSelector: 'span[1]/text()',
123+
offset: 3,
124+
direction: 1,
125+
expectedSelector: 'span[2]/text()',
126+
expectedOffset: 0,
127+
},
128+
'over line break backwards to text node': {
129+
html: `<span>foo</span><br/><span>bar</span>`,
130+
nodeSelector: 'span[2]/text()',
131+
offset: 0,
132+
direction: -1,
133+
expectedSelector: 'span[1]/text()',
134+
expectedOffset: 3,
135+
},
136+
'over line break forwards to checkbox': {
137+
html: `<span>foo</span><input type="checkbox"/><span></span><br/><span></span><input type="checkbox"/><span>bar</span>`,
138+
nodeSelector: '.',
139+
offset: 2,
140+
direction: 1,
141+
expectedSelector: '.',
142+
expectedOffset: 5,
143+
},
144+
'over line break backwards to checkbox': {
145+
html: `<span>foo</span><input type="checkbox"/><span></span><br/><span></span><input type="checkbox"/><span>bar</span>`,
146+
nodeSelector: '.',
147+
offset: 5,
148+
direction: -1,
149+
expectedSelector: '.',
150+
expectedOffset: 2,
151+
},
152+
'over line break forwards to line break': {
153+
html: `<span>foo</span><br/><div></div><br/><span>bar</span>`,
154+
nodeSelector: 'span[1]/text()',
155+
offset: 3,
156+
direction: 1,
157+
expectedSelector: '.',
158+
expectedOffset: 3,
159+
},
160+
'over line break backwards to line break': {
161+
html: `<span>foo</span><br/><div></div><br/><span>bar</span>`,
162+
nodeSelector: 'span[2]/text()',
163+
offset: 0,
164+
direction: -1,
165+
expectedSelector: '.',
166+
expectedOffset: 3,
167+
},
168+
'over line break forwards at edge of focus area': {
169+
html: `<span></span><br/><span>foo</span><br/><span></span>`,
170+
nodeSelector: 'span[2]/text()',
171+
offset: 3,
172+
direction: 1,
173+
expectedSelector: undefined,
174+
expectedOffset: undefined,
175+
},
176+
'over line break backwards at edge of focus area': {
177+
html: `<span></span><br/><span>foo</span><br/><span></span>`,
178+
nodeSelector: 'span[2]/text()',
179+
offset: 0,
180+
direction: -1,
181+
expectedSelector: undefined,
182+
expectedOffset: undefined,
183+
},
184+
'over line break backwards at edge of focus area when deleting': {
185+
html: `<span></span><br/><span>foo</span><br/><span></span>`,
186+
nodeSelector: 'span[2]/text()',
187+
offset: 0,
188+
direction: -1,
189+
inputType: 'deleteContentBackward',
190+
expectedSelector: '.',
191+
expectedOffset: 1,
192+
},
193+
'at edge of focus area': {
194+
html: `foo<span contenteditable>bar</span>baz`,
195+
nodeSelector: 'span/text()',
196+
offset: 3,
197+
direction: 1,
198+
expectedSelector: undefined,
199+
expectedOffset: undefined,
200+
},
201+
'over comment': {
202+
html: `<span>foo</span><!--comment--><span>bar</span>`,
203+
nodeSelector: 'span[1]/text()',
204+
offset: 3,
205+
direction: 1,
206+
expectedSelector: 'span[2]/text()',
207+
expectedOffset: 1,
208+
},
209+
},
210+
)

0 commit comments

Comments
 (0)
Please sign in to comment.