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

feat(type): replace selected text #306

Closed
wants to merge 3 commits into from
Closed
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
56 changes: 56 additions & 0 deletions src/__tests__/type.js
Expand Up @@ -34,6 +34,62 @@ test('should append text all at once', async () => {
expect(screen.getByTestId('input')).toHaveProperty('value', 'hello world')
})

test('should replace selected text one by one', async () => {
const onChange = jest.fn()
render(
<input
data-testid="input"
defaultValue="hello world"
onChange={onChange}
/>,
)
const selectionStart = 'hello world'.search('world')
const selectionEnd = selectionStart + 'world'.length
screen.getByTestId('input').setSelectionRange(selectionStart, selectionEnd)
await userEvent.type(screen.getByTestId('input'), 'friend')
expect(onChange).toHaveBeenCalledTimes('friend'.length)
expect(screen.getByTestId('input')).toHaveValue('hello friend')
})

test('should replace selected text one by one up to maxLength if provided', async () => {
const maxLength = 10
const onChange = jest.fn()
render(
<input
data-testid="input"
defaultValue="hello world"
onChange={onChange}
maxLength={maxLength}
/>,
)
const selectionStart = 'hello world'.search('world')
const selectionEnd = selectionStart + 'world'.length
screen.getByTestId('input').setSelectionRange(selectionStart, selectionEnd)
const resultIfUnlimited = 'hello friend'
const slicedText = resultIfUnlimited.slice(0, maxLength)
await userEvent.type(screen.getByTestId('input'), 'friend')
const truncatedCharCount = resultIfUnlimited.length - slicedText.length
expect(onChange).toHaveBeenCalledTimes('friend'.length - truncatedCharCount)
expect(screen.getByTestId('input')).toHaveValue(slicedText)
})

test('should replace selected text all at once', async () => {
const onChange = jest.fn()
render(
<input
data-testid="input"
defaultValue="hello world"
onChange={onChange}
/>,
)
const selectionStart = 'hello world'.search('world')
const selectionEnd = selectionStart + 'world'.length
screen.getByTestId('input').setSelectionRange(selectionStart, selectionEnd)
await userEvent.type(screen.getByTestId('input'), 'friend', {allAtOnce: true})
expect(onChange).toHaveBeenCalledTimes(1)
expect(screen.getByTestId('input')).toHaveValue('hello friend')
})

test('should not type when event.preventDefault() is called', async () => {
const onChange = jest.fn()
const onKeydown = jest
Expand Down
30 changes: 26 additions & 4 deletions src/index.js
Expand Up @@ -332,12 +332,20 @@ async function type(...args) {
return result
}

function replaceElementSelection(element, text) {
return (
element.value.substr(0, element.selectionStart) +
text +
element.value.substr(element.selectionEnd)
)
}

async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
if (element.disabled) return

element.focus()

// The focussed element could change between each event, so get the currently active element each time
// 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

Expand All @@ -349,12 +357,23 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
)
: text

const textAfterReplacingSelection = replaceElementSelection(element, text)
const slicedTextAfterReplacingSelection = textAfterReplacingSelection.slice(
0,
element.maxLength || textAfterReplacingSelection.length,
)

if (allAtOnce) {
if (!element.readOnly) {
const previousText = element.value

fireEvent.input(element, {
target: {value: previousText + computeText()},
target: {
value:
element.selectionEnd > element.selectionStart
? slicedTextAfterReplacingSelection
: previousText + computeText(),
},
})
}
} else {
Expand All @@ -381,13 +400,16 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
charCode: keyCode,
})

const isTextPastThreshold = !computeText().length
const hasSelection = element.selectionEnd > element.selectionStart
const isTextPastThreshold = !hasSelection && !computeText().length

if (pressEvent && !isTextPastThreshold) {
if (!element.readOnly) {
fireEvent.input(currentElement(), {
target: {
value: currentValue() + key,
value: hasSelection
? replaceElementSelection(element, key)
: currentValue() + key,
},
bubbles: true,
cancelable: true,
Expand Down