Skip to content

Commit

Permalink
Merge branch 'master' into popup-keepMounted
Browse files Browse the repository at this point in the history
  • Loading branch information
atomiks authored Nov 12, 2024
2 parents 6c2d0cd + 88d593e commit c1e163f
Show file tree
Hide file tree
Showing 37 changed files with 955 additions and 728 deletions.
1 change: 1 addition & 0 deletions docs/data/api/alert-dialog-popup.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"props": {
"className": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;string" } },
"container": { "type": { "name": "union", "description": "HTML element<br>&#124;&nbsp;ref" } },
"finalFocus": { "type": { "name": "custom", "description": "ref" } },
"initialFocus": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;ref" } },
"keepMounted": { "type": { "name": "bool" }, "default": "false" },
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } }
Expand Down
1 change: 1 addition & 0 deletions docs/data/api/dialog-popup.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"props": {
"className": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;string" } },
"container": { "type": { "name": "union", "description": "HTML element<br>&#124;&nbsp;ref" } },
"finalFocus": { "type": { "name": "custom", "description": "ref" } },
"initialFocus": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;ref" } },
"keepMounted": { "type": { "name": "bool" }, "default": "false" },
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } }
Expand Down
1 change: 1 addition & 0 deletions docs/data/api/popover-positioner.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"default": "5"
},
"container": { "type": { "name": "union", "description": "HTML element<br>&#124;&nbsp;func" } },
"finalFocus": { "type": { "name": "custom", "description": "ref" } },
"hideWhenDetached": { "type": { "name": "bool" }, "default": "false" },
"initialFocus": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;ref" } },
"keepMounted": { "type": { "name": "bool" }, "default": "false" },
Expand Down
2 changes: 0 additions & 2 deletions docs/data/components/field/UnstyledFieldServerError.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ export default function UnstyledFieldServerError() {
<FieldSubmit
type="submit"
aria-disabled={status === 'loading'}
// The aria-description attribute is not a standard ARIA attribute (it's defined in ARIA 1.3 Editor's Draft).
// eslint-disable-next-line jsx-a11y/aria-props
aria-description={
!state.validity.valid ? 'Field has errors' : undefined
}
Expand Down
2 changes: 0 additions & 2 deletions docs/data/components/field/UnstyledFieldServerError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ export default function UnstyledFieldServerError() {
<FieldSubmit
type="submit"
aria-disabled={status === 'loading'}
// The aria-description attribute is not a standard ARIA attribute (it's defined in ARIA 1.3 Editor's Draft).
// eslint-disable-next-line jsx-a11y/aria-props
aria-description={
!state.validity.valid ? 'Field has errors' : undefined
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
},
"container": { "description": "The container element to which the popup is appended to." },
"finalFocus": {
"description": "Determines an element to focus after the dialog is closed. If not provided, the focus returns to the trigger."
},
"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."
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
},
"container": { "description": "The container element to which the popup is appended to." },
"finalFocus": {
"description": "Determines an element to focus after the dialog is closed. If not provided, the focus returns to the trigger."
},
"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."
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"componentDescription": "",
"componentDescription": "Renders a trigger element that opens the Dialog.",
"propDescriptions": {
"className": {
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"description": "The padding between the popover element and the edges of the collision boundary to add whitespace between them to prevent them from touching."
},
"container": { "description": "The element the popover positioner element is appended to." },
"finalFocus": {
"description": "Determines an element to focus after the popover is closed. If not provided, the focus returns to the trigger."
},
"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."
},
Expand Down
6 changes: 3 additions & 3 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@
"postcss": "^8.4.47",
"postcss-import": "^16.1.0",
"prop-types": "^15.8.1",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"react": "19.0.0-rc-fb9a90fa48-20240614",
"react-dom": "19.0.0-rc-fb9a90fa48-20240614",
"react-error-boundary": "^4.0.13",
"react-is": "19.0.0-rc-69d4b800-20241021",
"react-is": "19.0.0-rc-fb9a90fa48-20240614",
"react-router-dom": "^6.23.1",
"react-runner": "^1.0.5",
"react-simple-code-editor": "^0.13.1",
Expand Down
1 change: 1 addition & 0 deletions docs/src/components/ApiReference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function ApiReference(props: ApiReferenceProps) {
<code>{prop.type.name}</code>
</td>
<td>{prop.defaultValue != null ? <code>{prop.defaultValue}</code> : null}</td>
{/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
<td dangerouslySetInnerHTML={{ __html: prop.description }} />
</tr>
))}
Expand Down
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"inline-scripts": "tsx ./scripts/inlineScripts.mts"
},
"devDependencies": {
"@argos-ci/core": "^2.8.1",
"@argos-ci/core": "^2.10.0",
"@babel/cli": "^7.25.9",
"@babel/core": "^7.26.0",
"@babel/node": "^7.26.0",
Expand All @@ -85,7 +85,7 @@
"@mui/internal-test-utils": "https://pkg.csb.dev/mui/material-ui/commit/92c23999/@mui/internal-test-utils",
"@mui/monorepo": "github:mui/material-ui#v6.1.6",
"@mui/utils": "6.1.6",
"@next/eslint-plugin-next": "^14.2.13",
"@next/eslint-plugin-next": "^14.2.17",
"@octokit/rest": "^20.1.1",
"@playwright/test": "1.48.2",
"@tailwindcss/postcss": "4.0.0-alpha.30",
Expand All @@ -95,10 +95,10 @@
"@types/node": "^18.19.63",
"@types/react": "npm:[email protected]",
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-react": "^4.3.3",
"@vitest/browser": "^2.1.3",
"@vitest/browser": "^2.1.4",
"@vitest/coverage-v8": "^2.1.4",
"@vitest/ui": "2.1.4",
"babel-loader": "^9.2.1",
Expand All @@ -125,13 +125,13 @@
"eslint-import-resolver-webpack": "^0.13.9",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-material-ui": "workspace:^",
"eslint-plugin-mocha": "^10.5.0",
"eslint-plugin-react": "^7.37.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-testing-library": "^6.3.0",
"eslint-plugin-testing-library": "^6.4.0",
"execa": "^8.0.1",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
Expand All @@ -158,9 +158,9 @@
"prettier-plugin-tailwindcss": "^0.6.8",
"pretty-quick": "^4.0.0",
"process": "^0.11.10",
"react": "19.0.0-rc-69d4b800-20241021",
"react": "19.0.0-rc-fb9a90fa48-20240614",
"react-docgen": "^5.4.3",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-fb9a90fa48-20240614",
"recast": "^0.23.9",
"remark": "^15.0.1",
"rimraf": "^5.0.10",
Expand All @@ -174,7 +174,7 @@
"tsx": "^4.8.2",
"typescript": "^5.6.3",
"unist-util-visit": "^5.0.0",
"vitest": "^2.1.3",
"vitest": "^2.1.4",
"webpack": "^5.91.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4",
Expand Down
4 changes: 2 additions & 2 deletions packages/mui-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
"chai": "^4.5.0",
"fast-glob": "^3.3.2",
"lodash": "^4.17.21",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"react": "19.0.0-rc-fb9a90fa48-20240614",
"react-dom": "19.0.0-rc-fb9a90fa48-20240614",
"sinon": "^19.0.2",
"typescript": "^5.6.3"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,12 @@ const AlertDialogBackdrop = React.forwardRef(function AlertDialogBackdrop(
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const { render, className, keepMounted = false, ...other } = props;
const { open, hasParentDialog, setBackdropPresent, animated } = useAlertDialogRootContext();

const handleMount = React.useCallback(() => setBackdropPresent(true), [setBackdropPresent]);
const handleUnmount = React.useCallback(() => setBackdropPresent(false), [setBackdropPresent]);
const { open, hasParentDialog, animated } = useAlertDialogRootContext();

const { getRootProps, mounted, transitionStatus } = useDialogBackdrop({
animated,
open,
ref: forwardedRef,
onMount: handleMount,
onUnmount: handleUnmount,
});

const ownerState: AlertDialogBackdrop.OwnerState = { open, transitionStatus };
Expand Down
63 changes: 63 additions & 0 deletions packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,67 @@ describe('<AlertDialog.Popup />', () => {
});
});
});

