mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
fix: page.locator.focus() and page.locator(…).type(…) (#14267)
Fixes focus and blur management when `page.locator(…).focus()` and `page.locator(…).type(…)` are used which was regressed by7a5b070
(#13510). #13510 relied on an implicit assumption that this (conditional) [`blur`](7a5b070e95/packages/playwright-core/src/server/injected/injectedScript.ts (L672)
) call would always be followed by a call that resulted in a newly focused element via this [`focus`](7a5b070e95/packages/playwright-core/src/server/injected/injectedScript.ts (L674)
) call. 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 to7a5b070e95
(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.
This commit is contained in:
parent
9a73dfe773
commit
fbb364c1cd
@ -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();
|
||||
|
@ -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',
|
||||
]);
|
||||
});
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user