diff --git a/docs/data/api/alert-dialog-popup.json b/docs/data/api/alert-dialog-popup.json
index 902e31c78..472c6acc3 100644
--- a/docs/data/api/alert-dialog-popup.json
+++ b/docs/data/api/alert-dialog-popup.json
@@ -2,6 +2,7 @@
"props": {
"className": { "type": { "name": "union", "description": "func | string" } },
"container": { "type": { "name": "union", "description": "HTML element | ref" } },
+ "initialFocus": { "type": { "name": "union", "description": "func | ref" } },
"keepMounted": { "type": { "name": "bool" }, "default": "false" },
"render": { "type": { "name": "union", "description": "element | func" } }
},
diff --git a/docs/data/api/dialog-popup.json b/docs/data/api/dialog-popup.json
index 4bb1be663..d2ae6b679 100644
--- a/docs/data/api/dialog-popup.json
+++ b/docs/data/api/dialog-popup.json
@@ -2,6 +2,7 @@
"props": {
"className": { "type": { "name": "union", "description": "func | string" } },
"container": { "type": { "name": "union", "description": "HTML element | ref" } },
+ "initialFocus": { "type": { "name": "union", "description": "func | ref" } },
"keepMounted": { "type": { "name": "bool" }, "default": "false" },
"render": { "type": { "name": "union", "description": "element | func" } }
},
diff --git a/docs/data/api/popover-positioner.json b/docs/data/api/popover-positioner.json
index dc1d42a20..798223f68 100644
--- a/docs/data/api/popover-positioner.json
+++ b/docs/data/api/popover-positioner.json
@@ -32,6 +32,7 @@
},
"container": { "type": { "name": "union", "description": "HTML element | func" } },
"hideWhenDetached": { "type": { "name": "bool" }, "default": "false" },
+ "initialFocus": { "type": { "name": "union", "description": "func | ref" } },
"keepMounted": { "type": { "name": "bool" }, "default": "false" },
"positionMethod": {
"type": { "name": "enum", "description": "'absolute' | 'fixed'" },
diff --git a/docs/data/translations/api-docs/alert-dialog-popup/alert-dialog-popup.json b/docs/data/translations/api-docs/alert-dialog-popup/alert-dialog-popup.json
index e91f99d7c..4f0c02e6a 100644
--- a/docs/data/translations/api-docs/alert-dialog-popup/alert-dialog-popup.json
+++ b/docs/data/translations/api-docs/alert-dialog-popup/alert-dialog-popup.json
@@ -5,6 +5,9 @@
"description": "Class names applied to the element or a function that returns them based on the component's state."
},
"container": { "description": "The container element to which the popup is appended to." },
+ "initialFocus": {
+ "description": "Determines an element to focus when the dialog is opened. It can be either a ref to the element or a function that returns such a ref. If not provided, the first focusable element is focused."
+ },
"keepMounted": {
"description": "If true, the dialog element is kept in the DOM when closed."
},
diff --git a/docs/data/translations/api-docs/dialog-popup/dialog-popup.json b/docs/data/translations/api-docs/dialog-popup/dialog-popup.json
index e91f99d7c..4f0c02e6a 100644
--- a/docs/data/translations/api-docs/dialog-popup/dialog-popup.json
+++ b/docs/data/translations/api-docs/dialog-popup/dialog-popup.json
@@ -5,6 +5,9 @@
"description": "Class names applied to the element or a function that returns them based on the component's state."
},
"container": { "description": "The container element to which the popup is appended to." },
+ "initialFocus": {
+ "description": "Determines an element to focus when the dialog is opened. It can be either a ref to the element or a function that returns such a ref. If not provided, the first focusable element is focused."
+ },
"keepMounted": {
"description": "If true, the dialog element is kept in the DOM when closed."
},
diff --git a/docs/data/translations/api-docs/popover-positioner/popover-positioner.json b/docs/data/translations/api-docs/popover-positioner/popover-positioner.json
index a3e4cceb4..e21cb08fe 100644
--- a/docs/data/translations/api-docs/popover-positioner/popover-positioner.json
+++ b/docs/data/translations/api-docs/popover-positioner/popover-positioner.json
@@ -24,6 +24,9 @@
"hideWhenDetached": {
"description": "Whether the popover element is hidden if it appears detached from its anchor element due to the anchor element being clipped (or hidden) from view."
},
+ "initialFocus": {
+ "description": "Determines an element to focus when the popover is opened. It can be either a ref to the element or a function that returns such a ref. If not provided, the first focusable element is focused."
+ },
"keepMounted": {
"description": "Whether the popover remains mounted in the DOM while closed."
},
diff --git a/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx b/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx
index 8f5d9f729..4d70908f4 100644
--- a/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx
+++ b/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx
@@ -10,6 +10,8 @@ import type { BaseUIComponentProps } from '../../utils/types';
import type { TransitionStatus } from '../../utils/useTransitionStatus';
import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
import { popupOpenStateMapping as baseMapping } from '../../utils/popupOpenStateMapping';
+import { useForkRef } from '../../utils/useForkRef';
+import { PointerType } from '../../utils/useEnhancedClickHandler';
const customStyleHookMapping: CustomStyleHookMapping = {
...baseMapping,
@@ -39,18 +41,23 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup(
props: AlertDialogPopup.Props,
forwardedRef: React.ForwardedRef,
) {
- const { className, container, id, keepMounted = false, render, ...other } = props;
+ const { className, container, id, keepMounted = false, render, initialFocus, ...other } = props;
const rootContext = useAlertDialogRootContext();
const { open, nestedOpenDialogCount } = rootContext;
- const { getRootProps, floatingContext, mounted, transitionStatus } = useDialogPopup({
- id,
- ref: forwardedRef,
- dismissible: false,
- isTopmost: nestedOpenDialogCount === 0,
- ...rootContext,
- });
+ const popupRef = React.useRef(null);
+ const mergedRef = useForkRef(forwardedRef, popupRef);
+
+ const { getRootProps, floatingContext, mounted, transitionStatus, resolvedInitialFocus } =
+ useDialogPopup({
+ id,
+ ref: mergedRef,
+ isTopmost: nestedOpenDialogCount === 0,
+ dismissible: false,
+ initialFocus,
+ ...rootContext,
+ });
const ownerState: AlertDialogPopup.OwnerState = React.useMemo(
() => ({
@@ -80,7 +87,12 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup(
return (
-
+
{renderElement()}
@@ -99,6 +111,14 @@ namespace AlertDialogPopup {
* @default false
*/
keepMounted?: boolean;
+ /**
+ * Determines an element to focus when the dialog is opened.
+ * It can be either a ref to the element or a function that returns such a ref.
+ * If not provided, the first focusable element is focused.
+ */
+ initialFocus?:
+ | React.RefObject
+ | ((pointerType: PointerType) => React.RefObject);
}
export interface OwnerState {
@@ -129,6 +149,15 @@ AlertDialogPopup.propTypes /* remove-proptypes */ = {
* @ignore
*/
id: PropTypes.string,
+ /**
+ * Determines an element to focus when the dialog is opened.
+ * It can be either a ref to the element or a function that returns such a ref.
+ * If not provided, the first focusable element is focused.
+ */
+ initialFocus: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
+ PropTypes.func,
+ refType,
+ ]),
/**
* If `true`, the dialog element is kept in the DOM when closed.
*
diff --git a/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.test.ts b/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.test.ts
deleted file mode 100644
index 7d85215e8..000000000
--- a/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.test.ts
+++ /dev/null
@@ -1 +0,0 @@
-// This file must be present for the doc gen to work
diff --git a/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.test.tsx b/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.test.tsx
new file mode 100644
index 000000000..61ba8a08c
--- /dev/null
+++ b/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.test.tsx
@@ -0,0 +1,115 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { act, waitFor } from '@mui/internal-test-utils';
+import { AlertDialog } from '@base_ui/react/AlertDialog';
+import { createRenderer } from '#test-utils';
+
+describe('', () => {
+ const { render } = createRenderer();
+
+ describe('prop: initial focus', () => {
+ before(function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+ });
+
+ it('should focus the first focusable element within the popup by default', async () => {
+ const { getByText, getByTestId } = await render(
+
+
+
+
+ Open
+
+
+
+
+
+
+
,
+ );
+
+ const trigger = getByText('Open');
+ await act(async () => {
+ trigger.click();
+ });
+
+ await waitFor(() => {
+ const dialogInput = getByTestId('dialog-input');
+ expect(dialogInput).to.toHaveFocus();
+ });
+ });
+ });
+
+ it('should focus the element provided to `initialFocus` as a ref when open', async () => {
+ function TestComponent() {
+ const input2Ref = React.useRef(null);
+ return (
+