fix(packages): scope menu data attributes (#1628)

This commit is contained in:
Sam Potts
2026-05-29 16:26:47 +10:00
committed by GitHub
parent e7aa0a6669
commit 01a2115aab
24 changed files with 237 additions and 79 deletions
-1
View File
@@ -33,7 +33,6 @@ export const MenuBack = forwardRef<HTMLButtonElement, MenuBackProps>(function Me
{ render, className, style },
{
state: parentMenu?.state ?? ({} as MenuState),
stateAttrMap: parentMenu?.stateAttrMap,
ref: forwardedRef,
props: [
{
@@ -21,7 +21,7 @@ export const MenuCheckboxItem = forwardRef<HTMLDivElement, MenuCheckboxItemProps
{ render, className, style, checked, onCheckedChange, disabled, onClick, ...elementProps },
forwardedRef
) {
const { menu, state, stateAttrMap } = useMenuContext();
const { menu, state } = useMenuContext();
const elementRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -50,7 +50,6 @@ export const MenuCheckboxItem = forwardRef<HTMLDivElement, MenuCheckboxItemProps
{ render, className, style },
{
state,
stateAttrMap,
ref: [forwardedRef, elementRef],
props: [
{
@@ -332,7 +332,6 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
{ render, className, style },
{
state,
stateAttrMap,
ref: menuViewComposedRef,
props: [
{
+1 -2
View File
@@ -17,14 +17,13 @@ export const MenuGroup = forwardRef<HTMLDivElement, MenuGroupProps>(function Men
{ render, className, style, label, ...elementProps },
forwardedRef
) {
const { state, stateAttrMap } = useMenuContext();
const { state } = useMenuContext();
return renderElement(
'div',
{ render, className, style },
{
state,
stateAttrMap,
ref: [forwardedRef],
props: [{ role: 'group' as const, 'aria-label': label }, elementProps],
}
@@ -19,7 +19,7 @@ export const MenuItemIndicator = forwardRef<HTMLSpanElement, MenuItemIndicatorPr
{ render, className, style, checked, forceMount = false, ...elementProps },
forwardedRef
) {
const { state, stateAttrMap } = useMenuContext();
const { state } = useMenuContext();
if (!checked && !forceMount) return null;
@@ -28,7 +28,6 @@ export const MenuItemIndicator = forwardRef<HTMLSpanElement, MenuItemIndicatorPr
{ render, className, style },
{
state,
stateAttrMap,
ref: [forwardedRef],
props: [{ 'aria-hidden': true }, elementProps],
}
+1 -2
View File
@@ -20,7 +20,7 @@ export const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(function MenuI
{ render, className, style, onSelect, disabled, onClick, ...elementProps },
forwardedRef
) {
const { menu, state, stateAttrMap } = useMenuContext();
const { menu, state } = useMenuContext();
const subMenuCtx = useSubMenuContext();
const parentMenu = subMenuCtx?.parentMenu.menu ?? null;
const elementRef = useRef<HTMLDivElement>(null);
@@ -52,7 +52,6 @@ export const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(function MenuI
{ render, className, style },
{
state,
stateAttrMap,
ref: [forwardedRef, elementRef],
props: [
{
+1 -2
View File
@@ -14,14 +14,13 @@ export const MenuLabel = forwardRef<HTMLDivElement, MenuLabelProps>(function Men
{ render, className, style, ...elementProps },
forwardedRef
) {
const { state, stateAttrMap } = useMenuContext();
const { state } = useMenuContext();
return renderElement(
'div',
{ render, className, style },
{
state,
stateAttrMap,
ref: [forwardedRef],
props: [elementProps],
}
@@ -21,7 +21,7 @@ export const MenuRadioGroup = forwardRef<HTMLDivElement, MenuRadioGroupProps>(fu
{ render, className, style, value, onValueChange, label, ...elementProps },
forwardedRef
) {
const { state, stateAttrMap } = useMenuContext();
const { state } = useMenuContext();
return (
<MenuRadioGroupContextProvider value={{ value, onValueChange }}>
@@ -30,7 +30,6 @@ export const MenuRadioGroup = forwardRef<HTMLDivElement, MenuRadioGroupProps>(fu
{ render, className, style },
{
state,
stateAttrMap,
ref: [forwardedRef],
props: [{ role: 'group' as const, 'aria-label': label }, elementProps],
}
@@ -20,7 +20,7 @@ export const MenuRadioItem = forwardRef<HTMLDivElement, MenuRadioItemProps>(func
{ render, className, style, value, disabled, onClick, ...elementProps },
forwardedRef
) {
const { menu, state, stateAttrMap } = useMenuContext();
const { menu, state } = useMenuContext();
const { value: groupValue, onValueChange } = useMenuRadioGroupContext();
const subMenuCtx = useSubMenuContext();
const parentMenu = subMenuCtx?.parentMenu.menu ?? null;
@@ -54,7 +54,6 @@ export const MenuRadioItem = forwardRef<HTMLDivElement, MenuRadioItemProps>(func
{ render, className, style },
{
state,
stateAttrMap,
ref: [forwardedRef, elementRef],
props: [
{
@@ -14,14 +14,13 @@ export const MenuSeparator = forwardRef<HTMLDivElement, MenuSeparatorProps>(func
{ render, className, style, ...elementProps },
forwardedRef
) {
const { state, stateAttrMap } = useMenuContext();
const { state } = useMenuContext();
return renderElement(
'div',
{ render, className, style },
{
state,
stateAttrMap,
ref: [forwardedRef],
props: [{ role: 'separator' as const }, elementProps],
}
+1 -3
View File
@@ -49,7 +49,7 @@ export const MenuTrigger = forwardRef<HTMLButtonElement | HTMLDivElement, MenuTr
{ render, className, style, disabled, onClick, onKeyDown, ...elementProps },
forwardedRef
) {
const { core, menu, state, stateAttrMap, anchorName, contentId } = useMenuContext();
const { core, menu, state, anchorName, contentId } = useMenuContext();
const subMenuCtx = useSubMenuContext();
const isSubMenuTrigger = subMenuCtx !== null;
@@ -119,7 +119,6 @@ export const MenuTrigger = forwardRef<HTMLButtonElement | HTMLDivElement, MenuTr
{ render, className, style },
{
state: parentState,
stateAttrMap,
ref: [forwardedRef as React.Ref<HTMLDivElement>, elementRef as React.Ref<HTMLDivElement>],
props: [
{
@@ -145,7 +144,6 @@ export const MenuTrigger = forwardRef<HTMLButtonElement | HTMLDivElement, MenuTr
{ render, className, style },
{
state,
stateAttrMap,
ref: [forwardedRef as React.Ref<HTMLButtonElement>, triggerRef],
props: [
{ type: 'button' as const, ...core.getTriggerAttrs(state, contentId) },
@@ -6,8 +6,14 @@ import { ControlsContextProvider } from '../../controls/context';
import { MenuBack } from '../menu-back';
import { MenuCheckboxItem } from '../menu-checkbox-item';
import { MenuContent } from '../menu-content';
import { MenuGroup } from '../menu-group';
import { MenuItem } from '../menu-item';
import { MenuItemIndicator } from '../menu-item-indicator';
import { MenuLabel } from '../menu-label';
import { MenuRadioGroup } from '../menu-radio-group';
import { MenuRadioItem } from '../menu-radio-item';
import { MenuRoot } from '../menu-root';
import { MenuSeparator } from '../menu-separator';
import { MenuTrigger } from '../menu-trigger';
import { MenuView } from '../menu-view';
@@ -263,7 +269,93 @@ function FocusOutFixture({ onRootOpenChange }: { onRootOpenChange: NonNullable<M
);
}
const menuStateAttrs = ['data-open', 'data-side', 'data-align', 'data-starting-style', 'data-ending-style'] as const;
function expectNoMenuStateAttrs(element: HTMLElement): void {
for (const attr of menuStateAttrs) {
expect(element.hasAttribute(attr), `${element.dataset.testid ?? element.tagName} should not have ${attr}`).toBe(
false
);
}
}
describe('MenuContent', () => {
it('scopes menu state data attributes to content elements', async () => {
render(
<MenuRoot defaultOpen side="top" align="end">
<MenuTrigger data-testid="trigger">Settings</MenuTrigger>
<MenuContent data-testid="root-content">
<MenuLabel data-testid="label">Playback</MenuLabel>
<MenuGroup data-testid="group" label="Playback">
<MenuItem data-testid="item">Copy link</MenuItem>
<MenuCheckboxItem data-testid="checkbox-item" checked={false} onCheckedChange={vi.fn()}>
Autoplay
</MenuCheckboxItem>
<MenuRadioGroup data-testid="radio-group" value="auto" onValueChange={vi.fn()} label="Quality">
<MenuRadioItem data-testid="radio-item" value="auto">
Auto
<MenuItemIndicator data-testid="indicator" checked>
Checked
</MenuItemIndicator>
</MenuRadioItem>
</MenuRadioGroup>
</MenuGroup>
<MenuSeparator data-testid="separator" />
<MenuView data-testid="root-view">
<MenuRoot>
<MenuTrigger data-testid="submenu-trigger">Quality</MenuTrigger>
<MenuContent data-testid="submenu-content">
<MenuBack data-testid="back">Back</MenuBack>
<MenuItem data-testid="submenu-item">Auto</MenuItem>
</MenuContent>
</MenuRoot>
</MenuView>
</MenuContent>
</MenuRoot>
);
const rootContent = screen.getByTestId('root-content');
expect(rootContent.hasAttribute('data-open')).toBe(true);
expect(rootContent.getAttribute('data-side')).toBe('top');
expect(rootContent.getAttribute('data-align')).toBe('end');
for (const testId of [
'trigger',
'label',
'group',
'separator',
'item',
'checkbox-item',
'radio-group',
'radio-item',
'indicator',
'submenu-trigger',
]) {
expectNoMenuStateAttrs(screen.getByTestId(testId));
}
expect(screen.getByTestId('item').hasAttribute('data-item')).toBe(true);
expect(screen.getByTestId('radio-item').hasAttribute('data-item')).toBe(true);
expect(screen.getByTestId('checkbox-item').hasAttribute('data-item')).toBe(true);
expect(screen.getByTestId('submenu-trigger').hasAttribute('data-item')).toBe(true);
fireEvent.click(screen.getByTestId('submenu-trigger'));
await waitFor(() => {
expect(screen.getByTestId('submenu-content').getAttribute('data-menu-view-state')).toBe('active');
});
const submenuContent = screen.getByTestId('submenu-content');
expect(submenuContent.hasAttribute('data-submenu')).toBe(true);
expect(submenuContent.hasAttribute('data-menu-view')).toBe(true);
expect(submenuContent.hasAttribute('data-open')).toBe(true);
expect(submenuContent.hasAttribute('data-side')).toBe(false);
expect(submenuContent.hasAttribute('data-align')).toBe(false);
expectNoMenuStateAttrs(screen.getByTestId('back'));
});
it('marks the root view inactive while a submenu view is active', async () => {
render(<SubmenuFixture />);