describe('prop: final focus', () => {
it('should focus the trigger by default when closed', async () => {
const { getByText, user } = await render(
<div>
<input />
<AlertDialog.Root animated={false}>
<AlertDialog.Backdrop />
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
<AlertDialog.Popup>
<AlertDialog.Close>Close</AlertDialog.Close>
</AlertDialog.Popup>
</AlertDialog.Root>
<input />
</div>,
);

const trigger = getByText('Open');
await user.click(trigger);

const closeButton = getByText('Close');
await user.click(closeButton);

await waitFor(() => {
expect(trigger).toHaveFocus();
});
});

it('should focus the element provided to the prop when closed', async () => {
function TestComponent() {
const inputRef = React.useRef<HTMLInputElement>(null);
return (
<div>
<input />
<AlertDialog.Root animated={false}>
<AlertDialog.Backdrop />
<AlertDialog.Trigger>Open</AlertDialog.Trigger>
<AlertDialog.Popup finalFocus={inputRef}>
<AlertDialog.Close>Close</AlertDialog.Close>
</AlertDialog.Popup>
</AlertDialog.Root>
<input />
<input data-testid="input-to-focus" ref={inputRef} />
<input />
</div>
);
}

const { getByText, getByTestId, user } = await render(<TestComponent />);

const trigger = getByText('Open');
await user.click(trigger);

const closeButton = getByText('Close');
await user.click(closeButton);

const inputToFocus = getByTestId('input-to-focus');

await waitFor(() => {
expect(inputToFocus).toHaveFocus();
});
});
});
});
65 changes: 52 additions & 13 deletions packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,51 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup(
props: AlertDialogPopup.Props,
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const { className, container, id, keepMounted = false, render, initialFocus, ...other } = props;
const {
className,
container,
id,
keepMounted = false,
render,
initialFocus,
finalFocus,
...other
} = props;

const rootContext = useAlertDialogRootContext();
const { open, nestedOpenDialogCount } = rootContext;
const {
descriptionElementId,
floatingRootContext,
getPopupProps,
mounted,
nestedOpenDialogCount,
onOpenChange,
open,
openMethod,
popupRef,
setPopupElement,
setPopupElementId,
titleElementId,
transitionStatus,
} = useAlertDialogRootContext();

const popupRef = React.useRef<HTMLElement | null>(null);
const mergedRef = useForkRef(forwardedRef, popupRef);

const { getRootProps, floatingContext, mounted, transitionStatus, resolvedInitialFocus } =
useDialogPopup({
id,
ref: mergedRef,
isTopmost: nestedOpenDialogCount === 0,
dismissible: false,
initialFocus,
...rootContext,
});
const { getRootProps, floatingContext, resolvedInitialFocus } = useDialogPopup({
descriptionElementId,
floatingRootContext,
getPopupProps,
id,
initialFocus,
modal: true,
mounted,
onOpenChange,
open,
openMethod,
ref: mergedRef,
setPopupElement,
setPopupElementId,
titleElementId,
});

const ownerState: AlertDialogPopup.OwnerState = React.useMemo(
() => ({
Expand Down Expand Up @@ -92,6 +120,7 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup(
modal
disabled={!mounted}
initialFocus={resolvedInitialFocus}
returnFocus={finalFocus}
>
{renderElement()}
</FloatingFocusManager>
Expand Down Expand Up @@ -119,6 +148,11 @@ namespace AlertDialogPopup {
initialFocus?:
| React.RefObject<HTMLElement | null>
| ((interactionType: InteractionType) => React.RefObject<HTMLElement | null>);
/**
* Determines an element to focus after the dialog is closed.
* If not provided, the focus returns to the trigger.
*/
finalFocus?: React.RefObject<HTMLElement | null>;
}

export interface OwnerState {
Expand All @@ -145,6 +179,11 @@ AlertDialogPopup.propTypes /* remove-proptypes */ = {
* The container element to which the popup is appended to.
*/
container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([HTMLElementType, refType]),
/**
* Determines an element to focus after the dialog is closed.
* If not provided, the focus returns to the trigger.
*/
finalFocus: refType,
/**
* @ignore
*/
Expand Down
13 changes: 7 additions & 6 deletions packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,21 @@ import { useDialogRoot } from '../../Dialog/Root/useDialogRoot';
* - [AlertDialogRoot API](https://base-ui.netlify.app/components/react-alert-dialog/#api-reference-AlertDialogRoot)
*/
const AlertDialogRoot: React.FC<AlertDialogRoot.Props> = function AlertDialogRoot(props) {
const { children, defaultOpen, onOpenChange, open: openProp, animated = true } = props;
const { animated = true, children, defaultOpen, onOpenChange, open } = props;

const dialogRootContext = React.useContext(AlertDialogRootContext);
const parentDialogRootContext = React.useContext(AlertDialogRootContext);

const dialogRoot = useDialogRoot({
open: openProp,
open,
defaultOpen,
onOpenChange,
modal: true,
onNestedDialogClose: dialogRootContext?.onNestedDialogClose,
onNestedDialogOpen: dialogRootContext?.onNestedDialogOpen,
dismissible: false,
onNestedDialogClose: parentDialogRootContext?.onNestedDialogClose,
onNestedDialogOpen: parentDialogRootContext?.onNestedDialogOpen,
});

const hasParentDialog = Boolean(dialogRootContext);
const hasParentDialog = Boolean(parentDialogRootContext);

const contextValue = React.useMemo(
() => ({ ...dialogRoot, hasParentDialog, animated }),
Expand Down
Loading

0 comments on commit c1e163f

Please sign in to comment.