From 21ef4871bd0638800a4138028f4e9023cc12febc Mon Sep 17 00:00:00 2001 From: Jean-Franck Date: Mon, 16 Mar 2026 19:44:28 +0100 Subject: [PATCH 01/10] feat(rangeCalendar): allow to customise the behavior when the pointer is released outside of the component (#8899) * feat(rangeCalendar): allow to customise the behavior when the pointer is released outside of the component * feat: add test about pointerUpOutsideAction * Add assertions to tests * fix test * add more tests * change name of prop --------- Co-authored-by: Robert Snow Co-authored-by: Robert Snow --- .../calendar/src/useRangeCalendar.ts | 19 +- .../calendar/test/RangeCalendar.test.js | 212 +++++++++++++++++- packages/@react-stately/calendar/src/types.ts | 4 +- .../calendar/src/useRangeCalendarState.ts | 6 +- packages/@react-types/calendar/src/index.d.ts | 18 +- 5 files changed, 247 insertions(+), 12 deletions(-) diff --git a/packages/@react-aria/calendar/src/useRangeCalendar.ts b/packages/@react-aria/calendar/src/useRangeCalendar.ts index 78bff4f50af..2f09ab5bb2f 100644 --- a/packages/@react-aria/calendar/src/useRangeCalendar.ts +++ b/packages/@react-aria/calendar/src/useRangeCalendar.ts @@ -22,7 +22,8 @@ import {useRef} from 'react'; * A range calendar displays one or more date grids and allows users to select a contiguous range of dates. */ export function useRangeCalendar(props: AriaRangeCalendarProps, state: RangeCalendarState, ref: RefObject): CalendarAria { - let res = useCalendarBase(props, state); + let {interactOutsideBehavior = 'select', ...otherProps} = props; + let res = useCalendarBase(otherProps, state); // We need to ignore virtual pointer events from VoiceOver due to these bugs. // https://bugs.webkit.org/show_bug.cgi?id=222627 @@ -36,7 +37,13 @@ export function useRangeCalendar(props: AriaRangeCalendarPr isVirtualClick.current = e.width === 0 && e.height === 0; }); - // Stop range selection when pressing or releasing a pointer outside the calendar body, + const interactOutsideBehaviorMapping = { + clear: () => state.clearSelection(), + reset: () => state.setAnchorDate(null), + select: () => state.selectFocusedDate() + }; + + // Execute method corresponding to `interactOutsideBehavior` when pressing or releasing a pointer outside the calendar body, // except when pressing the next or previous buttons to switch months. let endDragging = (e: PointerEvent) => { if (isVirtualClick.current) { @@ -55,19 +62,21 @@ export function useRangeCalendar(props: AriaRangeCalendarPr isFocusWithin(ref.current) && (!nodeContains(ref.current, target) || !target.closest('button, [role="button"]')) ) { - state.selectFocusedDate(); + interactOutsideBehaviorMapping[interactOutsideBehavior](); } }; useEvent(windowRef, 'pointerup', endDragging); - // Also stop range selection on blur, e.g. tabbing away from the calendar. + // Also execute method corresponding to `interactOutsideBehavior` on blur, + // e.g. tabbing away from the calendar. res.calendarProps.onBlur = e => { if (!ref.current) { return; } + if ((!e.relatedTarget || !nodeContains(ref.current, e.relatedTarget)) && state.anchorDate) { - state.selectFocusedDate(); + interactOutsideBehaviorMapping[interactOutsideBehavior](); } }; diff --git a/packages/@react-spectrum/calendar/test/RangeCalendar.test.js b/packages/@react-spectrum/calendar/test/RangeCalendar.test.js index befe4384ef0..62025906c96 100644 --- a/packages/@react-spectrum/calendar/test/RangeCalendar.test.js +++ b/packages/@react-spectrum/calendar/test/RangeCalendar.test.js @@ -209,10 +209,10 @@ describe('RangeCalendar', () => { const {getAllByRole} = render( ); - + let grids = getAllByRole('grid'); expect(grids).toHaveLength(3); - + expect(grids[0]).toHaveAttribute('aria-label', expected[0]); expect(grids[1]).toHaveAttribute('aria-label', expected[1]); expect(grids[2]).toHaveAttribute('aria-label', expected[2]); @@ -1543,4 +1543,212 @@ describe('RangeCalendar', () => { expect(announce).toHaveBeenCalledWith('March 5 BC'); }); }); + + describe('pointer events', () => { + beforeAll(() => { + jest.setSystemTime(new Date('2025-11-01')); + }); + + it('should select the last hovered date when interactOutsideBehavior is "select"', async () => { + const onChange = jest.fn(); + + let {getByText, getAllByText} = render( + + ); + + await user.click(getByText('25')); + await user.hover(getByText('20')); + + await user.click(getAllByText('November 2025')[0]); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2025, 11, 20), end: new CalendarDate(2025, 11, 25)}); + }); + + it('should clear the selection when interactOutsideBehavior is "clear"', async () => { + const onChange = jest.fn(); + let {getByText, getAllByText, getByRole} = render( + + ); + + let startCell = getByRole('gridcell', {name: '25'}); + let endCell = getByRole('gridcell', {name: '20'}); + + await user.click(getByText('25')); + await user.hover(getByText('20')); + expect(startCell).toHaveAttribute('aria-selected', 'true'); + + await user.click(getAllByText('November 2025')[0]); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(null); + expect(startCell).not.toHaveAttribute('aria-selected'); + expect(endCell).not.toHaveAttribute('aria-selected'); + }); + + it('should clear the selection when interactOutsideBehavior is "clear" no default selected range', async () => { + const onChange = jest.fn(); + let {getByText, getByRole} = render( + + ); + + let startCell = getByRole('gridcell', {name: '25'}); + let endCell = getByRole('gridcell', {name: '20'}); + + await user.click(getByText('25')); + await user.hover(getByText('20')); + expect(startCell).toHaveAttribute('aria-selected', 'true'); + + await user.click(document.body); + + expect(onChange).toHaveBeenCalledTimes(0); + expect(startCell).not.toHaveAttribute('aria-selected'); + expect(endCell).not.toHaveAttribute('aria-selected'); + }); + + it('should reset to the initial range when interactOutsideBehavior is "reset"', async () => { + const onChange = jest.fn(); + let {getByText, getAllByText, getByRole} = render( + + ); + + let originalStartCell = getByRole('gridcell', {name: '13'}); + let originalEndCell = getByRole('gridcell', {name: '15'}); + let newStartCell = getByRole('gridcell', {name: '25'}); + + expect(originalStartCell).toHaveAttribute('aria-selected', 'true'); + expect(originalEndCell).toHaveAttribute('aria-selected', 'true'); + expect(newStartCell).not.toHaveAttribute('aria-selected'); + + await user.click(getByText('25')); + await user.hover(getByText('20')); + expect(newStartCell).toHaveAttribute('aria-selected', 'true'); + expect(originalStartCell).not.toHaveAttribute('aria-selected'); + expect(originalEndCell).not.toHaveAttribute('aria-selected'); + + await user.click(getAllByText('November 2025')[0]); + + expect(onChange).toHaveBeenCalledTimes(0); + expect(newStartCell).not.toHaveAttribute('aria-selected'); + expect(originalStartCell).toHaveAttribute('aria-selected', 'true'); + expect(originalEndCell).toHaveAttribute('aria-selected', 'true'); + }); + + it('should reset to the initial range when interactOutsideBehavior is "reset" (controlled value)', async () => { + const onChange = jest.fn(); + let {getByText, getAllByText, getByRole} = render( + + ); + + let originalStartCell = getByRole('gridcell', {name: '13'}); + let originalEndCell = getByRole('gridcell', {name: '15'}); + let newStartCell = getByRole('gridcell', {name: '25'}); + + await user.click(getByText('25')); + await user.hover(getByText('20')); + expect(newStartCell).toHaveAttribute('aria-selected', 'true'); + expect(originalStartCell).not.toHaveAttribute('aria-selected'); + expect(originalEndCell).not.toHaveAttribute('aria-selected'); + + await user.click(getAllByText('November 2025')[0]); + + expect(onChange).toHaveBeenCalledTimes(0); + expect(newStartCell).not.toHaveAttribute('aria-selected'); + expect(originalStartCell).toHaveAttribute('aria-selected', 'true'); + expect(originalEndCell).toHaveAttribute('aria-selected', 'true'); + }); + + describe('blur (e.g. tabbing away)', () => { + it('should select the hovered range when interactOutsideBehavior is "select" and calendar blurs', async () => { + const onChange = jest.fn(); + let {getByText, getByRole} = render( + + ); + + let startCell = getByRole('gridcell', {name: '13'}); + let endCell = getByRole('gridcell', {name: '15'}); + + let newStartCell = getByRole('gridcell', {name: '25'}); + let newEndCell = getByRole('gridcell', {name: '20'}); + + await user.click(getByText('25')); + await user.hover(getByText('20')); + + await user.tab(); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith({start: new CalendarDate(2025, 11, 20), end: new CalendarDate(2025, 11, 25)}); + expect(startCell).not.toHaveAttribute('aria-selected', 'true'); + expect(endCell).not.toHaveAttribute('aria-selected', 'true'); + expect(newStartCell).toHaveAttribute('aria-selected', 'true'); + expect(newEndCell).toHaveAttribute('aria-selected', 'true'); + }); + + it('should clear the selection when interactOutsideBehavior is "clear" and calendar blurs', async () => { + const onChange = jest.fn(); + let {getByText, getByRole} = render( + + ); + + let startCell = getByRole('gridcell', {name: '13'}); + let endCell = getByRole('gridcell', {name: '15'}); + let newStartCell = getByRole('gridcell', {name: '25'}); + let newEndCell = getByRole('gridcell', {name: '20'}); + + await user.click(getByText('25')); + await user.hover(getByText('20')); + await user.tab(); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(null); + expect(startCell).not.toHaveAttribute('aria-selected', 'true'); + expect(endCell).not.toHaveAttribute('aria-selected', 'true'); + expect(newStartCell).not.toHaveAttribute('aria-selected', 'true'); + expect(newEndCell).not.toHaveAttribute('aria-selected', 'true'); + }); + + it('should reset to the initial range when interactOutsideBehavior is "reset" and calendar blurs', async () => { + const onChange = jest.fn(); + let {getByText, getByRole} = render( + + ); + + let originalStartCell = getByRole('gridcell', {name: '13'}); + let originalEndCell = getByRole('gridcell', {name: '15'}); + let newStartCell = getByRole('gridcell', {name: '25'}); + + await user.click(getByText('25')); + await user.hover(getByText('20')); + await user.tab(); + + expect(onChange).toHaveBeenCalledTimes(0); + expect(originalStartCell).toHaveAttribute('aria-selected', 'true'); + expect(originalEndCell).toHaveAttribute('aria-selected', 'true'); + expect(newStartCell).not.toHaveAttribute('aria-selected'); + }); + }); + }); }); diff --git a/packages/@react-stately/calendar/src/types.ts b/packages/@react-stately/calendar/src/types.ts index ec7b26470c6..839c2528882 100644 --- a/packages/@react-stately/calendar/src/types.ts +++ b/packages/@react-stately/calendar/src/types.ts @@ -122,5 +122,7 @@ export interface RangeCalendarState extends Cal /** Whether the user is currently dragging over the calendar. */ readonly isDragging: boolean, /** Sets whether the user is dragging over the calendar. */ - setDragging(isDragging: boolean): void + setDragging(isDragging: boolean): void, + /** Clears the current selection. */ + clearSelection(): void } diff --git a/packages/@react-stately/calendar/src/useRangeCalendarState.ts b/packages/@react-stately/calendar/src/useRangeCalendarState.ts index 284500aa184..305d90df227 100644 --- a/packages/@react-stately/calendar/src/useRangeCalendarState.ts +++ b/packages/@react-stately/calendar/src/useRangeCalendarState.ts @@ -191,7 +191,11 @@ export function useRangeCalendarState(props: Ra return calendar.isInvalid(date) || isInvalid(date, availableRangeRef.current?.start, availableRangeRef.current?.end); }, isDragging, - setDragging + setDragging, + clearSelection() { + setAnchorDate(null); + setValue(null); + } }; } diff --git a/packages/@react-types/calendar/src/index.d.ts b/packages/@react-types/calendar/src/index.d.ts index 8d9e1e2381d..0fda150a6f9 100644 --- a/packages/@react-types/calendar/src/index.d.ts +++ b/packages/@react-types/calendar/src/index.d.ts @@ -67,8 +67,8 @@ export interface CalendarPropsBase { * The day that starts the week. */ firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat', - /** - * Determines the alignment of the visible months on initial render based on the current selection or current date if there is no selection. + /** + * Determines the alignment of the visible months on initial render based on the current selection or current date if there is no selection. * @default 'center' */ selectionAlignment?: 'start' | 'center' | 'end' @@ -86,7 +86,19 @@ export interface RangeCalendarProps extends CalendarPropsBa export interface AriaCalendarProps extends CalendarProps, DOMProps, AriaLabelingProps {} -export interface AriaRangeCalendarProps extends RangeCalendarProps, DOMProps, AriaLabelingProps {} +export interface AriaRangeCalendarProps extends RangeCalendarProps, DOMProps, AriaLabelingProps { + /** + * Controls the behavior when a pointer is released outside the calendar: + * + * - `clear`: clear the currently selected range of dates. + * + * - `reset`: reset the selection to the previously selected range of dates. + * + * - `select`: select the currently hovered range of dates. + * @default 'select' + */ + interactOutsideBehavior?: 'clear' | 'reset' | 'select' +} export type PageBehavior = 'single' | 'visible'; From f439194e4de5b70a8d813e146b9a8c3520ada0a2 Mon Sep 17 00:00:00 2001 From: Arne-Christian Rundereim Date: Mon, 16 Mar 2026 18:44:42 +0000 Subject: [PATCH 02/10] Select - add `shouldCloseOnSelect` (#8733) * Select - add `shouldCloseOnSelect` This enables support for workflows where the select should stay open even after the user has selected an option. Fixes https://github.com/adobe/react-spectrum/issues/8729 * fix test * simplify logic and add assertion that fails for the opposite case * forgot to save --------- Co-authored-by: Robert Snow --- packages/@react-aria/test-utils/src/select.ts | 12 +++- .../select/src/useSelectState.ts | 9 ++- packages/@react-types/select/src/index.d.ts | 2 + .../react-aria-components/test/Select.test.js | 56 +++++++++++++++++++ 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/packages/@react-aria/test-utils/src/select.ts b/packages/@react-aria/test-utils/src/select.ts index f4b3b8ac432..66da8c85262 100644 --- a/packages/@react-aria/test-utils/src/select.ts +++ b/packages/@react-aria/test-utils/src/select.ts @@ -25,7 +25,12 @@ interface SelectTriggerOptionOpts extends SelectOpenOpts { /** * The index, text, or node of the option to select. Option nodes can be sourced via `options()`. */ - option: number | string | HTMLElement + option: number | string | HTMLElement, + /** + * Whether or not the select closes on selection. Depends on select implementation and configuration. + * @default true + */ + closesOnSelect?: boolean } export class SelectTester { @@ -165,6 +170,7 @@ export class SelectTester { async selectOption(opts: SelectTriggerOptionOpts): Promise { let { option, + closesOnSelect, interactionType = this._interactionType } = opts || {}; let trigger = this.trigger; @@ -186,6 +192,8 @@ export class SelectTester { } let isMultiSelect = listbox.getAttribute('aria-multiselectable') === 'true'; + let isSingleSelect = !isMultiSelect; + closesOnSelect = closesOnSelect ?? isSingleSelect; if (interactionType === 'keyboard') { if (option?.getAttribute('aria-disabled') === 'true') { @@ -206,7 +214,7 @@ export class SelectTester { } } - if (!isMultiSelect && option?.getAttribute('href') == null) { + if (closesOnSelect && option?.getAttribute('href') == null) { await waitFor(() => { if (document.activeElement !== this._trigger) { throw new Error(`Expected the document.activeElement after selecting an option to be the select component trigger but got ${document.activeElement}`); diff --git a/packages/@react-stately/select/src/useSelectState.ts b/packages/@react-stately/select/src/useSelectState.ts index 2bcf82787a9..058d71f23a3 100644 --- a/packages/@react-stately/select/src/useSelectState.ts +++ b/packages/@react-stately/select/src/useSelectState.ts @@ -79,7 +79,10 @@ export interface SelectState extends List * multiple selection state. */ export function useSelectState(props: SelectStateOptions): SelectState { - let {selectionMode = 'single' as M} = props; + let { + selectionMode = 'single' as M, + shouldCloseOnSelect = selectionMode === 'single' + } = props; let triggerState = useOverlayTriggerState(props); let [focusStrategy, setFocusStrategy] = useState(null); let defaultValue = useMemo(() => { @@ -125,10 +128,12 @@ export function useSelectState extends Coll defaultOpen?: boolean, /** Method that is called when the open state of the menu changes. */ onOpenChange?: (isOpen: boolean) => void, + /** Whether the Select should close when an item is selected. Defaults to true if selectionMode is single, false otherwise. */ + shouldCloseOnSelect?: boolean, /** Whether the select should be allowed to be open when the collection is empty. */ allowsEmptyCollection?: boolean } diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index 78dea4fccc1..c5e44c1133b 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -222,6 +222,62 @@ describe('Select', () => { expect(trigger).toHaveTextContent('close'); }); + it('should stay open on selecting an option if shouldCloseOnSelect is false and single selection mode', async () => { + let {getByTestId} = render( + + ); + + let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')}); + let trigger = selectTester.trigger; + + await selectTester.open(); + expect(trigger).toHaveAttribute('data-pressed', 'true'); + + await selectTester.selectOption({option: 'Dog', closesOnSelect: false}); + expect(trigger).toHaveTextContent('Dog'); + expect(trigger).toHaveAttribute('data-pressed', 'true'); + }); + + it('should close on selecting an option if shouldCloseOnSelect is true and multiple selection mode', async () => { + let {getByTestId} = render( + + ); + + let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')}); + let trigger = selectTester.trigger; + + await selectTester.open(); + expect(trigger).toHaveAttribute('data-pressed', 'true'); + + await selectTester.selectOption({option: 'Dog', closesOnSelect: true}); + expect(trigger).toHaveTextContent('Dog'); + expect(trigger).not.toHaveAttribute('data-pressed', 'true'); + }); + it('should send disabled prop to the hidden field', () => { render( From 1ce0a5413951480f634bd9a1171978a1eed87fa5 Mon Sep 17 00:00:00 2001 From: Christoph Meise Date: Mon, 16 Mar 2026 20:49:42 +0100 Subject: [PATCH 03/10] Fix animations promise settle (#9772) Co-authored-by: Robert Snow --- packages/@react-aria/utils/src/animation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/utils/src/animation.ts b/packages/@react-aria/utils/src/animation.ts index c613b451163..39b1a89daa3 100644 --- a/packages/@react-aria/utils/src/animation.ts +++ b/packages/@react-aria/utils/src/animation.ts @@ -87,13 +87,13 @@ function useAnimation(ref: RefObject, isActive: boolean, onE } let canceled = false; - Promise.all(animations.map(a => a.finished)).then(() => { + Promise.allSettled(animations.map(a => a.finished)).then(() => { if (!canceled) { flushSync(() => { onEnd(); }); } - }).catch(() => {}); + }); return () => { canceled = true; From 4402632c26d6a2de798c222fc66f5aeea573076b Mon Sep 17 00:00:00 2001 From: Fellipe Utaka Date: Mon, 16 Mar 2026 16:54:49 -0300 Subject: [PATCH 04/10] feat(FieldError): expose elementType prop to control host element (#9759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(FieldError): expose elementType prop to control host element Fixes #9757 — Option C: expose elementType on FieldErrorProps so users can set elementType="div" when wrapping block-level children (e.g.
    ) without triggering the DOMElement render mismatch warning. * test(FieldError): replace html-validation test with warn suppression tests --- .../react-aria-components/src/FieldError.tsx | 15 +++-- .../test/FieldError.test.js | 60 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/react-aria-components/src/FieldError.tsx b/packages/react-aria-components/src/FieldError.tsx index ca8331db63b..69af239de38 100644 --- a/packages/react-aria-components/src/FieldError.tsx +++ b/packages/react-aria-components/src/FieldError.tsx @@ -24,7 +24,13 @@ export interface FieldErrorProps extends RenderProps, DOM * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. * @default 'react-aria-FieldError' */ - className?: ClassNameOrFunction + className?: ClassNameOrFunction, + /** + * The HTML element type to render. Defaults to `'span'`. + * Set to `'div'` when using block-level children (e.g. `
      `) to avoid invalid HTML. + * @default 'span' + */ + elementType?: string } /** @@ -41,9 +47,10 @@ export const FieldError = forwardRef(function FieldError(props: FieldErrorProps, const FieldErrorInner = forwardRef((props: FieldErrorProps, ref: ForwardedRef) => { let validation = useContext(FieldErrorContext)!; - let domProps = filterDOMProps(props, {global: true})!; + let {elementType, ...restProps} = props; + let domProps = filterDOMProps(restProps, {global: true})!; let renderProps = useRenderProps({ - ...props, + ...restProps, defaultClassName: 'react-aria-FieldError', defaultChildren: validation.validationErrors.length === 0 ? undefined : validation.validationErrors.join(' '), values: validation @@ -53,5 +60,5 @@ const FieldErrorInner = forwardRef((props: FieldErrorProps, ref: ForwardedRef; + return ; }); diff --git a/packages/react-aria-components/test/FieldError.test.js b/packages/react-aria-components/test/FieldError.test.js index 455348fdb92..05638875463 100644 --- a/packages/react-aria-components/test/FieldError.test.js +++ b/packages/react-aria-components/test/FieldError.test.js @@ -20,4 +20,64 @@ describe('FieldError', function () { const element = getByTestId(TEST_ID); expect(element).toHaveTextContent('An error'); }); + + it('renders as span by default', async () => { + const {getByText} = render( + + + + An error + + ); + + const element = getByText('An error'); + expect(element.tagName).toBe('SPAN'); + }); + + it('supports elementType prop', async () => { + const {getByText} = render( + + + + An error + + ); + + const element = getByText('An error'); + expect(element.tagName).toBe('DIV'); + }); + + it('does not warn when render prop returns element matching elementType', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + render( + + + +
      • Error
      }> + Error +
      +
      + ); + + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unexpected DOM element')); + warnSpy.mockRestore(); + }); + + it('warns when render prop returns element that does not match elementType', async () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + render( + + + +
      }> + Error + + + ); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unexpected DOM element')); + warnSpy.mockRestore(); + }); }); From db6c93474793f0968a2f86712162fe91d7d62553 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Mon, 16 Mar 2026 15:31:46 -0500 Subject: [PATCH 05/10] fix: Prevent Spectrum Picker from selecting new value after closing (#8475) * fix: Picker select after close * apply pointerevents none to every child when popover is closing * fix the fix due to when state vs renders happen --- .../@adobe/spectrum-css-temp/components/popover/index.css | 8 ++++++++ packages/@react-spectrum/overlays/src/Popover.tsx | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/@adobe/spectrum-css-temp/components/popover/index.css b/packages/@adobe/spectrum-css-temp/components/popover/index.css index 744aef962ed..c4d786a8b45 100644 --- a/packages/@adobe/spectrum-css-temp/components/popover/index.css +++ b/packages/@adobe/spectrum-css-temp/components/popover/index.css @@ -51,6 +51,14 @@ governing permissions and limitations under the License. overflow: hidden; } +/* Prevent clicks during exit animation (e.g. Picker selecting wrong item). + pointer-events is not inherited, so we must disable it on the popover and all descendants. */ +.spectrum-Popover.is-exiting, +.spectrum-Popover.is-exiting * { + pointer-events: none; + touch-action: none; +} + .is-open { composes: spectrum-overlay--open; } diff --git a/packages/@react-spectrum/overlays/src/Popover.tsx b/packages/@react-spectrum/overlays/src/Popover.tsx index de4b7064334..707540596d6 100644 --- a/packages/@react-spectrum/overlays/src/Popover.tsx +++ b/packages/@react-spectrum/overlays/src/Popover.tsx @@ -138,7 +138,8 @@ const PopoverWrapper = forwardRef((props: PopoverWrapperProps, ref: ForwardedRef { 'spectrum-Popover--withTip': !hideArrow, 'is-open': isOpen, - [`is-open--${placement}`]: isOpen + [`is-open--${placement}`]: isOpen, + 'is-exiting': !state.isOpen }, classNames( overrideStyles, From 8b3016ce720c28fb7dc4ec7a174d1996256f448f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sonsu/=EC=9D=B4=EC=84=B1=EC=88=98?= <127682098+sonsu-lee@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:11:49 +0900 Subject: [PATCH 06/10] fix(@react-aria/select): avoid empty option in hidden select markup (#9677) Co-authored-by: Robert Snow --- .../@react-aria/select/src/HiddenSelect.tsx | 2 +- .../select/test/HiddenSelect.test.tsx | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/@react-aria/select/src/HiddenSelect.tsx b/packages/@react-aria/select/src/HiddenSelect.tsx index 07efe254bc5..b2656bc22cf 100644 --- a/packages/@react-aria/select/src/HiddenSelect.tsx +++ b/packages/@react-aria/select/src/HiddenSelect.tsx @@ -157,7 +157,7 @@ export function HiddenSelect(props: Hidde