Skip to content

Commit 52efb3b

Browse files
committed
feat(type): automatically release modifiers
1 parent 6981612 commit 52efb3b

File tree

2 files changed

+113
-26
lines changed

2 files changed

+113
-26
lines changed

src/user-event/__tests__/type-modifiers.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,3 +728,68 @@ test('{del} does not delete if keydown is prevented', async () => {
728728
expect(element.selectionEnd).toBe(2)
729729
expect(eventWasFired('input')).not.toBe(true)
730730
})
731+
732+
test('any remaining type modifiers are automatically released at the end', async () => {
733+
const {element, getEventSnapshot} = setup('<input />')
734+
735+
await userEvent.type(element, '{meta}{alt}{ctrl}a{/alt}')
736+
737+
expect(getEventSnapshot()).toMatchInlineSnapshot(`
738+
Events fired on: input[value="a"]
739+
740+
input[value=""] - pointerover
741+
input[value=""] - pointerenter
742+
input[value=""] - mouseover: Left (0)
743+
input[value=""] - mouseenter: Left (0)
744+
input[value=""] - pointermove
745+
input[value=""] - mousemove: Left (0)
746+
input[value=""] - pointerdown
747+
input[value=""] - mousedown: Left (0)
748+
input[value=""] - focus
749+
input[value=""] - focusin
750+
input[value=""] - pointerup
751+
input[value=""] - mouseup: Left (0)
752+
input[value=""] - click: Left (0)
753+
input[value=""] - keydown: Meta (93) {meta}
754+
input[value=""] - keydown: Alt (18) {alt}{meta}
755+
input[value=""] - keydown: Control (17) {alt}{meta}{ctrl}
756+
input[value=""] - keydown: a (97) {alt}{meta}{ctrl}
757+
input[value=""] - keypress: a (97) {alt}{meta}{ctrl}
758+
input[value="a"] - input
759+
"{CURSOR}" -> "a{CURSOR}"
760+
input[value="a"] - keyup: a (97) {alt}{meta}{ctrl}
761+
input[value="a"] - keyup: Alt (18) {meta}{ctrl}
762+
input[value="a"] - keyup: Meta (93) {ctrl}
763+
input[value="a"] - keyup: Control (17)
764+
`)
765+
})
766+
767+
test('modifiers will not be closed if skipAutoClose is enabled', async () => {
768+
const {element, getEventSnapshot} = setup('<input />')
769+
770+
await userEvent.type(element, '{meta}a', {skipAutoClose: true})
771+
772+
expect(getEventSnapshot()).toMatchInlineSnapshot(`
773+
Events fired on: input[value="a"]
774+
775+
input[value=""] - pointerover
776+
input[value=""] - pointerenter
777+
input[value=""] - mouseover: Left (0)
778+
input[value=""] - mouseenter: Left (0)
779+
input[value=""] - pointermove
780+
input[value=""] - mousemove: Left (0)
781+
input[value=""] - pointerdown
782+
input[value=""] - mousedown: Left (0)
783+
input[value=""] - focus
784+
input[value=""] - focusin
785+
input[value=""] - pointerup
786+
input[value=""] - mouseup: Left (0)
787+
input[value=""] - click: Left (0)
788+
input[value=""] - keydown: Meta (93) {meta}
789+
input[value=""] - keydown: a (97) {meta}
790+
input[value=""] - keypress: a (97) {meta}
791+
input[value="a"] - input
792+
"{CURSOR}" -> "a{CURSOR}"
793+
input[value="a"] - keyup: a (97) {meta}
794+
`)
795+
})

src/user-event/type.js

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ function setSelectionRangeIfNecessary(
2929
async function type(
3030
element,
3131
text,
32-
{skipClick = false, delay, initialSelectionStart, initialSelectionEnd} = {},
32+
{
33+
skipClick = false,
34+
skipAutoClose = false,
35+
delay,
36+
initialSelectionStart,
37+
initialSelectionEnd,
38+
} = {},
3339
) {
3440
if (element.disabled) return
3541

@@ -89,21 +95,34 @@ async function type(
8995

9096
function queueCallbacks() {
9197
const callbacks = []
98+
const modifierClosers = []
9299
let remainingString = text
93100
while (remainingString) {
94101
const eventKey = Object.keys(eventCallbackMap).find(key =>
95102
remainingString.startsWith(key),
96103
)
97104
if (eventKey) {
98-
callbacks.push(eventCallbackMap[eventKey])
105+
const modifierCallback = eventCallbackMap[eventKey]
106+
callbacks.push(modifierCallback)
107+
108+
// if this modifier has an associated "close" callback and the developer
109+
// doesn't close it themselves, then we close it for them automatically
110+
// Effectively if they send in: '{alt}a' then we type: '{alt}a{/alt}'
111+
if (
112+
!skipAutoClose &&
113+
modifierCallback.close &&
114+
!remainingString.includes(modifierCallback.close.name)
115+
) {
116+
modifierClosers.push(modifierCallback.close.fn)
117+
}
99118
remainingString = remainingString.slice(eventKey.length)
100119
} else {
101120
const character = remainingString[0]
102121
callbacks.push((...args) => typeCharacter(character, ...args))
103122
remainingString = remainingString.slice(1)
104123
}
105124
}
106-
return callbacks
125+
return [...callbacks, ...modifierClosers]
107126
}
108127

109128
async function runCallbacks(callbacks) {
@@ -498,33 +517,36 @@ function getEventCallbackMap({
498517
}
499518

500519
function modifier({name, key, keyCode, modifierProperty}) {
501-
return {
502-
[`{${name}}`]: async ({eventOverrides}) => {
503-
const newEventOverrides = {[modifierProperty]: true}
520+
async function open({eventOverrides}) {
521+
const newEventOverrides = {[modifierProperty]: true}
504522

505-
await fireEvent.keyDown(currentElement(), {
506-
key,
507-
keyCode,
508-
which: keyCode,
509-
...eventOverrides,
510-
...newEventOverrides,
511-
})
523+
await fireEvent.keyDown(currentElement(), {
524+
key,
525+
keyCode,
526+
which: keyCode,
527+
...eventOverrides,
528+
...newEventOverrides,
529+
})
512530

513-
return {eventOverrides: newEventOverrides}
514-
},
515-
[`{/${name}}`]: async ({eventOverrides}) => {
516-
const newEventOverrides = {[modifierProperty]: false}
531+
return {eventOverrides: newEventOverrides}
532+
}
533+
open.close = {name: [`{/${name}}`], fn: close}
534+
async function close({eventOverrides}) {
535+
const newEventOverrides = {[modifierProperty]: false}
517536

518-
await fireEvent.keyUp(currentElement(), {
519-
key,
520-
keyCode,
521-
which: keyCode,
522-
...eventOverrides,
523-
...newEventOverrides,
524-
})
537+
await fireEvent.keyUp(currentElement(), {
538+
key,
539+
keyCode,
540+
which: keyCode,
541+
...eventOverrides,
542+
...newEventOverrides,
543+
})
525544

526-
return {eventOverrides: newEventOverrides}
527-
},
545+
return {eventOverrides: newEventOverrides}
546+
}
547+
return {
548+
[`{${name}}`]: open,
549+
[`{/${name}}`]: close,
528550
}
529551
}
530552
}

0 commit comments

Comments
 (0)