diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 43d0e94ff00..cbe98cb187f 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -562,4 +562,32 @@ describe('DateField', () => { expect(segements[1]).toHaveTextContent('dd'); expect(segements[2]).toHaveTextContent('yyyy'); }); + + // Regression test for #10259: in Firefox a stale selection anchor can remain inside a segment + // after focus moves away, and the selectionchange handler would collapse onto it, stealing focus. + it('does not collapse the selection onto a segment while another element is focused', () => { + let {getByRole} = render( + <> + + + + {segment => } + + + ); + + let button = getByRole('button'); + let segment = within(getByRole('group')).getAllByRole('spinbutton').at(-1); + act(() => button.focus()); + expect(document.activeElement).toBe(button); + + let collapse = jest.fn(); + jest.spyOn(window, 'getSelection').mockReturnValue({anchorNode: segment.firstChild, collapse}); + act(() => { + document.dispatchEvent(new Event('selectionchange')); + }); + + expect(collapse).not.toHaveBeenCalled(); + jest.restoreAllMocks(); + }); }); diff --git a/packages/react-aria/src/datepicker/useDateSegment.ts b/packages/react-aria/src/datepicker/useDateSegment.ts index 9119e2b7d76..963fc4659ff 100644 --- a/packages/react-aria/src/datepicker/useDateSegment.ts +++ b/packages/react-aria/src/datepicker/useDateSegment.ts @@ -264,7 +264,13 @@ export function useDateSegment( // Otherwise, when tapping on a segment in Android Chrome and then entering text, // composition events will be fired that break the DOM structure and crash the page. let selection = window.getSelection(); - if (selection?.anchorNode && nodeContains(ref.current, selection?.anchorNode)) { + // Only collapse while focused, otherwise a stale anchor left in the segment (e.g. on Firefox) + // steals focus back into it on selectionchange. See #10259. + if ( + selection?.anchorNode && + nodeContains(ref.current, selection?.anchorNode) && + getActiveElement() === ref.current + ) { selection.collapse(ref.current); } });