From d573ae4e66cc7a065b52ed6fab2917894627f5e2 Mon Sep 17 00:00:00 2001 From: "yugo.innami" Date: Fri, 15 May 2026 19:31:29 +0900 Subject: [PATCH] [menu] Support group labels in radio groups --- .../demos/group-labels/css-modules/index.tsx | 42 +++++++------ .../demos/group-labels/tailwind/index.tsx | 60 +++++++++---------- .../app/(docs)/react/components/menu/page.mdx | 2 +- docs/src/error-codes.json | 2 +- .../menu/group-label/MenuGroupLabel.test.tsx | 40 +++++++++++++ .../react/src/menu/group/MenuGroupContext.ts | 2 +- .../src/menu/radio-group/MenuRadioGroup.tsx | 10 +++- 7 files changed, 101 insertions(+), 57 deletions(-) diff --git a/docs/src/app/(docs)/react/components/menu/demos/group-labels/css-modules/index.tsx b/docs/src/app/(docs)/react/components/menu/demos/group-labels/css-modules/index.tsx index fb1d37e3cdf..d6760e84494 100644 --- a/docs/src/app/(docs)/react/components/menu/demos/group-labels/css-modules/index.tsx +++ b/docs/src/app/(docs)/react/components/menu/demos/group-labels/css-modules/index.tsx @@ -21,29 +21,27 @@ export default function ExampleMenu() { - + Sort - - - - - - Date - - - - - - Name - - - - - - Type - - - + + + + + Date + + + + + + Name + + + + + + Type + + diff --git a/docs/src/app/(docs)/react/components/menu/demos/group-labels/tailwind/index.tsx b/docs/src/app/(docs)/react/components/menu/demos/group-labels/tailwind/index.tsx index f5cadcd14c5..ce87e94e0d8 100644 --- a/docs/src/app/(docs)/react/components/menu/demos/group-labels/tailwind/index.tsx +++ b/docs/src/app/(docs)/react/components/menu/demos/group-labels/tailwind/index.tsx @@ -20,40 +20,38 @@ export default function ExampleMenu() { - + Sort - - - - - - Date - - - - - - Name - - - - - - Type - - - + + + + + Date + + + + + + Name + + + + + + Type + + diff --git a/docs/src/app/(docs)/react/components/menu/page.mdx b/docs/src/app/(docs)/react/components/menu/page.mdx index 3b4c45945b0..84d43daab23 100644 --- a/docs/src/app/(docs)/react/components/menu/page.mdx +++ b/docs/src/app/(docs)/react/components/menu/page.mdx @@ -93,7 +93,7 @@ Use the `closeOnClick` prop to change whether the menu closes when an item is cl ### Group labels -Use the `` part to add a label to a `` +Use the `` part to add a label to a `` or ``. import { DemoMenuGroupLabels } from './demos/group-labels'; diff --git a/docs/src/error-codes.json b/docs/src/error-codes.json index f879c383ecf..ab3c38e3e10 100644 --- a/docs/src/error-codes.json +++ b/docs/src/error-codes.json @@ -28,7 +28,7 @@ "28": "Base UI: FieldRootContext is missing. Field parts must be placed within .", "29": "[Floating UI]: Invalid grid - item width at index %s is greater than grid columns", "30": "Base UI: MenuCheckboxItemContext is missing. MenuCheckboxItem parts must be placed within .", - "31": "Base UI: MenuGroupRootContext is missing. Menu group parts must be used within .", + "31": "Base UI: MenuGroupRootContext is missing. Menu group parts must be used within or .", "32": "Base UI: is missing.", "33": "Base UI: MenuPositionerContext is missing. MenuPositioner parts must be placed within .", "34": "Base UI: MenuRadioGroupContext is missing. MenuRadioGroup parts must be placed within .", diff --git a/packages/react/src/menu/group-label/MenuGroupLabel.test.tsx b/packages/react/src/menu/group-label/MenuGroupLabel.test.tsx index 43f5503df06..52fcdc1c9b8 100644 --- a/packages/react/src/menu/group-label/MenuGroupLabel.test.tsx +++ b/packages/react/src/menu/group-label/MenuGroupLabel.test.tsx @@ -79,5 +79,45 @@ describe('', () => { const group = screen.getByRole('group'); expect(group).toHaveAttribute('aria-labelledby', 'test-group'); }); + + it("should reference the generated id in RadioGroup's `aria-labelledby`", async () => { + await render( + + + + + + Test group + + + + + , + ); + + const radioGroup = screen.getByRole('group'); + const groupLabel = screen.getByText('Test group'); + + expect(radioGroup).toHaveAttribute('aria-labelledby', groupLabel.id); + }); + + it("should reference the provided id in RadioGroup's `aria-labelledby`", async () => { + await render( + + + + + + Test group + + + + + , + ); + + const radioGroup = screen.getByRole('group'); + expect(radioGroup).toHaveAttribute('aria-labelledby', 'test-group'); + }); }); }); diff --git a/packages/react/src/menu/group/MenuGroupContext.ts b/packages/react/src/menu/group/MenuGroupContext.ts index 6331afd21e9..0b88f15e73c 100644 --- a/packages/react/src/menu/group/MenuGroupContext.ts +++ b/packages/react/src/menu/group/MenuGroupContext.ts @@ -11,7 +11,7 @@ export function useMenuGroupRootContext() { const context = React.useContext(MenuGroupContext); if (context === undefined) { throw new Error( - 'Base UI: MenuGroupRootContext is missing. Menu group parts must be used within .', + 'Base UI: MenuGroupRootContext is missing. Menu group parts must be used within or .', ); } diff --git a/packages/react/src/menu/radio-group/MenuRadioGroup.tsx b/packages/react/src/menu/radio-group/MenuRadioGroup.tsx index 55bd79eee6f..789ba5b3342 100644 --- a/packages/react/src/menu/radio-group/MenuRadioGroup.tsx +++ b/packages/react/src/menu/radio-group/MenuRadioGroup.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { useControlled } from '@base-ui/utils/useControlled'; import { useStableCallback } from '@base-ui/utils/useStableCallback'; import { MenuRadioGroupContext } from './MenuRadioGroupContext'; +import { MenuGroupContext } from '../group/MenuGroupContext'; import { useRenderElement } from '../../internals/useRenderElement'; import type { BaseUIComponentProps } from '../../internals/types'; import type { MenuRoot } from '../root/MenuRoot'; @@ -29,6 +30,8 @@ export const MenuRadioGroup = React.memo( ...elementProps } = componentProps; + const [labelId, setLabelId] = React.useState(undefined); + const [value, setValueUnwrapped] = useControlled({ controlled: valueProp, default: defaultValue, @@ -54,6 +57,7 @@ export const MenuRadioGroup = React.memo( ref: forwardedRef, props: { role: 'group', + 'aria-labelledby': labelId, 'aria-disabled': disabled || undefined, ...elementProps, }, @@ -68,8 +72,12 @@ export const MenuRadioGroup = React.memo( [value, setValue, disabled], ); + const groupContext = React.useMemo(() => ({ setLabelId }), [setLabelId]); + return ( - {element} + + {element} + ); }), );