Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,27 @@ export default function ExampleMenu() {
<ArrowSvg />
</Menu.Arrow>

<Menu.Group>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.GroupLabel className={styles.GroupLabel}>Sort</Menu.GroupLabel>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.RadioItem className={styles.RadioItem} value="date">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Date</span>
</Menu.RadioItem>
<Menu.RadioItem className={styles.RadioItem} value="name">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Name</span>
</Menu.RadioItem>
<Menu.RadioItem className={styles.RadioItem} value="type">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Type</span>
</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Group>
<Menu.RadioItem className={styles.RadioItem} value="date">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Date</span>
</Menu.RadioItem>
<Menu.RadioItem className={styles.RadioItem} value="name">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Name</span>
</Menu.RadioItem>
<Menu.RadioItem className={styles.RadioItem} value="type">
<Menu.RadioItemIndicator className={styles.RadioItemIndicator}>
<CheckIcon className={styles.RadioItemIndicatorIcon} />
</Menu.RadioItemIndicator>
<span className={styles.RadioItemText}>Type</span>
</Menu.RadioItem>
</Menu.RadioGroup>

<Menu.Separator className={styles.Separator} />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,38 @@ export default function ExampleMenu() {
<ArrowSvg />
</Menu.Arrow>

<Menu.Group>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.GroupLabel className="cursor-default py-2 pr-8 pl-7.5 text-sm leading-4 text-gray-600 select-none">
Sort
</Menu.GroupLabel>
<Menu.RadioGroup value={value} onValueChange={setValue}>
<Menu.RadioItem
value="date"
className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-hidden select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[disabled]:text-gray-400 data-[disabled]:data-[highlighted]:before:bg-gray-300"
>
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Date</span>
</Menu.RadioItem>
<Menu.RadioItem
value="name"
className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-hidden select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[disabled]:text-gray-400 data-[disabled]:data-[highlighted]:before:bg-gray-300"
>
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Name</span>
</Menu.RadioItem>
<Menu.RadioItem
value="type"
className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-hidden select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[disabled]:text-gray-400 data-[disabled]:data-[highlighted]:before:bg-gray-300"
>
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Type</span>
</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Group>
<Menu.RadioItem
value="date"
className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-hidden select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[disabled]:text-gray-400 data-[disabled]:data-[highlighted]:before:bg-gray-300"
>
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Date</span>
</Menu.RadioItem>
<Menu.RadioItem
value="name"
className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-hidden select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[disabled]:text-gray-400 data-[disabled]:data-[highlighted]:before:bg-gray-300"
>
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Name</span>
</Menu.RadioItem>
<Menu.RadioItem
value="type"
className="grid cursor-default grid-cols-[0.75rem_1fr] items-center gap-2 py-2 pr-8 pl-2.5 text-sm leading-4 outline-hidden select-none data-[highlighted]:relative data-[highlighted]:z-0 data-[highlighted]:text-gray-50 data-[highlighted]:before:absolute data-[highlighted]:before:inset-x-1 data-[highlighted]:before:inset-y-0 data-[highlighted]:before:z-[-1] data-[highlighted]:before:rounded-sm data-[highlighted]:before:bg-gray-900 data-[disabled]:text-gray-400 data-[disabled]:data-[highlighted]:before:bg-gray-300"
>
<Menu.RadioItemIndicator className="col-start-1">
<CheckIcon className="size-3" />
</Menu.RadioItemIndicator>
<span className="col-start-2">Type</span>
</Menu.RadioItem>
</Menu.RadioGroup>

<Menu.Separator className="my-1.5 mr-4 ml-7.5 h-px bg-gray-200" />

Expand Down
2 changes: 1 addition & 1 deletion docs/src/app/(docs)/react/components/menu/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Use the `closeOnClick` prop to change whether the menu closes when an item is cl

### Group labels

Use the `<Menu.GroupLabel>` part to add a label to a `<Menu.Group>`
Use the `<Menu.GroupLabel>` part to add a label to a `<Menu.Group>` or `<Menu.RadioGroup>`.

import { DemoMenuGroupLabels } from './demos/group-labels';

Expand Down
2 changes: 1 addition & 1 deletion docs/src/error-codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"28": "Base UI: FieldRootContext is missing. Field parts must be placed within <Field.Root>.",
"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 <Menu.CheckboxItem>.",
"31": "Base UI: MenuGroupRootContext is missing. Menu group parts must be used within <Menu.Group>.",
"31": "Base UI: MenuGroupRootContext is missing. Menu group parts must be used within <Menu.Group> or <Menu.RadioGroup>.",
"32": "Base UI: <Menu.Portal> is missing.",
"33": "Base UI: MenuPositionerContext is missing. MenuPositioner parts must be placed within <Menu.Positioner>.",
"34": "Base UI: MenuRadioGroupContext is missing. MenuRadioGroup parts must be placed within <Menu.RadioGroup>.",
Expand Down
40 changes: 40 additions & 0 deletions packages/react/src/menu/group-label/MenuGroupLabel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,45 @@ describe('<Menu.GroupLabel />', () => {
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(
<Menu.Root open>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.RadioGroup>
<Menu.GroupLabel>Test group</Menu.GroupLabel>
</Menu.RadioGroup>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>,
);

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(
<Menu.Root open>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.RadioGroup>
<Menu.GroupLabel id="test-group">Test group</Menu.GroupLabel>
</Menu.RadioGroup>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>,
);

const radioGroup = screen.getByRole('group');
expect(radioGroup).toHaveAttribute('aria-labelledby', 'test-group');
});
});
});
2 changes: 1 addition & 1 deletion packages/react/src/menu/group/MenuGroupContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Menu.Group>.',
'Base UI: MenuGroupRootContext is missing. Menu group parts must be used within <Menu.Group> or <Menu.RadioGroup>.',
);
}

Expand Down
10 changes: 9 additions & 1 deletion packages/react/src/menu/radio-group/MenuRadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,6 +30,8 @@ export const MenuRadioGroup = React.memo(
...elementProps
} = componentProps;

const [labelId, setLabelId] = React.useState<string | undefined>(undefined);

const [value, setValueUnwrapped] = useControlled({
controlled: valueProp,
default: defaultValue,
Expand All @@ -54,6 +57,7 @@ export const MenuRadioGroup = React.memo(
ref: forwardedRef,
props: {
role: 'group',
'aria-labelledby': labelId,
'aria-disabled': disabled || undefined,
...elementProps,
},
Expand All @@ -68,8 +72,12 @@ export const MenuRadioGroup = React.memo(
[value, setValue, disabled],
);

const groupContext = React.useMemo(() => ({ setLabelId }), [setLabelId]);

return (
<MenuRadioGroupContext.Provider value={context}>{element}</MenuRadioGroupContext.Provider>
<MenuGroupContext.Provider value={groupContext}>
<MenuRadioGroupContext.Provider value={context}>{element}</MenuRadioGroupContext.Provider>
</MenuGroupContext.Provider>
);
}),
);
Expand Down
Loading