From 7b267eb7cea05dc6ae31bd96ed5ef0cd8bf7bf4c Mon Sep 17 00:00:00 2001 From: chirokas <157580465+chirokas@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:37:26 +0000 Subject: [PATCH 1/3] fix: TokenField IME composition broken in Firefox --- packages/react-aria/src/tokenfield/useTokenField.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/react-aria/src/tokenfield/useTokenField.ts b/packages/react-aria/src/tokenfield/useTokenField.ts index aef7c40d9f2..c5169d9422b 100644 --- a/packages/react-aria/src/tokenfield/useTokenField.ts +++ b/packages/react-aria/src/tokenfield/useTokenField.ts @@ -198,11 +198,14 @@ export function useTokenField( stopComposition(); } - let selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) { - return; + let range = e.getTargetRanges()[0]; + if (!range) { + let selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + return; + } + range = selection.getRangeAt(0); } - let range = selection.getRangeAt(0); let [start, end] = rangeToPositions(ref.current!, range); // https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes @@ -277,7 +280,7 @@ export function useTokenField( case 'deleteContent': case 'deleteByCut': case 'deleteCompositionText': { - if (!range.collapsed) { + if (!range.collapsed && !isSamePosition(start, end)) { apply(tokens => tokens.replaceRange(start, end, '')); break; } From b479bc9891334bf366d34dda753d0442b6d7138f Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Wed, 12 Aug 2026 17:17:42 -0700 Subject: [PATCH 2/3] fix tests --- .../ai/test/utils/promptFieldTestUtils.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx b/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx index 926d308a47b..37810a7c86f 100644 --- a/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx +++ b/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx @@ -47,8 +47,11 @@ export const TINY_PNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; /** - * Jsdom doesn't implement Range.getBoundingClientRect / getClientRects, which the completion - * popover relies on for positioning. Install stubs so the popover can open. Call in beforeAll. + * Install DOM stubs that jsdom is missing but TokenField/the completion popover rely on. Call + * in beforeAll. + * - Range.getBoundingClientRect / getClientRects: the popover uses these for positioning. + * - InputEvent.getTargetRanges: TokenField reads it on beforeinput to find the edited range. + * Returning [] makes it fall back to the current selection (its pre-existing code path). */ export function installRangePolyfill(): void { let proto = Range.prototype as any; @@ -72,6 +75,9 @@ export function installRangePolyfill(): void { [Symbol.iterator]: function* () {} }); } + if (typeof InputEvent !== 'undefined' && !(InputEvent.prototype as any).getTargetRanges) { + (InputEvent.prototype as any).getTargetRanges = () => []; + } } // Completion data, trimmed from PromptField.stories.tsx. From a76caedba757a718f713be0a365fe7012c74a53e Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 12 Aug 2026 17:31:49 -0700 Subject: [PATCH 3/3] fix lint --- packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx b/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx index 37810a7c86f..fd0b7852fcb 100644 --- a/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx +++ b/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx @@ -51,7 +51,7 @@ export const TINY_PNG = * in beforeAll. * - Range.getBoundingClientRect / getClientRects: the popover uses these for positioning. * - InputEvent.getTargetRanges: TokenField reads it on beforeinput to find the edited range. - * Returning [] makes it fall back to the current selection (its pre-existing code path). + * Returning [] makes it fall back to the current selection (its pre-existing code path). */ export function installRangePolyfill(): void { let proto = Range.prototype as any;