From 577dcfc4bc3ba906243f75f1f3c027274ff1f797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Mon, 29 Apr 2024 12:32:21 +0200 Subject: [PATCH 01/92] Wrap the native dialog API --- docs/pages/experiments/dialog.tsx | 20 +++++++++ packages/mui-base/src/Dialog/Dialog.tsx | 59 +++++++++++++++++++++++++ packages/mui-base/src/Dialog/index.ts | 1 + 3 files changed, 80 insertions(+) create mode 100644 docs/pages/experiments/dialog.tsx create mode 100644 packages/mui-base/src/Dialog/Dialog.tsx create mode 100644 packages/mui-base/src/Dialog/index.ts diff --git a/docs/pages/experiments/dialog.tsx b/docs/pages/experiments/dialog.tsx new file mode 100644 index 000000000..b9725f7e7 --- /dev/null +++ b/docs/pages/experiments/dialog.tsx @@ -0,0 +1,20 @@ +import * as Dialog from '@base_ui/react/Dialog'; +import React from 'react'; + +export default function DialogExperiment() { + const [state, setState] = React.useState({ open: false }); + + return ( +
+ + + {}}> +

Dialog content

+ +
+ +
+
+
+ ); +} diff --git a/packages/mui-base/src/Dialog/Dialog.tsx b/packages/mui-base/src/Dialog/Dialog.tsx new file mode 100644 index 000000000..86fa609c7 --- /dev/null +++ b/packages/mui-base/src/Dialog/Dialog.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { useForkRef } from '../utils/useForkRef'; + +export type DialogOpenState = 'closed' | 'open' | 'openModal'; + +export interface DialogRootProps { + open?: boolean; + modal?: boolean; + children?: React.ReactNode; + onClosed?: () => void; +} + +const defaultRender = (props: React.ComponentPropsWithRef<'dialog'>) => ; + +const DialogRoot = React.forwardRef(function DialogRoot( + props: DialogRootProps, + forwardedRef: React.Ref, +) { + const { open, modal, onClosed, ...other } = props; + + const ref = React.useRef(null); + const handleRef = useForkRef(ref, forwardedRef); + + React.useEffect(() => { + if (!open) { + ref.current?.close(); + return; + } + + if (modal) { + ref.current?.showModal(); + } else { + ref.current?.show(); + } + }, [open, modal]); + + const handleCancel = (event: React.SyntheticEvent) => { + event.preventDefault(); + onClosed?.(); + }; + + const handleFormSubmit = (event: React.FormEvent) => { + if ((event.target as HTMLFormElement).method === 'dialog') { + event.preventDefault(); + handleCancel(event); + } + }; + + const outputProps: React.ComponentPropsWithRef<'dialog'> = { + ...other, + onCancel: handleCancel, + onSubmit: handleFormSubmit, + ref: handleRef, + }; + + return defaultRender(outputProps); +}); + +export { DialogRoot }; diff --git a/packages/mui-base/src/Dialog/index.ts b/packages/mui-base/src/Dialog/index.ts new file mode 100644 index 000000000..f5e611906 --- /dev/null +++ b/packages/mui-base/src/Dialog/index.ts @@ -0,0 +1 @@ +export { DialogRoot as Root, type DialogRootProps as RootProps } from './Dialog'; From 774d0608c2144a07374ce3c14ab01b0752ceb4ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Mon, 29 Apr 2024 15:38:29 +0200 Subject: [PATCH 02/92] Experiment --- docs/pages/experiments/dialog.module.css | 52 ++++++++++++++++++++++ docs/pages/experiments/dialog.tsx | 55 +++++++++++++++++++----- packages/mui-base/src/Dialog/Dialog.tsx | 48 +++++++++++++++++---- 3 files changed, 136 insertions(+), 19 deletions(-) create mode 100644 docs/pages/experiments/dialog.module.css diff --git a/docs/pages/experiments/dialog.module.css b/docs/pages/experiments/dialog.module.css new file mode 100644 index 000000000..489d0e6f9 --- /dev/null +++ b/docs/pages/experiments/dialog.module.css @@ -0,0 +1,52 @@ +.page { + max-width: 1000px; + margin: 0 auto; + padding: 16px; +} + +.dialog { + background: #fff; + border: 1px solid #f5f5f5; + min-width: 300px; + max-width: 500px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + padding: 16px; + + &::backdrop { + backdrop-filter: blur(8px); + background: radial-gradient(#cecdcf36, #8b94ab47); + } +} + +.button { + background: #eee; + padding: 8px 16px; + margin: 8px; + border: 1px solid #d8d8d8; + border-radius: 4px; + font-family: inherit; + + &:hover { + background: #ffbf2b; + } + + &:active { + background: #cc9922; + } +} + +.form { + display: flex; + gap: 16px; + margin-top: 24px; + + & > * { + flex: 1; + margin: 0; + } +} + +.nonmodal { + top: 20vh; +} diff --git a/docs/pages/experiments/dialog.tsx b/docs/pages/experiments/dialog.tsx index b9725f7e7..80d873d38 100644 --- a/docs/pages/experiments/dialog.tsx +++ b/docs/pages/experiments/dialog.tsx @@ -1,18 +1,53 @@ +import * as React from 'react'; import * as Dialog from '@base_ui/react/Dialog'; -import React from 'react'; +import clsx from 'clsx'; +import classes from './dialog.module.css'; export default function DialogExperiment() { - const [state, setState] = React.useState({ open: false }); + const [open, setOpen] = React.useState(false); + const [modal, setModal] = React.useState(false); + + function setState(shouldOpen: boolean, shouldBeModal?: boolean) { + return () => { + setOpen(shouldOpen); + if (shouldBeModal !== undefined) { + setModal(shouldBeModal); + } + }; + } return ( -
- - - {}}> -

