Skip to content

Commit

Permalink
chery-pick(#14267): fix: page.locator.focus() and page.locator(…).typ…
Browse files Browse the repository at this point in the history
…e(…) (#14296)

cherry-pick of fbb364c:

Fixes focus and blur management when `page.locator(…).focus()`  and  `page.locator(…).type(…)` are used which was regressed by 7a5b070 (#13510).

However, some elements are [not focusable](https://html.spec.whatwg.org/multipage/interaction.html#focusable-area), so we were blurring incorrectly, and losing focus that we should have maintained.

Two regression tests were added that pass on the commit prior to 7a5b070 (and match manual testing/expectations):

* `page.locator(…).focus()`: _keeps focus on element when attempting to focus a non-focusable element_
* `page.locator(…).type(…)`: _should type repeatedly in input in shadow dom_

Additionally, a third test (_should type repeatedly in input in shadow dom_) was added to check the invariant from #13510 that states:

> This affects [contenteditable] elements, but not input elements.

and allows us to introduce the targeted fix (contenteditble check before blur) without breaking FF again.

And _should type repeatedly in contenteditable in shadow dom with nested elements_ was added to ensure the above fix works with nest contenteditble detection.

Fixes #14254.
  • Loading branch information
rwoll authored May 19, 2022
1 parent 14782d7 commit abd4258
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ export class InjectedScript {

const activeElement = (node.getRootNode() as (Document | ShadowRoot)).activeElement;
const wasFocused = activeElement === node && node.ownerDocument && node.ownerDocument.hasFocus();
if (!wasFocused && activeElement && (activeElement as HTMLElement | SVGElement).blur) {
if ((node as HTMLElement).isContentEditable && !wasFocused && activeElement && (activeElement as HTMLElement | SVGElement).blur) {
// Workaround the Firefox bug where focusing the element does not switch current
// contenteditable to the new element. However, blurring the previous one helps.
(activeElement as HTMLElement | SVGElement).blur();
Expand Down
28 changes: 28 additions & 0 deletions tests/page/page-focus.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,31 @@ it('clicking checkbox should activate it', async ({ page, browserName, headless,
const nodeName = await page.evaluate(() => document.activeElement.nodeName);
expect(nodeName).toBe('INPUT');
});

it('keeps focus on element when attempting to focus a non-focusable element', async ({ page }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/14254' });

await page.setContent(`
<div id="focusable" tabindex="0">focusable</div>
<div id="non-focusable">not focusable</div>
<script>
window.eventLog = [];
const focusable = document.getElementById("focusable");
focusable.addEventListener('blur', () => window.eventLog.push('blur focusable'));
focusable.addEventListener('focus', () => window.eventLog.push('focus focusable'));
const nonFocusable = document.getElementById("non-focusable");
nonFocusable.addEventListener('blur', () => window.eventLog.push('blur non-focusable'));
nonFocusable.addEventListener('focus', () => window.eventLog.push('focus non-focusable'));
</script>
`);
await page.locator('#focusable').click();
expect.soft(await page.evaluate(() => document.activeElement?.id)).toBe('focusable');
await page.locator('#non-focusable').focus();
expect.soft(await page.evaluate(() => document.activeElement?.id)).toBe('focusable');
expect.soft(await page.evaluate(() => window['eventLog'])).toEqual([
'focus focusable',
]);
});
92 changes: 92 additions & 0 deletions tests/page/page-keyboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,98 @@ it('should type repeatedly in contenteditable in shadow dom', async ({ page }) =
expect(await sectionEditor.textContent()).toBe('This is the second box.');
});

it('should type repeatedly in contenteditable in shadow dom with nested elements', async ({ page }) => {
await page.setContent(`
<html>
<body>
<shadow-element></shadow-element>
<script>
customElements.define('shadow-element', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = \`
<style>
.editor { padding: 1rem; margin: 1rem; border: 1px solid #ccc; }
</style>
<div class=editor contenteditable id=foo><p>hello</p></div>
<hr>
<section>
<div class=editor contenteditable id=bar><p>world</p></div>
</section>
\`;
}
});
</script>
</body>
</html>
`);

const editor = page.locator('shadow-element > .editor').first();
await editor.type('This is the first box: ');

const sectionEditor = page.locator('section .editor');
await sectionEditor.type('This is the second box: ');

expect(await editor.textContent()).toBe('This is the first box: hello');
expect(await sectionEditor.textContent()).toBe('This is the second box: world');
});

it('should type repeatedly in input in shadow dom', async ({ page }) => {
await page.setContent(`
<html>
<body>
<shadow-element></shadow-element>
<script>
customElements.define('shadow-element', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = \`
<style>
.editor { padding: 1rem; margin: 1rem; border: 1px solid #ccc; }
</style>
<input class=editor id=foo>
<hr>
<section>
<input class=editor id=bar>
</section>
\`;
}
});
</script>
</body>
</html>
`);

const editor = page.locator('shadow-element > .editor').first();
await editor.type('This is the first box.');

const sectionEditor = page.locator('section .editor');
await sectionEditor.type('This is the second box.');

expect(await editor.inputValue()).toBe('This is the first box.');
expect(await sectionEditor.inputValue()).toBe('This is the second box.');
});

it('type to non-focusable element should maintain old focus', async ({ page }) => {
await page.setContent(`
<div id="focusable" tabindex="0">focusable div</div>
<div id="non-focusable-and-non-editable">non-editable, non-focusable</div>
`);

await page.locator('#focusable').focus();
expect(await page.evaluate(() => document.activeElement?.id)).toBe('focusable');
await page.locator('#non-focusable-and-non-editable').type('foo');
expect(await page.evaluate(() => document.activeElement?.id)).toBe('focusable');
});

async function captureLastKeydown(page) {
const lastEvent = await page.evaluateHandle(() => {
const lastEvent = {
Expand Down

0 comments on commit abd4258

Please sign in to comment.