diff --git a/docs/reference/generated/alert-dialog-root.json b/docs/reference/generated/alert-dialog-root.json index f3e936c936..8457b68adf 100644 --- a/docs/reference/generated/alert-dialog-root.json +++ b/docs/reference/generated/alert-dialog-root.json @@ -15,7 +15,7 @@ "type": "(open, event, reason) => void", "description": "Event handler called when the dialog is opened or closed." }, - "action": { + "actionsRef": { "type": "{ current: { unmount: func } }", "description": "A ref to imperative actions." }, diff --git a/docs/reference/generated/dialog-root.json b/docs/reference/generated/dialog-root.json index c5f439d8ca..55279effdb 100644 --- a/docs/reference/generated/dialog-root.json +++ b/docs/reference/generated/dialog-root.json @@ -15,7 +15,7 @@ "type": "(open, event, reason) => void", "description": "Event handler called when the dialog is opened or closed." }, - "action": { + "actionsRef": { "type": "{ current: { unmount: func } }", "description": "A ref to imperative actions." }, diff --git a/docs/reference/generated/menu-root.json b/docs/reference/generated/menu-root.json index 1f80366659..0379693706 100644 --- a/docs/reference/generated/menu-root.json +++ b/docs/reference/generated/menu-root.json @@ -15,7 +15,7 @@ "type": "(open, event) => void", "description": "Event handler called when the menu is opened or closed." }, - "action": { + "actionsRef": { "type": "{ current: { unmount: func } }", "description": "A ref to imperative actions." }, diff --git a/docs/reference/generated/menu-submenu-trigger.json b/docs/reference/generated/menu-submenu-trigger.json index 0e5ad4665b..cf52883414 100644 --- a/docs/reference/generated/menu-submenu-trigger.json +++ b/docs/reference/generated/menu-submenu-trigger.json @@ -6,11 +6,6 @@ "type": "string", "description": "Overrides the text label to use when the item is matched during keyboard text navigation." }, - "disabled": { - "type": "boolean", - "default": "false", - "description": "Whether the component should ignore user interaction." - }, "className": { "type": "string | (state) => string", "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." diff --git a/docs/reference/generated/popover-root.json b/docs/reference/generated/popover-root.json index dafc147344..b2d582c26c 100644 --- a/docs/reference/generated/popover-root.json +++ b/docs/reference/generated/popover-root.json @@ -15,7 +15,7 @@ "type": "(open, event, reason) => void", "description": "Event handler called when the popover is opened or closed." }, - "action": { + "actionsRef": { "type": "{ current: { unmount: func } }", "description": "A ref to imperative actions." }, diff --git a/docs/reference/generated/preview-card-root.json b/docs/reference/generated/preview-card-root.json index ada1698505..2bb0f12fbc 100644 --- a/docs/reference/generated/preview-card-root.json +++ b/docs/reference/generated/preview-card-root.json @@ -15,7 +15,7 @@ "type": "(open, event, reason) => void", "description": "Event handler called when the preview card is opened or closed." }, - "action": { + "actionsRef": { "type": "{ current: { unmount: func } }", "description": "A ref to imperative actions." }, diff --git a/docs/reference/generated/tooltip-root.json b/docs/reference/generated/tooltip-root.json index 9915a30f70..a43f394c5f 100644 --- a/docs/reference/generated/tooltip-root.json +++ b/docs/reference/generated/tooltip-root.json @@ -15,7 +15,7 @@ "type": "(open, event, reason) => void", "description": "Event handler called when the tooltip is opened or closed." }, - "action": { + "actionsRef": { "type": "{ current: { unmount: func } }", "description": "A ref to imperative actions." }, diff --git a/docs/src/app/(private)/experiments/menu/menu-fully-featured.tsx b/docs/src/app/(private)/experiments/menu/menu-fully-featured.tsx index 36a68247b9..d3ccb455b0 100644 --- a/docs/src/app/(private)/experiments/menu/menu-fully-featured.tsx +++ b/docs/src/app/(private)/experiments/menu/menu-fully-featured.tsx @@ -12,16 +12,31 @@ interface Settings { customAnchor: boolean; modal: boolean; openOnHover: boolean; + disabled: boolean; + customTriggerElement: boolean; + side: Menu.Positioner.Props['side']; + align: Menu.Positioner.Props['align']; } export default function MenuFullyFeatured() { const { settings } = useExperimentSettings(); + const anchorRef = React.useRef(null); + const triggerRender = React.useMemo( + () => (settings.customTriggerElement ? : undefined), + [settings.customTriggerElement], + ); + return (
- - +

Fully featured menu

+ + Menu @@ -29,6 +44,8 @@ export default function MenuFullyFeatured() { className={classes.Positioner} sideOffset={8} anchor={settings.customAnchor ? anchorRef : undefined} + side={settings.side} + align={settings.align} > @@ -59,7 +76,7 @@ export default function MenuFullyFeatured() { Radio items - + @@ -69,7 +86,7 @@ export default function MenuFullyFeatured() { Option 1 - + @@ -81,7 +98,7 @@ export default function MenuFullyFeatured() { + + + + + + Disabled option + + @@ -139,6 +172,18 @@ export default function MenuFullyFeatured() { Option C (close on click) + + + + + + Disabled option + + @@ -171,6 +216,22 @@ export default function MenuFullyFeatured() { + + + + Disabled nested menu + + + + + + + This should not appear + + + + + @@ -199,6 +260,26 @@ export const settingsMetadata: SettingsMetadata = { type: 'boolean', label: 'Open on hover', }, + disabled: { + type: 'boolean', + label: 'Disabled', + }, + customTriggerElement: { + type: 'boolean', + label: 'Trigger as ', + }, + side: { + type: 'string', + label: 'Side', + options: ['top', 'right', 'bottom', 'left'], + default: 'bottom', + }, + align: { + type: 'string', + label: 'Align', + options: ['start', 'center', 'end'], + default: 'center', + }, }; function ChevronDownIcon(props: React.ComponentProps<'svg'>) { diff --git a/docs/src/app/(private)/experiments/menu/menu-nested.tsx b/docs/src/app/(private)/experiments/menu/menu-nested.tsx index 54b2ed18ed..fa097cb21a 100644 --- a/docs/src/app/(private)/experiments/menu/menu-nested.tsx +++ b/docs/src/app/(private)/experiments/menu/menu-nested.tsx @@ -82,7 +82,7 @@ export default function NestedMenu() { Paragraph - List + List - List + List ', () => { expect(trigger).to.have.attribute('aria-expanded', 'false'); expect(queryByText(PANEL_CONTENT_1)).to.equal(null); - setProps({ value: [0] }); - await flushMicrotasks(); + await setProps({ value: [0] }); expect(trigger).to.have.attribute('aria-expanded', 'true'); expect(trigger).to.have.attribute('data-panel-open'); @@ -158,8 +156,7 @@ describe('', () => { expect(queryByText(PANEL_CONTENT_1)).toBeVisible(); expect(queryByText(PANEL_CONTENT_1)).to.have.attribute('data-open'); - setProps({ value: [] }); - await flushMicrotasks(); + await setProps({ value: [] }); expect(trigger).to.have.attribute('aria-expanded', 'false'); expect(queryByText(PANEL_CONTENT_1)).to.equal(null); diff --git a/packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx b/packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx index f4dbee48a1..0b572d37a6 100644 --- a/packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx +++ b/packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { act, screen, waitFor } from '@mui/internal-test-utils'; import { AlertDialog } from '@base-ui-components/react/alert-dialog'; -import { createRenderer, isJSDOM } from '#test-utils'; +import { createRenderer, isJSDOM, popupConformanceTests } from '#test-utils'; import { spy } from 'sinon'; describe('', () => { @@ -12,6 +12,20 @@ describe('', () => { globalThis.BASE_UI_ANIMATIONS_DISABLED = true; }); + popupConformanceTests({ + createComponent: (props) => ( + + Open dialog + + Dialog + + + ), + render, + triggerMouseAction: 'click', + expectedPopupRole: 'alertdialog', + }); + it('ARIA attributes', async () => { const { queryByRole, getByText } = await render( diff --git a/packages/react/src/alert-dialog/root/AlertDialogRoot.tsx b/packages/react/src/alert-dialog/root/AlertDialogRoot.tsx index 47fc6fd191..0a144cfd66 100644 --- a/packages/react/src/alert-dialog/root/AlertDialogRoot.tsx +++ b/packages/react/src/alert-dialog/root/AlertDialogRoot.tsx @@ -67,7 +67,7 @@ AlertDialogRoot.propTypes /* remove-proptypes */ = { /** * A ref to imperative actions. */ - action: PropTypes.shape({ + actionsRef: PropTypes.shape({ current: PropTypes.shape({ unmount: PropTypes.func.isRequired, }).isRequired, diff --git a/packages/react/src/checkbox/root/CheckboxRoot.test.tsx b/packages/react/src/checkbox/root/CheckboxRoot.test.tsx index c7f74e7e23..e3b0c4e7b9 100644 --- a/packages/react/src/checkbox/root/CheckboxRoot.test.tsx +++ b/packages/react/src/checkbox/root/CheckboxRoot.test.tsx @@ -201,7 +201,7 @@ describe('', () => { expect(indicator).to.have.attribute('data-readonly', ''); expect(indicator).to.have.attribute('data-required', ''); - setProps({ disabled: false, readOnly: false }); + await setProps({ disabled: false, readOnly: false }); fireEvent.click(checkbox); expect(checkbox).to.have.attribute('data-unchecked', ''); diff --git a/packages/react/src/collapsible/root/CollapsibleRoot.test.tsx b/packages/react/src/collapsible/root/CollapsibleRoot.test.tsx index f09c088559..044dc9de27 100644 --- a/packages/react/src/collapsible/root/CollapsibleRoot.test.tsx +++ b/packages/react/src/collapsible/root/CollapsibleRoot.test.tsx @@ -1,7 +1,6 @@ 'use client'; import * as React from 'react'; import { expect } from 'chai'; -import { flushMicrotasks } from '@mui/internal-test-utils'; import { Collapsible } from '@base-ui-components/react/collapsible'; import { createRenderer, describeConformance, isJSDOM } from '#test-utils'; @@ -33,6 +32,21 @@ describe('', () => { }); }); + describe('collapsible status', () => { + it('disabled status', async () => { + const { getByRole } = await render( + + + + , + ); + + const trigger = getByRole('button'); + + expect(trigger).to.have.attribute('data-disabled'); + }); + }); + describe('open state', () => { it('controlled mode', async () => { const { queryByText, getByRole, setProps } = await render( @@ -48,8 +62,7 @@ describe('', () => { expect(trigger).to.have.attribute('aria-expanded', 'false'); expect(queryByText(PANEL_CONTENT)).to.equal(null); - setProps({ open: true }); - await flushMicrotasks(); + await setProps({ open: true }); expect(trigger).to.have.attribute('aria-expanded', 'true'); @@ -58,8 +71,7 @@ describe('', () => { expect(queryByText(PANEL_CONTENT)).to.have.attribute('data-open'); expect(trigger).to.have.attribute('data-panel-open'); - setProps({ open: false }); - await flushMicrotasks(); + await setProps({ open: false }); expect(trigger).to.not.have.attribute('aria-controls'); expect(trigger).to.have.attribute('aria-expanded', 'false'); diff --git a/packages/react/src/collapsible/trigger/CollapsibleTrigger.tsx b/packages/react/src/collapsible/trigger/CollapsibleTrigger.tsx index 99d5619949..e50912fa24 100644 --- a/packages/react/src/collapsible/trigger/CollapsibleTrigger.tsx +++ b/packages/react/src/collapsible/trigger/CollapsibleTrigger.tsx @@ -18,9 +18,9 @@ const CollapsibleTrigger = React.forwardRef(function CollapsibleTrigger( props: CollapsibleTrigger.Props, forwardedRef: React.ForwardedRef, ) { - const { className, disabled = false, id, render, ...otherProps } = props; + const { panelId, open, setOpen, state, disabled: contextDisabled } = useCollapsibleRootContext(); - const { panelId, open, setOpen, state } = useCollapsibleRootContext(); + const { className, disabled = contextDisabled, id, render, ...otherProps } = props; const { getRootProps } = useCollapsibleTrigger({ disabled, diff --git a/packages/react/src/dialog/root/DialogRoot.test.tsx b/packages/react/src/dialog/root/DialogRoot.test.tsx index 9683c2aa5e..1f8ace8a7d 100644 --- a/packages/react/src/dialog/root/DialogRoot.test.tsx +++ b/packages/react/src/dialog/root/DialogRoot.test.tsx @@ -3,16 +3,30 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import { act, fireEvent, screen, waitFor } from '@mui/internal-test-utils'; import { Dialog } from '@base-ui-components/react/dialog'; -import { createRenderer, isJSDOM } from '#test-utils'; +import { createRenderer, isJSDOM, popupConformanceTests } from '#test-utils'; import { Menu } from '@base-ui-components/react/menu'; import { Select } from '@base-ui-components/react/select'; describe('', () => { + const { render } = createRenderer(); + beforeEach(() => { globalThis.BASE_UI_ANIMATIONS_DISABLED = true; }); - const { render } = createRenderer(); + popupConformanceTests({ + createComponent: (props) => ( + + Open dialog + + Dialog + + + ), + render, + triggerMouseAction: 'click', + expectedPopupRole: 'dialog', + }); it('ARIA attributes', async () => { const { queryByRole, getByText } = await render( @@ -40,139 +54,6 @@ describe('', () => { ); }); - describe('uncontrolled mode', () => { - it('should open the dialog with the trigger', async () => { - const { queryByRole, getByRole } = await render( - - - - - - , - ); - - const button = getByRole('button'); - expect(queryByRole('dialog')).to.equal(null); - - await act(async () => { - button.click(); - }); - - expect(queryByRole('dialog')).not.to.equal(null); - }); - }); - - describe('controlled mode', () => { - it('should open and close the dialog with the `open` prop', async () => { - const { queryByRole, setProps } = await render( - - - - - , - ); - - expect(queryByRole('dialog')).to.equal(null); - - setProps({ open: true }); - expect(queryByRole('dialog')).not.to.equal(null); - - setProps({ open: false }); - expect(queryByRole('dialog')).to.equal(null); - }); - - it('should remove the popup when there is no exit animation defined', async ({ skip }) => { - if (isJSDOM) { - skip(); - } - - function Test() { - const [open, setOpen] = React.useState(true); - - return ( -
- - - - - - -
- ); - } - - const { user } = await render(); - - const closeButton = screen.getByText('Close'); - await user.click(closeButton); - - await waitFor(() => { - expect(screen.queryByRole('dialog')).to.equal(null); - }); - }); - - it('should remove the popup when the animation finishes', async ({ skip }) => { - if (isJSDOM) { - skip(); - } - - globalThis.BASE_UI_ANIMATIONS_DISABLED = false; - - let animationFinished = false; - const notifyAnimationFinished = () => { - animationFinished = true; - }; - - function Test() { - const style = ` - @keyframes test-anim { - to { - opacity: 0; - } - } - - .animation-test-popup[data-open] { - opacity: 1; - } - - .animation-test-popup[data-ending-style] { - animation: test-anim 1ms; - } - `; - - const [open, setOpen] = React.useState(true); - - return ( -
- {/* eslint-disable-next-line react/no-danger */} -