diff --git a/docs/data/api/select-trigger.json b/docs/data/api/select-trigger.json
index 92239a7615..f816e4ed5e 100644
--- a/docs/data/api/select-trigger.json
+++ b/docs/data/api/select-trigger.json
@@ -11,7 +11,10 @@
"import { Select } from '@base_ui/react/Select';\nconst SelectTrigger = Select.Trigger;"
],
"classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
"muiName": "SelectTrigger",
+ "forwardsRefTo": "HTMLDivElement",
"filename": "/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx",
"inheritance": null,
"demos": "
",
diff --git a/packages/mui-base/src/Menu/Trigger/MenuTrigger.test.tsx b/packages/mui-base/src/Menu/Trigger/MenuTrigger.test.tsx
index 836cb1488e..3c199cc104 100644
--- a/packages/mui-base/src/Menu/Trigger/MenuTrigger.test.tsx
+++ b/packages/mui-base/src/Menu/Trigger/MenuTrigger.test.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { expect } from 'chai';
import { FloatingRootContext, FloatingTree } from '@floating-ui/react';
import userEvent from '@testing-library/user-event';
-import { act } from '@mui/internal-test-utils';
+import { act, screen } from '@mui/internal-test-utils';
import { Menu } from '@base_ui/react/Menu';
import { describeConformance, createRenderer } from '#test-utils';
import { MenuRootContext } from '../Root/MenuRootContext';
@@ -164,4 +164,23 @@ describe('', () => {
expect(button).to.have.attribute('aria-expanded', 'true');
});
});
+
+ describe('style hooks', () => {
+ it('should have the data-popup-open and data-pressed attributes when open', async () => {
+ await render(
+
+
+ ,
+ );
+
+ const trigger = screen.getByRole('button');
+
+ await act(async () => {
+ trigger.click();
+ });
+
+ expect(trigger).to.have.attribute('data-popup-open');
+ expect(trigger).to.have.attribute('data-pressed');
+ });
+ });
});
diff --git a/packages/mui-base/src/Menu/Trigger/MenuTrigger.tsx b/packages/mui-base/src/Menu/Trigger/MenuTrigger.tsx
index 4c800e0957..eb0ba70bef 100644
--- a/packages/mui-base/src/Menu/Trigger/MenuTrigger.tsx
+++ b/packages/mui-base/src/Menu/Trigger/MenuTrigger.tsx
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { useFloatingTree } from '@floating-ui/react';
import { useMenuTrigger } from './useMenuTrigger';
import { useMenuRootContext } from '../Root/MenuRootContext';
-import { triggerOpenStateMapping } from '../../utils/popupOpenStateMapping';
+import { pressableTriggerOpenStateMapping } from '../../utils/popupOpenStateMapping';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
import { BaseUIComponentProps } from '../../utils/types';
@@ -52,7 +52,7 @@ const MenuTrigger = React.forwardRef(function MenuTrigger(
className,
ownerState,
propGetter: (externalProps) => getTriggerProps(getRootProps(externalProps)),
- customStyleHookMapping: triggerOpenStateMapping,
+ customStyleHookMapping: pressableTriggerOpenStateMapping,
extraProps: other,
});
diff --git a/packages/mui-base/src/Popover/Root/PopoverRoot.tsx b/packages/mui-base/src/Popover/Root/PopoverRoot.tsx
index 7a52ac417a..354645c25b 100644
--- a/packages/mui-base/src/Popover/Root/PopoverRoot.tsx
+++ b/packages/mui-base/src/Popover/Root/PopoverRoot.tsx
@@ -40,6 +40,7 @@ const PopoverRoot: React.FC = function PopoverRoot(props) {
descriptionId,
setDescriptionId,
openMethod,
+ openReason,
} = usePopoverRoot({
openOnHover,
delay: delayWithDefault,
@@ -73,6 +74,7 @@ const PopoverRoot: React.FC = function PopoverRoot(props) {
getRootPopupProps,
getRootTriggerProps,
openMethod,
+ openReason,
}),
[
openOnHover,
@@ -96,6 +98,7 @@ const PopoverRoot: React.FC = function PopoverRoot(props) {
getRootPopupProps,
getRootTriggerProps,
openMethod,
+ openReason,
],
);
diff --git a/packages/mui-base/src/Popover/Root/PopoverRootContext.ts b/packages/mui-base/src/Popover/Root/PopoverRootContext.ts
index 9efaa21b9d..7850e5e252 100644
--- a/packages/mui-base/src/Popover/Root/PopoverRootContext.ts
+++ b/packages/mui-base/src/Popover/Root/PopoverRootContext.ts
@@ -27,6 +27,7 @@ export interface PopoverRootContext {
getRootTriggerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
getRootPopupProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
openMethod: InteractionType | null;
+ openReason: OpenChangeReason | null;
}
export const PopoverRootContext = React.createContext(undefined);
diff --git a/packages/mui-base/src/Popover/Root/usePopoverRoot.ts b/packages/mui-base/src/Popover/Root/usePopoverRoot.ts
index d91198215c..4368f9173d 100644
--- a/packages/mui-base/src/Popover/Root/usePopoverRoot.ts
+++ b/packages/mui-base/src/Popover/Root/usePopoverRoot.ts
@@ -42,6 +42,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo
const [descriptionId, setDescriptionId] = React.useState();
const [triggerElement, setTriggerElement] = React.useState(null);
const [positionerElement, setPositionerElement] = React.useState(null);
+ const [openReason, setOpenReason] = React.useState(null);
const popupRef = React.useRef(null);
@@ -69,6 +70,12 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo
setMounted(false);
}
}
+
+ if (nextOpen) {
+ setOpenReason(reason ?? null);
+ } else {
+ setOpenReason(null);
+ }
},
);
@@ -138,6 +145,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo
floatingRootContext: context,
instantType,
openMethod,
+ openReason,
}),
[
mounted,
@@ -154,6 +162,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo
instantType,
openMethod,
triggerProps,
+ openReason,
],
);
}
@@ -222,5 +231,6 @@ export namespace usePopoverRoot {
setPositionerElement: React.Dispatch>;
popupRef: React.RefObject;
openMethod: InteractionType | null;
+ openReason: OpenChangeReason | null;
}
}
diff --git a/packages/mui-base/src/Popover/Trigger/PopoverTrigger.test.tsx b/packages/mui-base/src/Popover/Trigger/PopoverTrigger.test.tsx
index 9bb0c77738..53b6e382f0 100644
--- a/packages/mui-base/src/Popover/Trigger/PopoverTrigger.test.tsx
+++ b/packages/mui-base/src/Popover/Trigger/PopoverTrigger.test.tsx
@@ -1,6 +1,8 @@
import * as React from 'react';
import { Popover } from '@base_ui/react/Popover';
import { createRenderer, describeConformance } from '#test-utils';
+import { expect } from 'chai';
+import { act, screen } from '@mui/internal-test-utils';
describe('', () => {
const { render } = createRenderer();
@@ -15,4 +17,56 @@ describe('', () => {
);
},
}));
+
+ describe('style hooks', () => {
+ it('should have the data-popup-open and data-pressed attributes when open by clicking', async () => {
+ await render(
+
+
+ ,
+ );
+
+ const trigger = screen.getByRole('button');
+
+ await act(async () => {
+ trigger.click();
+ });
+
+ expect(trigger).to.have.attribute('data-popup-open');
+ expect(trigger).to.have.attribute('data-pressed');
+ });
+
+ it('should have the data-popup-open but not the data-pressed attribute when open by hover', async () => {
+ const { user } = await render(
+
+
+ ,
+ );
+
+ const trigger = screen.getByRole('button');
+
+ await user.hover(trigger);
+
+ expect(trigger).to.have.attribute('data-popup-open');
+ expect(trigger).not.to.have.attribute('data-pressed');
+ });
+
+ it('should have the data-popup-open and data-pressed attributes when open by click when `openOnHover=true`', async () => {
+ const { user } = await render(
+
+
+ ,
+ );
+
+ const trigger = screen.getByRole('button');
+
+ await user.hover(trigger);
+ await act(async () => {
+ trigger.click();
+ });
+
+ expect(trigger).to.have.attribute('data-popup-open');
+ expect(trigger).to.have.attribute('data-pressed');
+ });
+ });
});
diff --git a/packages/mui-base/src/Popover/Trigger/PopoverTrigger.tsx b/packages/mui-base/src/Popover/Trigger/PopoverTrigger.tsx
index 42906c985f..1d9057c2d1 100644
--- a/packages/mui-base/src/Popover/Trigger/PopoverTrigger.tsx
+++ b/packages/mui-base/src/Popover/Trigger/PopoverTrigger.tsx
@@ -5,7 +5,11 @@ import { usePopoverRootContext } from '../Root/PopoverRootContext';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
import { useForkRef } from '../../utils/useForkRef';
import type { BaseUIComponentProps } from '../../utils/types';
-import { triggerOpenStateMapping } from '../../utils/popupOpenStateMapping';
+import {
+ triggerOpenStateMapping,
+ pressableTriggerOpenStateMapping,
+} from '../../utils/popupOpenStateMapping';
+import { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
/**
* Renders a trigger element that opens the popover.
@@ -24,12 +28,25 @@ const PopoverTrigger = React.forwardRef(function PopoverTrigger(
) {
const { render, className, ...otherProps } = props;
- const { open, setTriggerElement, getRootTriggerProps } = usePopoverRootContext();
+ const { open, setTriggerElement, getRootTriggerProps, openReason } = usePopoverRootContext();
const ownerState: PopoverTrigger.OwnerState = React.useMemo(() => ({ open }), [open]);
const mergedRef = useForkRef(forwardedRef, setTriggerElement);
+ const customStyleHookMapping: CustomStyleHookMapping<{ open: boolean }> = React.useMemo(
+ () => ({
+ open(value) {
+ if (value && openReason === 'click') {
+ return pressableTriggerOpenStateMapping.open(value);
+ }
+
+ return triggerOpenStateMapping.open(value);
+ },
+ }),
+ [openReason],
+ );
+
const { renderElement } = useComponentRenderer({
propGetter: getRootTriggerProps,
render: render ?? 'button',
@@ -37,7 +54,7 @@ const PopoverTrigger = React.forwardRef(function PopoverTrigger(
ownerState,
ref: mergedRef,
extraProps: otherProps,
- customStyleHookMapping: triggerOpenStateMapping,
+ customStyleHookMapping,
});
return renderElement();
diff --git a/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx b/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx
new file mode 100644
index 0000000000..f4c0b1e8c9
--- /dev/null
+++ b/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx
@@ -0,0 +1,39 @@
+import * as React from 'react';
+import { Select } from '@base_ui/react/Select';
+import { createRenderer, describeConformance } from '#test-utils';
+import { expect } from 'chai';
+import { act, screen } from '@mui/internal-test-utils';
+
+describe('', () => {
+ const { render } = createRenderer();
+
+ describeConformance(, () => ({
+ refInstanceof: window.HTMLDivElement,
+ render(node) {
+ return render(
+
+ {node}
+ ,
+ );
+ },
+ }));
+
+ describe('style hooks', () => {
+ it('should have the data-popup-open and data-pressed attributes when open', async () => {
+ await render(
+
+
+ ,
+ );
+
+ const trigger = screen.getByRole('combobox');
+
+ await act(async () => {
+ trigger.click();
+ });
+
+ expect(trigger).to.have.attribute('data-popup-open');
+ expect(trigger).to.have.attribute('data-pressed');
+ });
+ });
+});
diff --git a/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx
index 909706b9ad..856ac0dbe5 100644
--- a/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx
+++ b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx
@@ -6,7 +6,7 @@ import { useSelectRootContext } from '../Root/SelectRootContext';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
import { BaseUIComponentProps } from '../../utils/types';
import { useFieldRootContext } from '../../Field/Root/FieldRootContext';
-import { triggerOpenStateMapping } from '../../utils/popupOpenStateMapping';
+import { pressableTriggerOpenStateMapping } from '../../utils/popupOpenStateMapping';
/**
*
@@ -55,7 +55,7 @@ const SelectTrigger = React.forwardRef(function SelectTrigger(
className,
ownerState,
propGetter: (externalProps) => getTriggerProps(getRootTriggerProps(externalProps)),
- customStyleHookMapping: triggerOpenStateMapping,
+ customStyleHookMapping: pressableTriggerOpenStateMapping,
extraProps: otherProps,
});
diff --git a/packages/mui-base/src/utils/popupOpenStateMapping.ts b/packages/mui-base/src/utils/popupOpenStateMapping.ts
index a17d2248f4..07e4ef00fe 100644
--- a/packages/mui-base/src/utils/popupOpenStateMapping.ts
+++ b/packages/mui-base/src/utils/popupOpenStateMapping.ts
@@ -1,23 +1,41 @@
import type { CustomStyleHookMapping } from './getStyleHookProps';
-export const triggerOpenStateMapping: CustomStyleHookMapping<{ open: boolean }> = {
+const TRIGGER_HOOK = {
+ 'data-popup-open': '',
+};
+
+const PRESSABLE_TRIGGER_HOOK = {
+ 'data-popup-open': '',
+ 'data-pressed': '',
+};
+
+const POPUP_HOOK = {
+ 'data-open': '',
+};
+
+export const triggerOpenStateMapping = {
open(value) {
if (value) {
- return {
- 'data-popup-open': '',
- };
+ return TRIGGER_HOOK;
}
return null;
},
-};
+} satisfies CustomStyleHookMapping<{ open: boolean }>;
-export const popupOpenStateMapping: CustomStyleHookMapping<{ open: boolean }> = {
+export const pressableTriggerOpenStateMapping = {
open(value) {
if (value) {
- return {
- 'data-open': '',
- };
+ return PRESSABLE_TRIGGER_HOOK;
}
return null;
},
-};
+} satisfies CustomStyleHookMapping<{ open: boolean }>;
+
+export const popupOpenStateMapping = {
+ open(value) {
+ if (value) {
+ return POPUP_HOOK;
+ }
+ return null;
+ },
+} satisfies CustomStyleHookMapping<{ open: boolean }>;