diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts
index 6aa6ac1e52d98..749ff91d11f2a 100644
--- a/packages/playwright-core/src/server/injected/injectedScript.ts
+++ b/packages/playwright-core/src/server/injected/injectedScript.ts
@@ -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();
diff --git a/tests/page/page-focus.spec.ts b/tests/page/page-focus.spec.ts
index 5fcc7c6ad8ed9..23c49bd8de6aa 100644
--- a/tests/page/page-focus.spec.ts
+++ b/tests/page/page-focus.spec.ts
@@ -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(`
+
focusable
+ not focusable
+
+ `);
+ 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',
+ ]);
+});
diff --git a/tests/page/page-keyboard.spec.ts b/tests/page/page-keyboard.spec.ts
index 32a4134ca3e7c..1a510da2d23bb 100644
--- a/tests/page/page-keyboard.spec.ts
+++ b/tests/page/page-keyboard.spec.ts
@@ -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(`
+
+
+
+
+
+
+ `);
+
+ 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(`
+
+
+
+
+
+
+ `);
+
+ 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(`
+ focusable div
+ non-editable, non-focusable
+ `);
+
+ 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 = {