Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ const config = require('kcd-scripts/jest')
module.exports = {
...config,
testEnvironment: 'jest-environment-jsdom',

// this repo is testing utils
testPathIgnorePatterns: config.testPathIgnorePatterns.filter(f => f !== '/__tests__/utils/'),
}
8 changes: 8 additions & 0 deletions src/__tests__/type.js
Original file line number Diff line number Diff line change
Expand Up @@ -1416,3 +1416,11 @@ test('type non-alphanumeric characters', () => {

expect(element).toHaveValue('https://test.local')
})

test('use {selectall} on <input type="number"/>', () => {
const {element} = setup(`<input type="number" value="0"/>`)

userEvent.type(element, '123{selectall}{backspace}4')

expect(element).toHaveValue(4)
})
13 changes: 13 additions & 0 deletions src/__tests__/utils/edit/isContentEditable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {setup} from '__tests__/helpers/utils'
import {isContentEditable} from '../../../utils'

test('report if element is contenteditable', () => {
const {elements} = setup(
`<div></div><div contenteditable="false"></div><div contenteditable></div><div contenteditable="true"></div>`,
)

expect(isContentEditable(elements[0])).toBe(false)
expect(isContentEditable(elements[1])).toBe(false)
expect(isContentEditable(elements[2])).toBe(true)
expect(isContentEditable(elements[3])).toBe(true)
})
72 changes: 72 additions & 0 deletions src/__tests__/utils/edit/selectionRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {getSelectionRange, setSelectionRange} from 'utils'
import {setup} from '__tests__/helpers/utils'

test('range on input', () => {
const {element} = setup('<input value="foo"/>')

expect(getSelectionRange(element as HTMLInputElement)).toEqual({
selectionStart: 0,
selectionEnd: 0,
})

setSelectionRange(element as HTMLInputElement, 0, 0)

expect(element).toHaveProperty('selectionStart', 0)
expect(element).toHaveProperty('selectionEnd', 0)
expect(getSelectionRange(element as HTMLInputElement)).toEqual({
selectionStart: 0,
selectionEnd: 0,
})

setSelectionRange(element as HTMLInputElement, 2, 3)

expect(element).toHaveProperty('selectionStart', 2)
expect(element).toHaveProperty('selectionEnd', 3)
expect(getSelectionRange(element as HTMLInputElement)).toEqual({
selectionStart: 2,
selectionEnd: 3,
})
})

test('range on contenteditable', () => {
const {element} = setup('<div contenteditable="true">foo</div>')

expect(getSelectionRange(element as HTMLInputElement)).toEqual({
selectionStart: null,
selectionEnd: null,
})

setSelectionRange(element as HTMLDivElement, 0, 0)

expect(getSelectionRange(element as HTMLInputElement)).toEqual({
selectionStart: 0,
selectionEnd: 0,
})

setSelectionRange(element as HTMLDivElement, 2, 3)

expect(document.getSelection()?.anchorNode).toBe(element?.firstChild)
expect(document.getSelection()?.focusNode).toBe(element?.firstChild)
expect(document.getSelection()?.anchorOffset).toBe(2)
expect(document.getSelection()?.focusOffset).toBe(3)
expect(getSelectionRange(element as HTMLInputElement)).toEqual({
selectionStart: 2,
selectionEnd: 3,
})
})

test('range on input without selection support', () => {
const {element} = setup(`<input type="number" value="123"/>`)

expect(getSelectionRange(element as HTMLInputElement)).toEqual({
selectionStart: null,
selectionEnd: null,
})

setSelectionRange(element as HTMLInputElement, 1, 2)

expect(getSelectionRange(element as HTMLInputElement)).toEqual({
selectionStart: 1,
selectionEnd: 2,
})
})
4 changes: 2 additions & 2 deletions src/keyboard/plugins/arrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import {behaviorPlugin} from '../types'
import {isElementType, setSelectionRangeIfNecessary} from '../../utils'
import {isElementType, setSelectionRange} from '../../utils'

export const keydownBehavior: behaviorPlugin[] = [
{
Expand All @@ -24,7 +24,7 @@ export const keydownBehavior: behaviorPlugin[] = [
? selectionStart
: selectionEnd) ?? /* istanbul ignore next */ 0

setSelectionRangeIfNecessary(element, newPos, newPos)
setSelectionRange(element, newPos, newPos)
},
},
]
6 changes: 3 additions & 3 deletions src/keyboard/plugins/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
getValue,
isContentEditable,
isElementType,
setSelectionRangeIfNecessary,
setSelectionRange,
} from '../../utils'
import {fireInputEventIfNeeded} from '../shared'
import {calculateNewDeleteValue} from './control/calculateNewDeleteValue'
Expand All @@ -22,10 +22,10 @@ export const keydownBehavior: behaviorPlugin[] = [
handle: (keyDef, element) => {
// This could probably been improved by collapsing a selection range
if (keyDef.key === 'Home') {
setSelectionRangeIfNecessary(element, 0, 0)
setSelectionRange(element, 0, 0)
} else {
const newPos = getValue(element)?.length ?? /* istanbul ignore next */ 0
setSelectionRangeIfNecessary(element, newPos, newPos)
setSelectionRange(element, newPos, newPos)
}
},
},
Expand Down
13 changes: 10 additions & 3 deletions src/keyboard/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {behaviorPlugin} from '../types'
import {isElementType} from '../../utils'
import {isElementType, setSelectionRange} from '../../utils'
import * as arrowKeys from './arrow'
import * as controlKeys from './control'
import * as characterKeys from './character'
Expand All @@ -10,8 +10,15 @@ export const replaceBehavior: behaviorPlugin[] = [
matches: (keyDef, element) =>
keyDef.key === 'selectall' &&
isElementType(element, ['input', 'textarea']),
handle: (keyDef, element) => {
;(element as HTMLInputElement).select()
handle: (keyDef, element, options, state) => {
setSelectionRange(
element,
0,
(
state.carryValue ??
(element as HTMLInputElement | HTMLTextAreaElement).value
).length,
)
},
},
]
Expand Down
36 changes: 30 additions & 6 deletions src/keyboard/shared/fireInputEventIfNeeded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {
isElementType,
isClickableInput,
getValue,
hasUnreliableEmptyValue,
isContentEditable,
setSelectionRange,
} from '../../utils'
import {setSelectionRange} from './setSelectionRange'

