Skip to content

Commit 0f0e2ac

Browse files
authored
fix(type): unify selection behavior when typing (#3141)
Before typing/pressing, we focus the target element. WebKit sometimes selects the value in this case. To unify the behavior between the browsers we behave similar to human: - when the input is already focused, we just type; - when the input is not focused, we focus it, move caret to the start (like if user clicked at the start to focus the input) and then type. Note this only affects inputs with non-empty value.
1 parent 678d164 commit 0f0e2ac

File tree

3 files changed

+91
-5
lines changed

3 files changed

+91
-5
lines changed

src/dom.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -519,9 +519,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
519519
}, 0, 'elementHandle.focus');
520520
}
521521

522-
async _focus(progress: Progress): Promise<'error:notconnected' | 'done'> {
522+
async _focus(progress: Progress, resetSelectionIfNotFocused?: boolean): Promise<'error:notconnected' | 'done'> {
523523
progress.throwIfAborted(); // Avoid action that has side-effects.
524-
const result = await this._evaluateInUtility(([injected, node]) => injected.focusNode(node), {});
524+
const result = await this._evaluateInUtility(([injected, node, resetSelectionIfNotFocused]) => injected.focusNode(node, resetSelectionIfNotFocused), resetSelectionIfNotFocused);
525525
return throwFatalDOMError(result);
526526
}
527527

@@ -535,7 +535,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
535535
async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
536536
progress.logger.info(`elementHandle.type("${text}")`);
537537
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
538-
const result = await this._focus(progress);
538+
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
539539
if (result !== 'done')
540540
return result;
541541
progress.throwIfAborted(); // Avoid action that has side-effects.
@@ -554,7 +554,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
554554
async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
555555
progress.logger.info(`elementHandle.press("${key}")`);
556556
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
557-
const result = await this._focus(progress);
557+
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
558558
if (result !== 'done')
559559
return result;
560560
progress.throwIfAborted(); // Avoid action that has side-effects.

src/injected/injectedScript.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,12 +357,22 @@ export default class InjectedScript {
357357
});
358358
}
359359

360-
focusNode(node: Node): FatalDOMError | 'error:notconnected' | 'done' {
360+
focusNode(node: Node, resetSelectionIfNotFocused?: boolean): FatalDOMError | 'error:notconnected' | 'done' {
361361
if (!node.isConnected)
362362
return 'error:notconnected';
363363
if (node.nodeType !== Node.ELEMENT_NODE)
364364
return 'error:notelement';
365+
const wasFocused = (node.getRootNode() as (Document | ShadowRoot)).activeElement === node && node.ownerDocument && node.ownerDocument.hasFocus();
365366
(node as HTMLElement | SVGElement).focus();
367+
368+
if (resetSelectionIfNotFocused && !wasFocused && node.nodeName.toLowerCase() === 'input') {
369+
try {
370+
const input = node as HTMLInputElement;
371+
input.setSelectionRange(0, 0);
372+
} catch (e) {
373+
// Some inputs do not allow selection.
374+
}
375+
}
366376
return 'done';
367377
}
368378

test/elementhandle.jest.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,3 +610,79 @@ describe('ElementHandle.focus', function() {
610610
expect(await button.evaluate(button => document.activeElement === button)).toBe(true);
611611
});
612612
});
613+
614+
describe('ElementHandle.type', function() {
615+
it('should work', async ({page}) => {
616+
await page.setContent(`<input type='text' />`);
617+
await page.type('input', 'hello');
618+
expect(await page.$eval('input', input => input.value)).toBe('hello');
619+
});
620+
it('should not select existing value', async ({page}) => {
621+
await page.setContent(`<input type='text' value='hello' />`);
622+
await page.type('input', 'world');
623+
expect(await page.$eval('input', input => input.value)).toBe('worldhello');
624+
});
625+
it('should reset selection when not focused', async ({page}) => {
626+
await page.setContent(`<input type='text' value='hello' /><div tabIndex=2>text</div>`);
627+
await page.$eval('input', input => {
628+
input.selectionStart = 2;
629+
input.selectionEnd = 4;
630+
document.querySelector('div').focus();
631+
});
632+
await page.type('input', 'world');
633+
expect(await page.$eval('input', input => input.value)).toBe('worldhello');
634+
});
635+
it('should not modify selection when focused', async ({page}) => {
636+
await page.setContent(`<input type='text' value='hello' />`);
637+
await page.$eval('input', input => {
638+
input.focus();
639+
input.selectionStart = 2;
640+
input.selectionEnd = 4;
641+
});
642+
await page.type('input', 'world');
643+
expect(await page.$eval('input', input => input.value)).toBe('heworldo');
644+
});
645+
it('should work with number input', async ({page}) => {
646+
await page.setContent(`<input type='number' value=2 />`);
647+
await page.type('input', '13');
648+
expect(await page.$eval('input', input => input.value)).toBe('132');
649+
});
650+
});
651+
652+
describe('ElementHandle.press', function() {
653+
it('should work', async ({page}) => {
654+
await page.setContent(`<input type='text' />`);
655+
await page.press('input', 'h');
656+
expect(await page.$eval('input', input => input.value)).toBe('h');
657+
});
658+
it('should not select existing value', async ({page}) => {
659+
await page.setContent(`<input type='text' value='hello' />`);
660+
await page.press('input', 'w');
661+
expect(await page.$eval('input', input => input.value)).toBe('whello');
662+
});
663+
it('should reset selection when not focused', async ({page}) => {
664+
await page.setContent(`<input type='text' value='hello' /><div tabIndex=2>text</div>`);
665+
await page.$eval('input', input => {
666+
input.selectionStart = 2;
667+
input.selectionEnd = 4;
668+
document.querySelector('div').focus();
669+
});
670+
await page.press('input', 'w');
671+
expect(await page.$eval('input', input => input.value)).toBe('whello');
672+
});
673+
it('should not modify selection when focused', async ({page}) => {
674+
await page.setContent(`<input type='text' value='hello' />`);
675+
await page.$eval('input', input => {
676+
input.focus();
677+
input.selectionStart = 2;
678+
input.selectionEnd = 4;
679+
});
680+
await page.press('input', 'w');
681+
expect(await page.$eval('input', input => input.value)).toBe('hewo');
682+
});
683+
it('should work with number input', async ({page}) => {
684+
await page.setContent(`<input type='number' value=2 />`);
685+
await page.press('input', '1');
686+
expect(await page.$eval('input', input => input.value)).toBe('12');
687+
});
688+
});

0 commit comments

Comments
 (0)