From e1c05d6d8191dc96b3fafa9a478835cfcf12e4ba Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 22 Dec 2025 10:11:24 -0500 Subject: [PATCH 01/12] fix(ListBox): prevent closing Combobox when a section header is clicked (#9223) * fix: prevent closing combobox when section headers are clicked * add unit test --------- Co-authored-by: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> --- .../listbox/src/useListBoxSection.ts | 8 ++- .../test/ComboBox.test.js | 66 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/listbox/src/useListBoxSection.ts b/packages/@react-aria/listbox/src/useListBoxSection.ts index 82952c43bcc..1b0a045cf4c 100644 --- a/packages/@react-aria/listbox/src/useListBoxSection.ts +++ b/packages/@react-aria/listbox/src/useListBoxSection.ts @@ -46,11 +46,15 @@ export function useListBoxSection(props: AriaListBoxSectionProps): ListBoxSectio role: 'presentation' }, headingProps: heading ? { - // Techincally, listbox cannot contain headings according to ARIA. + // Technically, listbox cannot contain headings according to ARIA. // We hide the heading from assistive technology, using role="presentation", // and only use it as a visual label for the nested group. id: headingId, - role: 'presentation' + role: 'presentation', + onMouseDown: (e) => { + // Prevent DOM focus from moving on mouse down when using virtual focus + e.preventDefault(); + } } : {}, groupProps: { role: 'group', diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js index b786271a0da..cdd950f47a1 100644 --- a/packages/react-aria-components/test/ComboBox.test.js +++ b/packages/react-aria-components/test/ComboBox.test.js @@ -482,4 +482,70 @@ describe('ComboBox', () => { expect(onAction).toHaveBeenCalledTimes(2); expect(comboboxTester.combobox).toHaveValue('Cat'); }); + + it('should not close the combobox when clicking on a section header', async () => { + let tree = render( + + + + + + + Open + Rename… + Duplicate + Share… + Delete… + + + + ); + + expect(queryByRole('menu')).not.toBeInTheDocument(); + + let button = getByRole('button'); + await user.click(button); + + let menu = getByRole('menu'); + expect(menu).toBeInTheDocument(); + + let items = getAllByRole('menuitem'); + expect(items).toHaveLength(5); + + let item = items[0]; + expect(item).toHaveTextContent('Open'); + await user.click(item); + expect(menu).toBeInTheDocument(); + }); + + it('should not close individual menu item when shouldCloseOnSelect=false', async () => { + let {queryByRole, getByRole, getAllByRole} = render( + + + + + Open + Rename… + Duplicate + Share… + Delete… + + + + ); + + expect(queryByRole('menu')).not.toBeInTheDocument(); + + let button = getByRole('button'); + await user.click(button); + + let menu = getByRole('menu'); + expect(menu).toBeInTheDocument(); + + let items = getAllByRole('menuitem'); + expect(items).toHaveLength(5); + + let item = items[0]; + expect(item).toHaveTextContent('Open'); + await user.click(item); + expect(menu).toBeInTheDocument(); + + item = items[1]; + expect(item).toHaveTextContent('Rename'); + await user.click(item); + expect(menu).not.toBeInTheDocument(); + }); + + it('should not close menu items within a section when shouldCloseOnSelect=false', async () => { + let {queryByRole, getByRole, getAllByRole} = render( + + + + + + Open + Rename… + Duplicate + + + Share… + Delete… + + + + + ); + + expect(queryByRole('menu')).not.toBeInTheDocument(); + + let button = getByRole('button'); + await user.click(button); + + let menu = getByRole('menu'); + expect(menu).toBeInTheDocument(); + + let items = getAllByRole('menuitem'); + expect(items).toHaveLength(5); + + let item = items[0]; + expect(item).toHaveTextContent('Open'); + await user.click(item); + expect(menu).toBeInTheDocument(); + + item = items[3]; + expect(item).toHaveTextContent('Share'); + await user.click(item); + expect(menu).not.toBeInTheDocument(); + }); + describe('supports links', function () { describe.each(['mouse', 'keyboard'])('%s', (type) => { it.each(['none', 'single', 'multiple'] as unknown as SelectionMode[])('with selectionMode = %s', async function (selectionMode) { From 6571d211e16ce1debef85ffa4e0e65937bd2e8b2 Mon Sep 17 00:00:00 2001 From: Aryan Bagade <73382554+AryanBagade@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:16:24 -0800 Subject: [PATCH 06/12] fix(react-aria-components): add id prop to Link component (#9349) The Link component was missing the `id` prop in its TypeScript interface, even though it worked at runtime via filterDOMProps. This adds DOMProps to LinkProps interface to expose the id prop, matching the pattern used by other components like OverlayArrow and FieldError Fixes #9348 --- packages/react-aria-components/src/Link.tsx | 4 ++-- packages/react-aria-components/test/Link.test.js | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/src/Link.tsx b/packages/react-aria-components/src/Link.tsx index 22df63e3ac3..35f0667c727 100644 --- a/packages/react-aria-components/src/Link.tsx +++ b/packages/react-aria-components/src/Link.tsx @@ -19,11 +19,11 @@ import { useContextProps, useRenderProps } from './utils'; +import {DOMProps, forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; import {filterDOMProps} from '@react-aria/utils'; -import {forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; import React, {createContext, ElementType, ForwardedRef, forwardRef} from 'react'; -export interface LinkProps extends Omit, HoverEvents, RenderProps, SlotProps, Omit, 'onClick'> { +export interface LinkProps extends Omit, HoverEvents, RenderProps, SlotProps, DOMProps, Omit, 'onClick'> { /** * 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-Link' diff --git a/packages/react-aria-components/test/Link.test.js b/packages/react-aria-components/test/Link.test.js index d2defeabf52..3b34d8dd1ff 100644 --- a/packages/react-aria-components/test/Link.test.js +++ b/packages/react-aria-components/test/Link.test.js @@ -40,6 +40,12 @@ describe('Link', () => { expect(link).toHaveAttribute('data-foo', 'bar'); }); + it('should support id prop', () => { + let {getByRole} = render(Test); + let link = getByRole('link'); + expect(link).toHaveAttribute('id', 'my-link-id'); + }); + it('should support render props', async () => { let {getByRole} = render({({isHovered}) => isHovered ? 'Hovered' : 'Test'}); let link = getByRole('link'); From 0e8db485e116fd0005d5253b0748a7ec6895d968 Mon Sep 17 00:00:00 2001 From: XiaoYan Li Date: Tue, 23 Dec 2025 02:17:33 +0800 Subject: [PATCH 07/12] fix: exclude disabledKeys prop from TabPansls component (#9238) --- packages/react-aria-components/src/Tabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria-components/src/Tabs.tsx b/packages/react-aria-components/src/Tabs.tsx index a655398a352..3ff831f1ebd 100644 --- a/packages/react-aria-components/src/Tabs.tsx +++ b/packages/react-aria-components/src/Tabs.tsx @@ -327,7 +327,7 @@ export const Tab = /*#__PURE__*/ createLeafComponent(TabItemNode, (props: TabPro ); }); -export interface TabPanelsProps extends CollectionProps, StyleProps, GlobalDOMAttributes { +export interface TabPanelsProps extends Omit, 'disabledKeys'>, StyleProps, GlobalDOMAttributes { /** * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. * @default 'react-aria-TabPanels' From 8f18b4bc4a9ec09d6084dee97be8867eec41eca5 Mon Sep 17 00:00:00 2001 From: XiaoYan Li Date: Tue, 23 Dec 2025 02:22:57 +0800 Subject: [PATCH 08/12] fix: exclude null type from RadioGroup validate value (#9240) --- packages/@react-types/radio/src/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-types/radio/src/index.d.ts b/packages/@react-types/radio/src/index.d.ts index 5646a4ed95e..383832e8e73 100644 --- a/packages/@react-types/radio/src/index.d.ts +++ b/packages/@react-types/radio/src/index.d.ts @@ -30,7 +30,7 @@ import { } from '@react-types/shared'; import {ReactElement, ReactNode} from 'react'; -export interface RadioGroupProps extends ValueBase, InputBase, Pick, Validation, LabelableProps, HelpTextProps, FocusEvents { +export interface RadioGroupProps extends ValueBase, InputBase, Pick, Validation, LabelableProps, HelpTextProps, FocusEvents { /** * The axis the Radio Button(s) should align with. * @default 'vertical' From 1cbf47d6fb82407039f813a1b3c65515f4ad1da5 Mon Sep 17 00:00:00 2001 From: Alex Dametto <33689349+alexdametto@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:23:21 +0100 Subject: [PATCH 09/12] fix: ensure contains is used as default filter in ComboBox (#9234) * fix: ensure contains is used as default filter in ComboBox * add test --------- Co-authored-by: Robert Snow Co-authored-by: Robert Snow --- .../react-aria-components/src/ComboBox.tsx | 2 +- .../test/ComboBox.test.js | 42 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/react-aria-components/src/ComboBox.tsx b/packages/react-aria-components/src/ComboBox.tsx index 538b381b32a..466c696b5fe 100644 --- a/packages/react-aria-components/src/ComboBox.tsx +++ b/packages/react-aria-components/src/ComboBox.tsx @@ -133,8 +133,8 @@ function ComboBoxInner({props, collection, comboBoxRef: ref}: let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native'; let {contains} = useFilter({sensitivity: 'base'}); let state = useComboBoxState({ - defaultFilter: props.defaultFilter || contains, ...props, + defaultFilter: props.defaultFilter || contains, // If props.items isn't provided, rely on collection filtering (aka listbox.items is provided or defaultItems provided to Combobox) items: props.items, children: undefined, diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js index cdd950f47a1..8cf20e4f382 100644 --- a/packages/react-aria-components/test/ComboBox.test.js +++ b/packages/react-aria-components/test/ComboBox.test.js @@ -147,6 +147,44 @@ describe('ComboBox', () => { expect(options).toHaveLength(1); }); + it('should support undefined defaultFilter', async () => { + let tree = render( + + + +