Skip to content

Commit 1e5a265

Browse files
authored
fix(useFocusVisible): use Reflect.defineProperty to wrap HTMLElement.prototype.focus (#10041)
* fix(useFocusVisible): use Object.defineProperty to wrap HTMLElement.prototype.focus * use Reflect.defineProperty instead of Object.defineProperty
1 parent bc141b2 commit 1e5a265

2 files changed

Lines changed: 36 additions & 5 deletions

File tree

packages/react-aria/src/interactions/useFocusVisible.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,18 @@ function setupGlobalFocusEvents(element?: HTMLElement | null) {
167167
// However, we need to detect other cases when a focus event occurs without
168168
// a preceding user event (e.g. screen reader focus). Overriding the focus
169169
// method on HTMLElement.prototype is a bit hacky, but works.
170+
// defineProperty (not assignment) so this works even if `focus` is currently
171+
// a getter-only accessor — e.g. when @testing-library/user-event's setup()
172+
// has instrumented it. Plain assignment throws in that case.
170173
let focus = windowObject.HTMLElement.prototype.focus;
171-
windowObject.HTMLElement.prototype.focus = function () {
172-
hasEventBeforeFocus = true;
173-
focus.apply(this, arguments as unknown as [options?: FocusOptions | undefined]);
174-
};
174+
Reflect.defineProperty(windowObject.HTMLElement.prototype, 'focus', {
175+
configurable: true,
176+
writable: true,
177+
value: function () {
178+
hasEventBeforeFocus = true;
179+
focus.apply(this, arguments as unknown as [options?: FocusOptions | undefined]);
180+
}
181+
});
175182

176183
documentObject.addEventListener('keydown', handleKeyboardEvent, true);
177184
documentObject.addEventListener('keyup', handleKeyboardEvent, true);
@@ -213,7 +220,11 @@ const tearDownWindowFocusTracking = (element, loadListener?: () => void) => {
213220
if (!hasSetupGlobalListeners.has(windowObject)) {
214221
return;
215222
}
216-
windowObject.HTMLElement.prototype.focus = hasSetupGlobalListeners.get(windowObject)!.focus;
223+
Reflect.defineProperty(windowObject.HTMLElement.prototype, 'focus', {
224+
configurable: true,
225+
writable: true,
226+
value: hasSetupGlobalListeners.get(windowObject)!.focus
227+
});
217228

218229
documentObject.removeEventListener('keydown', handleKeyboardEvent, true);
219230
documentObject.removeEventListener('keyup', handleKeyboardEvent, true);

packages/react-aria/test/interactions/useFocusVisible.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,26 @@ describe('useFocusVisible', function () {
148148
iframe.remove();
149149
});
150150

151+
// Regression test for https://github.com/adobe/react-spectrum/issues/9649
152+
it('does not throw when HTMLElement.prototype.focus is an accessor-only property', function () {
153+
const HTMLElementProto = iframe.contentWindow.HTMLElement.prototype;
154+
const original = Object.getOwnPropertyDescriptor(HTMLElementProto, 'focus');
155+
Object.defineProperty(HTMLElementProto, 'focus', {
156+
configurable: true,
157+
get() {
158+
return original?.value;
159+
}
160+
});
161+
162+
try {
163+
expect(() => addWindowFocusTracking(iframeRoot)).not.toThrow();
164+
} finally {
165+
if (original) {
166+
Object.defineProperty(HTMLElementProto, 'focus', original);
167+
}
168+
}
169+
});
170+
151171
it('sets up focus listener in a different window', async function () {
152172
render(<Example id="iframe-example" />, {container: iframeRoot});
153173
await waitFor(() => {

0 commit comments

Comments
 (0)