export function fireInputEventIfNeeded({
currentElement,
Expand Down Expand Up @@ -42,11 +43,7 @@ export function fireInputEventIfNeeded({
})
}

setSelectionRange({
currentElement,
newValue,
newSelectionStart,
})
setSelectionRangeAfterInput(el, newValue, newSelectionStart)
}

return {prevValue}
Expand All @@ -55,3 +52,30 @@ export function fireInputEventIfNeeded({
function isReadonly(element: Element): boolean {
return isElementType(element, ['input', 'textarea'], {readOnly: true})
}

function setSelectionRangeAfterInput(
element: Element,
newValue: string,
newSelectionStart: number,
) {
// if we *can* change the selection start, then we will if the new value
// is the same as the current value (so it wasn't programatically changed
// when the fireEvent.input was triggered).
// The reason we have to do this at all is because it actually *is*
// programmatically changed by fireEvent.input, so we have to simulate the
// browser's default behavior
const value = getValue(element) as string

// don't apply this workaround on elements that don't necessarily report the visible value - e.g. number
if (
value === newValue ||
(value === '' && hasUnreliableEmptyValue(element))
) {
setSelectionRange(element, newSelectionStart, newSelectionStart)
} else {
// If the currentValue is different than the expected newValue and we *can*
// change the selection range, than we should set it to the length of the
// currentValue to ensure that the browser behavior is mimicked.
setSelectionRange(element, value.length, value.length)
}
}
1 change: 0 additions & 1 deletion src/keyboard/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './fireChangeForInputTimeIfValid'
export * from './fireInputEventIfNeeded'
export * from './setSelectionRange'
30 changes: 0 additions & 30 deletions src/keyboard/shared/setSelectionRange.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/paste.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {fireEvent} from '@testing-library/dom'
import {
setSelectionRangeIfNecessary,
setSelectionRange,
calculateNewValue,
eventWrapper,
isDisabled,
Expand Down Expand Up @@ -38,7 +38,7 @@ function paste(
// initialSelectionEnd is if you have an input with a value and want to
// explicitely start typing with the cursor at 0. Not super common.
if (element.selectionStart === 0 && element.selectionEnd === 0) {
setSelectionRangeIfNecessary(
setSelectionRange(
element,
initialSelectionStart ?? element.value.length,
initialSelectionEnd ?? element.value.length,
Expand All @@ -53,7 +53,7 @@ function paste(
inputType: 'insertFromPaste',
target: {value: newValue},
})
setSelectionRangeIfNecessary(
setSelectionRange(
element,

// TODO: investigate why the selection caused by invalid parameters was expected
Expand Down
15 changes: 2 additions & 13 deletions src/type/typeImplementation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {
setSelectionRangeIfNecessary,
setSelectionRange,
getSelectionRange,
getValue,
isContentEditable,
getActiveElement,
} from '../utils'
import {click} from '../click'
Expand Down Expand Up @@ -33,16 +32,6 @@ export async function typeImplementation(

if (!skipClick) click(element)

if (isContentEditable(element)) {
const selection = document.getSelection()
// istanbul ignore else
if (selection && selection.rangeCount === 0) {
const range = document.createRange()
range.setStart(element, 0)
range.setEnd(element, 0)
selection.addRange(range)
}
}
// The focused element could change between each event, so get the currently active element each time
const currentElement = () => getActiveElement(element.ownerDocument)

Expand All @@ -59,7 +48,7 @@ export async function typeImplementation(
const {selectionStart, selectionEnd} = getSelectionRange(element)

if (value != null && selectionStart === 0 && selectionEnd === 0) {
setSelectionRangeIfNecessary(
setSelectionRange(
currentElement() as Element,
initialSelectionStart ?? value.length,
initialSelectionEnd ?? value.length,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/edit/calculateNewValue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {isElementType} from 'utils/misc/isElementType'
import {getSelectionRange} from './getSelectionRange'
import {getSelectionRange} from './selectionRange'
import {getValue} from './getValue'
import {isValidDateValue} from './isValidDateValue'
import {isValidInputTimeValue} from './isValidInputTimeValue'
Expand Down
30 changes: 0 additions & 30 deletions src/utils/edit/getSelectionRange.ts

This file was deleted.

21 changes: 21 additions & 0 deletions src/utils/edit/hasUnreliableEmptyValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {isElementType} from 'utils/misc/isElementType'

enum unreliableValueInputTypes {
'number' = 'number',
}

/**
* Check if an empty IDL value on the element could mean a derivation of displayed value and IDL value
*/
export function hasUnreliableEmptyValue(
element: Element,
): element is HTMLInputElement & {type: unreliableValueInputTypes} {
return (
isElementType(element, 'input') &&
Boolean(
unreliableValueInputTypes[
element.type as keyof typeof unreliableValueInputTypes
],
)
)
}
Loading