fix(chrome-ext): return focus to source element when SuggestionBox is closed (#2282)

* fix(chrome-ext): return focus to source element when `SuggestionBox` is closed

* test(chrome-ext): make sure focus ends up in the right places

* fix(chrome-ext): ignoring a suggestion should result in the right focus as well
This commit is contained in:
Elijah Potter 2025-12-02 08:38:55 -07:00 committed by GitHub
parent 611475176d
commit edff925df0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 39 additions and 4 deletions

View file

@ -1,6 +1,7 @@
import { test } from './fixtures';
import {
assertHarperHighlightBoxes,
assertLocatorIsFocused,
clickHarperHighlight,
getTextarea,
replaceEditorContent,
@ -73,4 +74,6 @@ test('Can dismiss with escape key', async ({ page }) => {
await page.keyboard.press('Escape');
await page.locator('.harper-container').waitFor({ state: 'hidden' });
await assertLocatorIsFocused(page, editor);
});

View file

@ -38,6 +38,20 @@ export function getHarperHighlights(page: Page): Locator {
return page.locator('#harper-highlight');
}
export async function assertLocatorIsFocused(page: Page, loc: Locator) {
await assertLocatorsResolveEqually(page, loc, page.locator(':focus'));
}
/** Checks that the two provided locators resolve to the same element. */
export async function assertLocatorsResolveEqually(page: Page, a: Locator, b: Locator) {
const areSame = await page.evaluate(
([a, b]) => a === b,
[await a.elementHandle(), await b.elementHandle()],
);
expect(areSame).toBe(true);
}
/** Locates the first Harper highlight on the page and clicks it.
* It should result in the popup opening.
* Returns whether the highlight was found. */
@ -97,6 +111,7 @@ export async function testBasicSuggestionTextarea(testPageUrl: TestPageUrlProvid
await page.waitForTimeout(3000);
expect(editor).toHaveValue('This is a test');
await assertLocatorIsFocused(page, editor);
});
}
@ -126,6 +141,7 @@ export async function testCanIgnoreTextareaSuggestion(testPageUrl: TestPageUrlPr
// Nothing should change.
expect(editor).toHaveValue(cacheSalt);
expect(await clickHarperHighlight(page)).toBe(false);
await assertLocatorIsFocused(page, editor);
});
}
@ -146,6 +162,7 @@ export async function testCanBlockRuleTextareaSuggestion(testPageUrl: TestPageUr
await page.waitForTimeout(1000);
await assertHarperHighlightBoxes(page, []);
await assertLocatorIsFocused(page, editor);
});
}

View file

@ -16,6 +16,8 @@ function iconSvg(definition: IconDefinition): string {
const settingsIconSvg = iconSvg(faGear);
const disableIconSvg = iconSvg(faBan);
let previouslyActiveElement: null | HTMLElement = null;
var FocusHook: any = function () {};
FocusHook.prototype.hook = function (node: any, _propertyName: any, _previousValue: any) {
if ((node as any).__harperAutofocused) {
@ -23,6 +25,10 @@ FocusHook.prototype.hook = function (node: any, _propertyName: any, _previousVal
}
requestAnimationFrame(() => {
if (document.activeElement?.tagName.toLowerCase() != 'harper-render-box') {
previouslyActiveElement = document.activeElement as HTMLElement;
}
node.focus();
Object.defineProperty(node, '__harperAutofocused', {
value: true,
@ -453,19 +459,26 @@ export default function SuggestionBox(
transformOrigin: `${bottom ? 'bottom' : 'top'} left`,
};
const ignoreLintCallback = box.ignoreLint;
const refocusClose = () => {
previouslyActiveElement?.focus();
close();
};
return h(
'div',
{
className: 'harper-container fade-in',
style: positionStyle,
'harper-close-on-escape': new CloseOnEscapeHook(close),
'harper-close-on-escape': new CloseOnEscapeHook(refocusClose),
},
[
styleTag(box.lint.lint_kind),
header(
box.lint.lint_kind_pretty,
lintKindColor(box.lint.lint_kind),
close,
refocusClose,
actions.openOptions,
box.rule,
actions.setRuleEnabled,
@ -474,13 +487,15 @@ export default function SuggestionBox(
footer(
suggestions(box.lint.lint_kind, box.lint.suggestions, (v) => {
box.applySuggestion(v);
close();
refocusClose();
}),
[
box.lint.lint_kind === 'Spelling' && actions.addToUserDictionary
? addToDictionary(box, actions.addToUserDictionary)
: undefined,
box.ignoreLint ? ignoreLint(box.ignoreLint) : undefined,
ignoreLintCallback
? ignoreLint(() => ignoreLintCallback().then(refocusClose))
: undefined,
],
),
hintDrawer(hint),