Dialog content

- -
- +
+ + + +

Dialog

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget sapien id dolor rutrum + porta. Sed enim nulla, placerat eu tincidunt non, ultrices in lectus. Curabitur + pellentesque diam nec ligula hendrerit dapibus. +

+ + + A nested dialog! + + + + +
diff --git a/packages/mui-base/src/Dialog/Dialog.tsx b/packages/mui-base/src/Dialog/Dialog.tsx index 86fa609c7..677b3bbe1 100644 --- a/packages/mui-base/src/Dialog/Dialog.tsx +++ b/packages/mui-base/src/Dialog/Dialog.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import { useForkRef } from '../utils/useForkRef'; export type DialogOpenState = 'closed' | 'open' | 'openModal'; @@ -7,16 +8,17 @@ export interface DialogRootProps { open?: boolean; modal?: boolean; children?: React.ReactNode; - onClosed?: () => void; + onOpenChange?: (open: boolean) => void; } const defaultRender = (props: React.ComponentPropsWithRef<'dialog'>) => ; const DialogRoot = React.forwardRef(function DialogRoot( - props: DialogRootProps, + props: DialogRootProps & React.ComponentPropsWithoutRef<'dialog'>, forwardedRef: React.Ref, ) { - const { open, modal, onClosed, ...other } = props; + const { open = false, modal = true, onOpenChange, ...other } = props; + const previousOpen = React.useRef(open); const ref = React.useRef(null); const handleRef = useForkRef(ref, forwardedRef); @@ -24,25 +26,30 @@ const DialogRoot = React.forwardRef(function DialogRoot( React.useEffect(() => { if (!open) { ref.current?.close(); - return; - } - - if (modal) { + } else if (modal) { + if (previousOpen.current === true) { + ref.current?.close(); + } ref.current?.showModal(); } else { + if (previousOpen.current === true) { + ref.current?.close(); + } ref.current?.show(); } + + previousOpen.current = open; }, [open, modal]); const handleCancel = (event: React.SyntheticEvent) => { event.preventDefault(); - onClosed?.(); + onOpenChange?.(false); }; const handleFormSubmit = (event: React.FormEvent) => { if ((event.target as HTMLFormElement).method === 'dialog') { event.preventDefault(); - handleCancel(event); + onOpenChange?.(false); } }; @@ -56,4 +63,27 @@ const DialogRoot = React.forwardRef(function DialogRoot( return defaultRender(outputProps); }); +DialogRoot.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * @ignore + */ + modal: PropTypes.bool, + /** + * @ignore + */ + onOpenChange: PropTypes.func, + /** + * @ignore + */ + open: PropTypes.bool, +} as any; + export { DialogRoot }; From d58c3ffd1ff12430b1863e9e6e5b3d5fe7f77d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Wed, 1 May 2024 09:27:53 +0200 Subject: [PATCH 03/92] Directory structure --- docs/pages/experiments/dialog.tsx | 4 ---- packages/mui-base/src/Dialog/Close/DialogClose.tsx | 3 +++ .../src/Dialog/Description/DialogDescription.tsx | 3 +++ packages/mui-base/src/Dialog/Popup/DialogPopup.tsx | 3 +++ .../src/Dialog/{Dialog.tsx => Root/DialogRoot.tsx} | 12 ++---------- .../mui-base/src/Dialog/Root/DialogRoot.types.ts | 6 ++++++ packages/mui-base/src/Dialog/Title/DialogTitle.tsx | 3 +++ .../mui-base/src/Dialog/Trigger/DialogTrigger.tsx | 3 +++ packages/mui-base/src/Dialog/index.ts | 3 ++- 9 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 packages/mui-base/src/Dialog/Close/DialogClose.tsx create mode 100644 packages/mui-base/src/Dialog/Description/DialogDescription.tsx create mode 100644 packages/mui-base/src/Dialog/Popup/DialogPopup.tsx rename packages/mui-base/src/Dialog/{Dialog.tsx => Root/DialogRoot.tsx} (90%) create mode 100644 packages/mui-base/src/Dialog/Root/DialogRoot.types.ts create mode 100644 packages/mui-base/src/Dialog/Title/DialogTitle.tsx create mode 100644 packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx diff --git a/docs/pages/experiments/dialog.tsx b/docs/pages/experiments/dialog.tsx index 80d873d38..004df42a3 100644 --- a/docs/pages/experiments/dialog.tsx +++ b/docs/pages/experiments/dialog.tsx @@ -37,10 +37,6 @@ export default function DialogExperiment() { pellentesque diam nec ligula hendrerit dapibus.

- - A nested dialog! - -
+ + + Dialog + This is a sample dialog +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget sapien id dolor rutrum + porta. Sed enim nulla, placerat eu tincidunt non, ultrices in lectus. Curabitur + pellentesque diam nec ligula hendrerit dapibus. +

+ + + + Cancel + +
+
+
+ ); +} + +function ControlledDialogDemo() { const [open, setOpen] = React.useState(false); const [modal, setModal] = React.useState(false); @@ -17,35 +47,45 @@ export default function DialogExperiment() { } return ( -
+
- -

Dialog

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget sapien id dolor rutrum - porta. Sed enim nulla, placerat eu tincidunt non, ultrices in lectus. Curabitur - pellentesque diam nec ligula hendrerit dapibus. -

+ + + Dialog + This is a sample dialog +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget sapien id dolor rutrum + porta. Sed enim nulla, placerat eu tincidunt non, ultrices in lectus. Curabitur + pellentesque diam nec ligula hendrerit dapibus. +

-
- - -
+
+ + +
+
); } + +export default function DialogExperiment() { + return ( +
+

Dialog

+

Uncontrolled

+ +

Controlled

+ +
+ ); +} diff --git a/packages/mui-base/src/Dialog/Close/DialogClose.tsx b/packages/mui-base/src/Dialog/Close/DialogClose.tsx index 333d47d04..675bfa346 100644 --- a/packages/mui-base/src/Dialog/Close/DialogClose.tsx +++ b/packages/mui-base/src/Dialog/Close/DialogClose.tsx @@ -1,3 +1,26 @@ -const DialogClose = React.forwardRef(function DialogClose(props, forwardedRef) {}); +import * as React from 'react'; +import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions'; +import { useDialogRootContext } from '../Root/DialogRootContext'; + +const DialogClose = React.forwardRef(function DialogClose( + props: React.ComponentPropsWithRef<'button'>, + forwardedRef: React.ForwardedRef, +) { + const { open, onOpenChange } = useDialogRootContext(); + + const handleClick = React.useCallback(() => { + if (open) { + onOpenChange?.(false); + } + }, [open, onOpenChange]); + + const rootProps = { + ...props, + onClick: handleClick, + ref: forwardedRef, + }; + + return defaultRenderFunctions.button(rootProps); +}); export { DialogClose }; diff --git a/packages/mui-base/src/Dialog/Description/DialogDescription.tsx b/packages/mui-base/src/Dialog/Description/DialogDescription.tsx index ea1fc0131..01a097612 100644 --- a/packages/mui-base/src/Dialog/Description/DialogDescription.tsx +++ b/packages/mui-base/src/Dialog/Description/DialogDescription.tsx @@ -1,3 +1,16 @@ -const DialogDescription = React.forwardRef(function DialogDescription(props, forwardedRef) {}); +import * as React from 'react'; +import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions'; -export { DialogDescription +const DialogDescription = React.forwardRef(function DialogDescription( + props: React.ComponentPropsWithRef<'p'>, + forwardedRef: React.ForwardedRef, +) { + const rootProps = { + ...props, + ref: forwardedRef, + }; + + return defaultRenderFunctions.p(rootProps); +}); + +export { DialogDescription }; diff --git a/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx index dc17ac1ee..4bca11842 100644 --- a/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx +++ b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx @@ -1,3 +1,62 @@ -const DialogPopup = React.forwardRef(function DialogPopup(props, forwardedRef) {}); +import * as React from 'react'; +import { useDialogRootContext } from '../Root/DialogRootContext'; +import { useForkRef } from '../../utils/useForkRef'; + +export interface DialogPopupProps { + keepMounted?: boolean; + children?: React.ReactNode; +} + +const DialogPopup = React.forwardRef(function DialogPopup( + props: DialogPopupProps & React.ComponentPropsWithRef<'dialog'>, + forwardedRef: React.ForwardedRef, +) { + const { keepMounted, ...other } = props; + + const { open, onOpenChange, modal } = useDialogRootContext(); + const previousOpen = React.useRef(open); + + const ref = React.useRef(null); + const handleRef = useForkRef(ref, forwardedRef); + + React.useEffect(() => { + if (!open) { + ref.current?.close(); + } else if (modal) { + if (previousOpen.current === true) { + ref.current?.close(); + } + ref.current?.showModal(); + } else { + if (previousOpen.current === true) { + ref.current?.close(); + } + ref.current?.show(); + } + + previousOpen.current = open; + }, [open, modal]); + + const handleCancel = (event: React.SyntheticEvent) => { + event.preventDefault(); + onOpenChange?.(false); + }; + + const handleFormSubmit = (event: React.FormEvent) => { + if ((event.target as HTMLFormElement).method === 'dialog') { + event.preventDefault(); + onOpenChange?.(false); + } + }; + + const outputProps: React.ComponentPropsWithRef<'dialog'> = { + ...other, + onCancel: handleCancel, + onSubmit: handleFormSubmit, + ref: handleRef, + }; + + return ; +}); export { DialogPopup }; diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.tsx b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx index af776fde4..830c98ec2 100644 --- a/packages/mui-base/src/Dialog/Root/DialogRoot.tsx +++ b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx @@ -1,58 +1,51 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { useForkRef } from '../../utils/useForkRef'; import { DialogRootProps } from './DialogRoot.types'; +import { DialogRootContext, DialogRootContextValue } from './DialogRootContext'; +import { useControlled } from '../../utils/useControlled'; -const defaultRender = (props: React.ComponentPropsWithRef<'dialog'>) => ; +const defaultRender = (props: React.PropsWithChildren<{ ref: React.Ref }>) => ( + // eslint-disable-next-line react/jsx-no-useless-fragment + {props.children} +); const DialogRoot = React.forwardRef(function DialogRoot( - props: DialogRootProps & React.ComponentPropsWithoutRef<'dialog'>, - forwardedRef: React.Ref, + props: DialogRootProps, + forwardedRef: React.Ref, ) { - const { open = false, modal = true, onOpenChange, ...other } = props; - const previousOpen = React.useRef(open); + const { modal = true, onOpenChange, open: openProp, defaultOpen, ...other } = props; + const [open, setOpen] = useControlled({ + controlled: openProp, + default: defaultOpen, + name: 'DialogRoot', + }); - const ref = React.useRef(null); - const handleRef = useForkRef(ref, forwardedRef); - - React.useEffect(() => { - if (!open) { - ref.current?.close(); - } else if (modal) { - if (previousOpen.current === true) { - ref.current?.close(); - } - ref.current?.showModal(); - } else { - if (previousOpen.current === true) { - ref.current?.close(); - } - ref.current?.show(); - } - - previousOpen.current = open; - }, [open, modal]); - - const handleCancel = (event: React.SyntheticEvent) => { - event.preventDefault(); - onOpenChange?.(false); + const rootProps = { + ...other, + ref: forwardedRef, }; - const handleFormSubmit = (event: React.FormEvent) => { - if ((event.target as HTMLFormElement).method === 'dialog') { - event.preventDefault(); - onOpenChange?.(false); - } - }; + const handleOpenChange = React.useCallback( + (shouldOpen: boolean) => { + setOpen(shouldOpen); + onOpenChange?.(shouldOpen); + }, + [onOpenChange, setOpen], + ); - const outputProps: React.ComponentPropsWithRef<'dialog'> = { - ...other, - onCancel: handleCancel, - onSubmit: handleFormSubmit, - ref: handleRef, - }; + const contextValue: DialogRootContextValue = React.useMemo(() => { + return { + modal, + onOpenChange: handleOpenChange, + open, + }; + }, [modal, handleOpenChange, open]); - return defaultRender(outputProps); + return ( + + {defaultRender(rootProps)} + + ); }); DialogRoot.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts b/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts index 2fae12ea0..f7ebfe66a 100644 --- a/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts +++ b/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts @@ -1,5 +1,6 @@ export interface DialogRootProps { open?: boolean; + defaultOpen?: boolean; modal?: boolean; children?: React.ReactNode; onOpenChange?: (open: boolean) => void; diff --git a/packages/mui-base/src/Dialog/Root/DialogRootContext.ts b/packages/mui-base/src/Dialog/Root/DialogRootContext.ts new file mode 100644 index 000000000..84cc2411b --- /dev/null +++ b/packages/mui-base/src/Dialog/Root/DialogRootContext.ts @@ -0,0 +1,17 @@ +import * as React from 'react'; + +export interface DialogRootContextValue { + open: boolean; + onOpenChange?: (open: boolean) => void; + modal: boolean; +} + +export const DialogRootContext = React.createContext(undefined); + +export function useDialogRootContext() { + const context = React.useContext(DialogRootContext); + if (context === undefined) { + throw new Error('useDialogRootContext must be used within a DialogRoot'); + } + return context; +} diff --git a/packages/mui-base/src/Dialog/Title/DialogTitle.tsx b/packages/mui-base/src/Dialog/Title/DialogTitle.tsx index 2977fac50..5d9dadba5 100644 --- a/packages/mui-base/src/Dialog/Title/DialogTitle.tsx +++ b/packages/mui-base/src/Dialog/Title/DialogTitle.tsx @@ -1,3 +1,16 @@ -const DialogTitle = React.forwardRef(function DialogTitle(props, forwardedRef) {}); +import * as React from 'react'; +import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions'; + +const DialogTitle = React.forwardRef(function DialogTitle( + props: React.ComponentPropsWithRef<'h2'>, + forwardedRef: React.ForwardedRef, +) { + const rootProps = { + ...props, + ref: forwardedRef, + }; + + return defaultRenderFunctions.h2(rootProps); +}); export { DialogTitle }; diff --git a/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx index 56c720a9c..819020cd2 100644 --- a/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx +++ b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx @@ -1,3 +1,22 @@ -const DialogTrigger = React.forwardRef(function DialogTrigger(props, forwardedRef) {}); +import * as React from 'react'; +import { useDialogRootContext } from '../Root/DialogRootContext'; + +interface DialogTriggerProps { + children: React.ReactElement; +} + +function DialogTrigger(props: DialogTriggerProps) { + const { children } = props; + + const { open, onOpenChange } = useDialogRootContext(); + + const handleClick = () => { + if (!open) { + onOpenChange?.(true); + } + }; + + return React.cloneElement(children, { onClick: handleClick }); +} export { DialogTrigger }; diff --git a/packages/mui-base/src/Dialog/index.ts b/packages/mui-base/src/Dialog/index.ts index babfc1c35..4d41f0789 100644 --- a/packages/mui-base/src/Dialog/index.ts +++ b/packages/mui-base/src/Dialog/index.ts @@ -1,2 +1,7 @@ +export { DialogClose as Close } from './Close/DialogClose'; +export { DialogDescription as Description } from './Description/DialogDescription'; +export { DialogPopup as Popup } from './Popup/DialogPopup'; export { DialogRoot as Root } from './Root/DialogRoot'; export type { DialogRootProps as RootProps } from './Root/DialogRoot.types'; +export { DialogTitle as Title } from './Title/DialogTitle'; +export { DialogTrigger as Trigger } from './Trigger/DialogTrigger'; diff --git a/packages/mui-base/src/utils/defaultRenderFunctions.tsx b/packages/mui-base/src/utils/defaultRenderFunctions.tsx index 37d5f0dd7..68d50e66a 100644 --- a/packages/mui-base/src/utils/defaultRenderFunctions.tsx +++ b/packages/mui-base/src/utils/defaultRenderFunctions.tsx @@ -7,6 +7,13 @@ export const defaultRenderFunctions = { div: (props: React.ComponentPropsWithRef<'div'>) => { return
; }, + h2: (props: React.ComponentPropsWithRef<'h2'>) => { + // eslint-disable-next-line jsx-a11y/heading-has-content + return

; + }, + p: (props: React.ComponentPropsWithRef<'p'>) => { + return

; + }, span: (props: React.ComponentPropsWithRef<'span'>) => { return ; }, From e8d09d399ed12cd0b06fa704f2b228f197f64c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Wed, 1 May 2024 12:13:18 +0200 Subject: [PATCH 05/92] Wire up aria attributes --- docs/pages/experiments/dialog.tsx | 2 +- .../Dialog/Description/DialogDescription.tsx | 14 ++++++++ .../mui-base/src/Dialog/Popup/DialogPopup.tsx | 21 ++++++++++-- .../mui-base/src/Dialog/Root/DialogRoot.tsx | 33 +++++++++++++++++-- .../src/Dialog/Root/DialogRoot.types.ts | 3 ++ .../src/Dialog/Root/DialogRootContext.ts | 8 +++++ .../mui-base/src/Dialog/Title/DialogTitle.tsx | 14 ++++++++ .../src/Dialog/Trigger/DialogTrigger.tsx | 10 ++++-- 8 files changed, 97 insertions(+), 8 deletions(-) diff --git a/docs/pages/experiments/dialog.tsx b/docs/pages/experiments/dialog.tsx index 6403e71ec..0516a2482 100644 --- a/docs/pages/experiments/dialog.tsx +++ b/docs/pages/experiments/dialog.tsx @@ -35,7 +35,7 @@ function UncontrolledDialogDemo() { function ControlledDialogDemo() { const [open, setOpen] = React.useState(false); - const [modal, setModal] = React.useState(false); + const [modal, setModal] = React.useState(true); function setState(shouldOpen: boolean, shouldBeModal?: boolean) { return () => { diff --git a/packages/mui-base/src/Dialog/Description/DialogDescription.tsx b/packages/mui-base/src/Dialog/Description/DialogDescription.tsx index 01a097612..0281d5def 100644 --- a/packages/mui-base/src/Dialog/Description/DialogDescription.tsx +++ b/packages/mui-base/src/Dialog/Description/DialogDescription.tsx @@ -1,12 +1,26 @@ import * as React from 'react'; import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions'; +import { useId } from '../../utils/useId'; +import { useDialogRootContext } from '../Root/DialogRootContext'; const DialogDescription = React.forwardRef(function DialogDescription( props: React.ComponentPropsWithRef<'p'>, forwardedRef: React.ForwardedRef, ) { + const { id: idProp } = props; + const { registerDescription } = useDialogRootContext(); + const id = useId(idProp); + + React.useEffect(() => { + registerDescription(id ?? null); + return () => { + registerDescription(null); + }; + }, [id, registerDescription]); + const rootProps = { ...props, + id, ref: forwardedRef, }; diff --git a/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx index 4bca11842..a5e4a61d4 100644 --- a/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx +++ b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { useDialogRootContext } from '../Root/DialogRootContext'; import { useForkRef } from '../../utils/useForkRef'; +import { useId } from '../../utils/useId'; export interface DialogPopupProps { keepMounted?: boolean; @@ -11,11 +12,14 @@ const DialogPopup = React.forwardRef(function DialogPopup( props: DialogPopupProps & React.ComponentPropsWithRef<'dialog'>, forwardedRef: React.ForwardedRef, ) { - const { keepMounted, ...other } = props; + const { keepMounted, id: idProp, ...other } = props; - const { open, onOpenChange, modal } = useDialogRootContext(); - const previousOpen = React.useRef(open); + const { open, onOpenChange, modal, titleElementId, descriptionElementId, registerPopup, type } = + useDialogRootContext(); + + const id = useId(idProp); + const previousOpen = React.useRef(open); const ref = React.useRef(null); const handleRef = useForkRef(ref, forwardedRef); @@ -37,6 +41,13 @@ const DialogPopup = React.forwardRef(function DialogPopup( previousOpen.current = open; }, [open, modal]); + React.useEffect(() => { + registerPopup(id ?? null); + return () => { + registerPopup(null); + }; + }); + const handleCancel = (event: React.SyntheticEvent) => { event.preventDefault(); onOpenChange?.(false); @@ -50,7 +61,11 @@ const DialogPopup = React.forwardRef(function DialogPopup( }; const outputProps: React.ComponentPropsWithRef<'dialog'> = { + 'aria-labelledby': titleElementId ?? undefined, + 'aria-describedby': descriptionElementId ?? undefined, + role: type === 'alertdialog' ? 'alertdialog' : undefined, ...other, + id, onCancel: handleCancel, onSubmit: handleFormSubmit, ref: handleRef, diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.tsx b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx index 830c98ec2..0db420320 100644 --- a/packages/mui-base/src/Dialog/Root/DialogRoot.tsx +++ b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx @@ -13,13 +13,24 @@ const DialogRoot = React.forwardRef(function DialogRoot( props: DialogRootProps, forwardedRef: React.Ref, ) { - const { modal = true, onOpenChange, open: openProp, defaultOpen, ...other } = props; + const { + modal = true, + onOpenChange, + open: openProp, + defaultOpen, + type = 'dialog', + ...other + } = props; const [open, setOpen] = useControlled({ controlled: openProp, default: defaultOpen, name: 'DialogRoot', }); + const [titleElementId, setTitleElementId] = React.useState(null); + const [descriptionElementId, setDescriptionElementId] = React.useState(null); + const [popupElementId, setPopupElementId] = React.useState(null); + const rootProps = { ...other, ref: forwardedRef, @@ -33,13 +44,31 @@ const DialogRoot = React.forwardRef(function DialogRoot( [onOpenChange, setOpen], ); + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line react-hooks/rules-of-hooks + React.useEffect(() => { + if (type === 'alertdialog' && !modal) { + console.warn( + 'Base UI: The `type="alertdialog"` prop is only valid when `modal={true}`. Alert dialogs must be modal according to WAI-ARIA.', + ); + } + }); + } + const contextValue: DialogRootContextValue = React.useMemo(() => { return { modal, onOpenChange: handleOpenChange, open, + type, + titleElementId, + registerTitle: setTitleElementId, + descriptionElementId, + registerDescription: setDescriptionElementId, + popupElementId, + registerPopup: setPopupElementId, }; - }, [modal, handleOpenChange, open]); + }, [modal, handleOpenChange, open, type, titleElementId, descriptionElementId, popupElementId]); return ( diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts b/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts index f7ebfe66a..ede804b37 100644 --- a/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts +++ b/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts @@ -1,7 +1,10 @@ +export type DialogType = 'dialog' | 'alertdialog'; + export interface DialogRootProps { open?: boolean; defaultOpen?: boolean; modal?: boolean; children?: React.ReactNode; onOpenChange?: (open: boolean) => void; + type?: DialogType; } diff --git a/packages/mui-base/src/Dialog/Root/DialogRootContext.ts b/packages/mui-base/src/Dialog/Root/DialogRootContext.ts index 84cc2411b..7ee056992 100644 --- a/packages/mui-base/src/Dialog/Root/DialogRootContext.ts +++ b/packages/mui-base/src/Dialog/Root/DialogRootContext.ts @@ -1,9 +1,17 @@ import * as React from 'react'; +import { DialogType } from './DialogRoot.types'; export interface DialogRootContextValue { open: boolean; onOpenChange?: (open: boolean) => void; modal: boolean; + type: DialogType; + titleElementId: string | null; + registerTitle: (elementId: string | null) => void; + descriptionElementId: string | null; + registerDescription: (elementId: string | null) => void; + popupElementId: string | null; + registerPopup: (elementId: string | null) => void; } export const DialogRootContext = React.createContext(undefined); diff --git a/packages/mui-base/src/Dialog/Title/DialogTitle.tsx b/packages/mui-base/src/Dialog/Title/DialogTitle.tsx index 5d9dadba5..69a2b758f 100644 --- a/packages/mui-base/src/Dialog/Title/DialogTitle.tsx +++ b/packages/mui-base/src/Dialog/Title/DialogTitle.tsx @@ -1,12 +1,26 @@ import * as React from 'react'; +import { useDialogRootContext } from '../Root/DialogRootContext'; import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions'; +import { useId } from '../../utils/useId'; const DialogTitle = React.forwardRef(function DialogTitle( props: React.ComponentPropsWithRef<'h2'>, forwardedRef: React.ForwardedRef, ) { + const { id: idProp } = props; + const { registerTitle } = useDialogRootContext(); + const id = useId(idProp); + + React.useEffect(() => { + registerTitle(id ?? null); + return () => { + registerTitle(null); + }; + }, [id, registerTitle]); + const rootProps = { ...props, + id, ref: forwardedRef, }; diff --git a/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx index 819020cd2..5ae6fe253 100644 --- a/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx +++ b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx @@ -8,7 +8,7 @@ interface DialogTriggerProps { function DialogTrigger(props: DialogTriggerProps) { const { children } = props; - const { open, onOpenChange } = useDialogRootContext(); + const { open, onOpenChange, popupElementId } = useDialogRootContext(); const handleClick = () => { if (!open) { @@ -16,7 +16,13 @@ function DialogTrigger(props: DialogTriggerProps) { } }; - return React.cloneElement(children, { onClick: handleClick }); + const newProps: React.ButtonHTMLAttributes = { + onClick: handleClick, + 'aria-haspopup': 'dialog', + 'aria-controls': popupElementId ?? undefined, + }; + + return React.cloneElement(children, newProps); } export { DialogTrigger }; From 369c620e2feb11650e054a4278ef6179c4f5a7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Wed, 1 May 2024 12:30:23 +0200 Subject: [PATCH 06/92] Lint + proptypes --- .../mui-base/src/Dialog/Close/DialogClose.tsx | 12 +++++++++++ .../Dialog/Description/DialogDescription.tsx | 16 +++++++++++++++ .../mui-base/src/Dialog/Popup/DialogPopup.tsx | 20 +++++++++++++++++++ .../mui-base/src/Dialog/Root/DialogRoot.tsx | 10 +++++++++- .../mui-base/src/Dialog/Title/DialogTitle.tsx | 16 +++++++++++++++ .../src/Dialog/Trigger/DialogTrigger.tsx | 12 +++++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) diff --git a/packages/mui-base/src/Dialog/Close/DialogClose.tsx b/packages/mui-base/src/Dialog/Close/DialogClose.tsx index 675bfa346..39e5a0845 100644 --- a/packages/mui-base/src/Dialog/Close/DialogClose.tsx +++ b/packages/mui-base/src/Dialog/Close/DialogClose.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions'; import { useDialogRootContext } from '../Root/DialogRootContext'; @@ -23,4 +24,15 @@ const DialogClose = React.forwardRef(function DialogClose( return defaultRenderFunctions.button(rootProps); }); +DialogClose.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, +} as any; + export { DialogClose }; diff --git a/packages/mui-base/src/Dialog/Description/DialogDescription.tsx b/packages/mui-base/src/Dialog/Description/DialogDescription.tsx index 0281d5def..f980dbcc9 100644 --- a/packages/mui-base/src/Dialog/Description/DialogDescription.tsx +++ b/packages/mui-base/src/Dialog/Description/DialogDescription.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions'; import { useId } from '../../utils/useId'; import { useDialogRootContext } from '../Root/DialogRootContext'; @@ -27,4 +28,19 @@ const DialogDescription = React.forwardRef(function DialogDescription( return defaultRenderFunctions.p(rootProps); }); +DialogDescription.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * @ignore + */ + id: PropTypes.string, +} as any; + export { DialogDescription }; diff --git a/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx index a5e4a61d4..da38cfcc5 100644 --- a/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx +++ b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import { useDialogRootContext } from '../Root/DialogRootContext'; import { useForkRef } from '../../utils/useForkRef'; import { useId } from '../../utils/useId'; @@ -74,4 +75,23 @@ const DialogPopup = React.forwardRef(function DialogPopup( return

; }); +DialogPopup.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * @ignore + */ + id: PropTypes.string, + /** + * @ignore + */ + keepMounted: PropTypes.bool, +} as any; + export { DialogPopup }; diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.tsx b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx index 0db420320..66b032df0 100644 --- a/packages/mui-base/src/Dialog/Root/DialogRoot.tsx +++ b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx @@ -6,7 +6,7 @@ import { useControlled } from '../../utils/useControlled'; const defaultRender = (props: React.PropsWithChildren<{ ref: React.Ref }>) => ( // eslint-disable-next-line react/jsx-no-useless-fragment - {props.children} + ); const DialogRoot = React.forwardRef(function DialogRoot( @@ -86,6 +86,10 @@ DialogRoot.propTypes /* remove-proptypes */ = { * @ignore */ children: PropTypes.node, + /** + * @ignore + */ + defaultOpen: PropTypes.bool, /** * @ignore */ @@ -98,6 +102,10 @@ DialogRoot.propTypes /* remove-proptypes */ = { * @ignore */ open: PropTypes.bool, + /** + * @ignore + */ + type: PropTypes.oneOf(['alertdialog', 'dialog']), } as any; export { DialogRoot }; diff --git a/packages/mui-base/src/Dialog/Title/DialogTitle.tsx b/packages/mui-base/src/Dialog/Title/DialogTitle.tsx index 69a2b758f..dab390745 100644 --- a/packages/mui-base/src/Dialog/Title/DialogTitle.tsx +++ b/packages/mui-base/src/Dialog/Title/DialogTitle.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import { useDialogRootContext } from '../Root/DialogRootContext'; import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions'; import { useId } from '../../utils/useId'; @@ -27,4 +28,19 @@ const DialogTitle = React.forwardRef(function DialogTitle( return defaultRenderFunctions.h2(rootProps); }); +DialogTitle.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * @ignore + */ + id: PropTypes.string, +} as any; + export { DialogTitle }; diff --git a/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx index 5ae6fe253..66a3859f4 100644 --- a/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx +++ b/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import PropTypes from 'prop-types'; import { useDialogRootContext } from '../Root/DialogRootContext'; interface DialogTriggerProps { @@ -25,4 +26,15 @@ function DialogTrigger(props: DialogTriggerProps) { return React.cloneElement(children, newProps); } +DialogTrigger.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.element.isRequired, +} as any; + export { DialogTrigger }; From f9b3ab808f4ef54ea61013f9edd7f6e797d20eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Wed, 1 May 2024 15:14:40 +0200 Subject: [PATCH 07/92] closeOnClickOutside --- docs/pages/experiments/dialog.module.css | 26 +++++++- docs/pages/experiments/dialog.tsx | 4 +- .../mui-base/src/Dialog/Popup/DialogPopup.tsx | 62 ++++++++++++++++++- .../mui-base/src/Dialog/Root/DialogRoot.tsx | 14 ++++- .../src/Dialog/Root/DialogRoot.types.ts | 1 + .../src/Dialog/Root/DialogRootContext.ts | 1 + 6 files changed, 101 insertions(+), 7 deletions(-) diff --git a/docs/pages/experiments/dialog.module.css b/docs/pages/experiments/dialog.module.css index 489d0e6f9..6ee2ea6e9 100644 --- a/docs/pages/experiments/dialog.module.css +++ b/docs/pages/experiments/dialog.module.css @@ -2,6 +2,18 @@ max-width: 1000px; margin: 0 auto; padding: 16px; + font-family: IBM Plex Sans; + + h1 { + font-family: General Sans; + font-weight: 600; + font-size: 2rem; + } + + h2 { + font-size: 1.5rem; + font-weight: 600; + } } .dialog { @@ -22,7 +34,6 @@ .button { background: #eee; padding: 8px 16px; - margin: 8px; border: 1px solid #d8d8d8; border-radius: 4px; font-family: inherit; @@ -31,8 +42,21 @@ background: #ffbf2b; } + &:focus-visible { + outline: 2px solid #ffbf2b; + } + &:active { background: #cc9922; + border-color: #cc9922; + + &:focus-visible { + outline-color: #cc9922; + } + } + + & + .button { + margin-left: 8px; } } diff --git a/docs/pages/experiments/dialog.tsx b/docs/pages/experiments/dialog.tsx index 0516a2482..6b043069a 100644 --- a/docs/pages/experiments/dialog.tsx +++ b/docs/pages/experiments/dialog.tsx @@ -6,7 +6,7 @@ import classes from './dialog.module.css'; function UncontrolledDialogDemo() { return (
- + - + Dialog This is a sample dialog diff --git a/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx index da38cfcc5..e510499a2 100644 --- a/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx +++ b/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useDialogRootContext } from '../Root/DialogRootContext'; import { useForkRef } from '../../utils/useForkRef'; +import { ownerDocument } from '../../utils/owner'; import { useId } from '../../utils/useId'; export interface DialogPopupProps { @@ -15,8 +16,16 @@ const DialogPopup = React.forwardRef(function DialogPopup( ) { const { keepMounted, id: idProp, ...other } = props; - const { open, onOpenChange, modal, titleElementId, descriptionElementId, registerPopup, type } = - useDialogRootContext(); + const { + open, + onOpenChange, + modal, + titleElementId, + descriptionElementId, + registerPopup, + type, + closeOnClickOutside, + } = useDialogRootContext(); const id = useId(idProp); @@ -47,7 +56,44 @@ const DialogPopup = React.forwardRef(function DialogPopup( return () => { registerPopup(null); }; - }); + }, [id, registerPopup]); + + const handleClickOutside = React.useCallback( + (event: PointerEvent) => { + const popupElement = ref.current; + if (!popupElement) { + return; + } + + if (modal) { + // When the dialog is modal, clicking on the backdrop is recognized as clicking on the dialog itself. + // We need to check whether the click was outside the dialog's bounding box. + // We also don't want to close the dialog when clicking on any descendant of it (such as an open select). + if ( + (event.target === popupElement && hasClickedOutsideBoundingBox(event, popupElement)) || + !popupElement.contains(event.target as Node) + ) { + onOpenChange?.(false); + } + } else if (!popupElement.contains(event.target as Node)) { + onOpenChange?.(false); + } + }, + [onOpenChange, modal], + ); + + React.useEffect(() => { + if (!closeOnClickOutside) { + return undefined; + } + + const doc = ownerDocument(ref.current); + if (open) { + doc.addEventListener('pointerdown', handleClickOutside); + } + + return () => doc.removeEventListener('pointerdown', handleClickOutside); + }, [open, handleClickOutside, closeOnClickOutside]); const handleCancel = (event: React.SyntheticEvent) => { event.preventDefault(); @@ -75,6 +121,16 @@ const DialogPopup = React.forwardRef(function DialogPopup( return ; }); +function hasClickedOutsideBoundingBox(event: PointerEvent, element: HTMLElement) { + const boundingRect = element.getBoundingClientRect(); + return ( + event.clientX < boundingRect.left || + event.clientX >= boundingRect.right || + event.clientY < boundingRect.top || + event.clientY >= boundingRect.bottom + ); +} + DialogPopup.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.tsx b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx index 66b032df0..d969a051c 100644 --- a/packages/mui-base/src/Dialog/Root/DialogRoot.tsx +++ b/packages/mui-base/src/Dialog/Root/DialogRoot.tsx @@ -19,8 +19,10 @@ const DialogRoot = React.forwardRef(function DialogRoot( open: openProp, defaultOpen, type = 'dialog', + closeOnClickOutside = false, ...other } = props; + const [open, setOpen] = useControlled({ controlled: openProp, default: defaultOpen, @@ -61,6 +63,7 @@ const DialogRoot = React.forwardRef(function DialogRoot( onOpenChange: handleOpenChange, open, type, + closeOnClickOutside, titleElementId, registerTitle: setTitleElementId, descriptionElementId, @@ -68,7 +71,16 @@ const DialogRoot = React.forwardRef(function DialogRoot( popupElementId, registerPopup: setPopupElementId, }; - }, [modal, handleOpenChange, open, type, titleElementId, descriptionElementId, popupElementId]); + }, [ + modal, + handleOpenChange, + open, + type, + titleElementId, + descriptionElementId, + popupElementId, + closeOnClickOutside, + ]); return ( diff --git a/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts b/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts index ede804b37..dec88b5ce 100644 --- a/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts +++ b/packages/mui-base/src/Dialog/Root/DialogRoot.types.ts @@ -7,4 +7,5 @@ export interface DialogRootProps { children?: React.ReactNode; onOpenChange?: (open: boolean) => void; type?: DialogType; + closeOnClickOutside?: boolean; } diff --git a/packages/mui-base/src/Dialog/Root/DialogRootContext.ts b/packages/mui-base/src/Dialog/Root/DialogRootContext.ts index 7ee056992..d51d53197 100644 --- a/packages/mui-base/src/Dialog/Root/DialogRootContext.ts +++ b/packages/mui-base/src/Dialog/Root/DialogRootContext.ts @@ -6,6 +6,7 @@ export interface DialogRootContextValue { onOpenChange?: (open: boolean) => void; modal: boolean; type: DialogType; + closeOnClickOutside: boolean; titleElementId: string | null; registerTitle: (elementId: string | null) => void; descriptionElementId: string | null; From 09fc8c4e4e6a97f2e6da2f2916199f20c92c8ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Fri, 3 May 2024 09:53:55 +0200 Subject: [PATCH 08/92] Use ClickAwayListener --- docs/pages/experiments/dialog.module.css | 16 +++++++ docs/pages/experiments/dialog.tsx | 4 ++ .../ClickAwayListener/ClickAwayListener.tsx | 43 +++++++++++++++++-- .../mui-base/src/Dialog/Popup/DialogPopup.tsx | 23 +++++++--- .../mui-base/src/Dialog/Root/DialogRoot.tsx | 1 + 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/docs/pages/experiments/dialog.module.css b/docs/pages/experiments/dialog.module.css index 6ee2ea6e9..b4839bbb9 100644 --- a/docs/pages/experiments/dialog.module.css +++ b/docs/pages/experiments/dialog.module.css @@ -14,6 +14,14 @@ font-size: 1.5rem; font-weight: 600; } + + input { + padding: 8px; + border: 1px solid #d8d8d8; + border-radius: 4px; + font-family: inherit; + box-sizing: border-box; + } } .dialog { @@ -74,3 +82,11 @@ .nonmodal { top: 20vh; } + +.textarea { + resize: vertical; + min-height: 100px; + width: 100%; + font-family: inherit; + box-sizing: border-box; +} diff --git a/docs/pages/experiments/dialog.tsx b/docs/pages/experiments/dialog.tsx index 6b043069a..837c732b2 100644 --- a/docs/pages/experiments/dialog.tsx +++ b/docs/pages/experiments/dialog.tsx @@ -21,6 +21,10 @@ function UncontrolledDialogDemo() { pellentesque diam nec ligula hendrerit dapibus.

+