diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.js b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.js new file mode 100644 index 0000000000..6c34a8e923 --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.js @@ -0,0 +1,117 @@ +import * as React from 'react'; +import * as AlertDialog from '@base_ui/react/AlertDialog'; +import { useTheme } from '@mui/system'; + +export default function AlertDialogIntroduction() { + return ( + + + Subscribe + + + Subscribe + + Are you sure you want to subscribe? + +
+ Yes + No +
+
+
+ +
+ ); +} + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +function Styles() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( + + ); +} diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.tsx b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.tsx new file mode 100644 index 0000000000..6c34a8e923 --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/css/index.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import * as AlertDialog from '@base_ui/react/AlertDialog'; +import { useTheme } from '@mui/system'; + +export default function AlertDialogIntroduction() { + return ( + + + Subscribe + + + Subscribe + + Are you sure you want to subscribe? + +
+ Yes + No +
+
+
+ +
+ ); +} + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +function Styles() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( + + ); +} diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.js b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.js new file mode 100644 index 0000000000..6f2f0907cd --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.js @@ -0,0 +1,104 @@ +import * as React from 'react'; +import * as AlertDialog from '@base_ui/react/AlertDialog'; +import { styled } from '@mui/system'; + +export default function AlertDialogIntroduction() { + return ( + + Subscribe + + + Subscribe + Are you sure you want to subscribe? + + Yes + No + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const TriggerButton = styled(AlertDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: "IBM Plex Sans", sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Popup = styled(AlertDialog.Popup)( + ({ theme }) => ` + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: "IBM Plex Sans", sans-serif; + transform: translate(-50%, -50%); + padding: 16px; + z-index: 2100; +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); + +const CloseButton = styled(AlertDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: "IBM Plex Sans", sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Title = styled(AlertDialog.Title)` + font-size: 1.25rem; +`; + +const Description = styled(AlertDialog.Description)``; + +const Backdrop = styled(AlertDialog.Backdrop)` + background: rgb(0 0 0 / 0.35); + position: fixed; + inset: 0; + backdrop-filter: blur(4px); + z-index: 2000; +`; diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx new file mode 100644 index 0000000000..6f2f0907cd --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import * as AlertDialog from '@base_ui/react/AlertDialog'; +import { styled } from '@mui/system'; + +export default function AlertDialogIntroduction() { + return ( + + Subscribe + + + Subscribe + Are you sure you want to subscribe? + + Yes + No + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const TriggerButton = styled(AlertDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: "IBM Plex Sans", sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Popup = styled(AlertDialog.Popup)( + ({ theme }) => ` + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: "IBM Plex Sans", sans-serif; + transform: translate(-50%, -50%); + padding: 16px; + z-index: 2100; +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); + +const CloseButton = styled(AlertDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: "IBM Plex Sans", sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Title = styled(AlertDialog.Title)` + font-size: 1.25rem; +`; + +const Description = styled(AlertDialog.Description)``; + +const Backdrop = styled(AlertDialog.Backdrop)` + background: rgb(0 0 0 / 0.35); + position: fixed; + inset: 0; + backdrop-filter: blur(4px); + z-index: 2000; +`; diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx.preview b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx.preview new file mode 100644 index 0000000000..b074c5cbb8 --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/system/index.tsx.preview @@ -0,0 +1,12 @@ + + Subscribe + + + Subscribe + Are you sure you want to subscribe? + + Yes + No + + + \ No newline at end of file diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.js b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.js new file mode 100644 index 0000000000..c051ff4a3d --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.js @@ -0,0 +1,72 @@ +import * as React from 'react'; +import * as AlertDialog from '@base_ui/react/AlertDialog'; + +export default function UnstyledDialogIntroduction() { + return ( + + Subscribe + + + Subscribe + Are you sure you want to subscribe? + + Yes + No + + + + ); +} + +function TriggerButton(props) { + const className = ` + bg-slate-900 dark:bg-slate-50 text-slate-50 dark:text-slate-900 + py-2 px-4 rounded min-w-[80px] border-none font-sans + hover:bg-slate-700 dark:hover:bg-slate-200`; + + return ; +} + +function Popup(props) { + const className = ` + bg-slate-50 dark:bg-slate-900 border-[1px] border-solid border-slate-100 dark:border-slate-700 + min-w-[400px] rounded shadow-xl fixed top-2/4 left-2/4 z-[2100] + -translate-x-2/4 -translate-y-2/4 p-4`; + + return ; +} + +function Controls(props) { + return ( +
+ ); +} + +function CloseButton(props) { + const className = ` + bg-transparent border-[1px] border-solid border-slate-500 dark:border-slate-300 + text-slate-900 dark:text-slate-50 py-2 px-4 rounded font-sans min-w-[80px] + hover:bg-slate-200 dark:hover:bg-slate-700`; + + return ; +} + +function Title(props) { + return ; +} + +function Description(props) { + return ; +} + +function Backdrop(props) { + return ( + + ); +} diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx new file mode 100644 index 0000000000..07d9cddfbf --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import * as AlertDialog from '@base_ui/react/AlertDialog'; + +export default function UnstyledDialogIntroduction() { + return ( + + Subscribe + + + Subscribe + Are you sure you want to subscribe? + + Yes + No + + + + ); +} + +function TriggerButton(props: AlertDialog.TriggerProps) { + const className = ` + bg-slate-900 dark:bg-slate-50 text-slate-50 dark:text-slate-900 + py-2 px-4 rounded min-w-[80px] border-none font-sans + hover:bg-slate-700 dark:hover:bg-slate-200`; + + return ; +} + +function Popup(props: AlertDialog.PopupProps) { + const className = ` + bg-slate-50 dark:bg-slate-900 border-[1px] border-solid border-slate-100 dark:border-slate-700 + min-w-[400px] rounded shadow-xl fixed top-2/4 left-2/4 z-[2100] + -translate-x-2/4 -translate-y-2/4 p-4`; + + return ; +} + +function Controls(props: React.ComponentPropsWithoutRef<'div'>) { + return ( +
+ ); +} + +function CloseButton(props: AlertDialog.CloseProps) { + const className = ` + bg-transparent border-[1px] border-solid border-slate-500 dark:border-slate-300 + text-slate-900 dark:text-slate-50 py-2 px-4 rounded font-sans min-w-[80px] + hover:bg-slate-200 dark:hover:bg-slate-700`; + + return ; +} + +function Title(props: AlertDialog.TitleProps) { + return ; +} + +function Description(props: AlertDialog.DescriptionProps) { + return ; +} + +function Backdrop(props: AlertDialog.BackdropProps) { + return ( + + ); +} diff --git a/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx.preview b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx.preview new file mode 100644 index 0000000000..b074c5cbb8 --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogIntroduction/tailwind/index.tsx.preview @@ -0,0 +1,12 @@ + + Subscribe + + + Subscribe + Are you sure you want to subscribe? + + Yes + No + + + \ No newline at end of file diff --git a/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.js b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.js new file mode 100644 index 0000000000..7e8e7fd592 --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.js @@ -0,0 +1,135 @@ +import * as React from 'react'; +import * as BaseAlertDialog from '@base_ui/react/AlertDialog'; +import { styled } from '@mui/system'; + +export default function AlertDialogWithTransitions() { + return ( + + Open + + Animated alert dialog + + This alert dialog uses CSS transitions on entry and exit. + + + Close + + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const Popup = styled(BaseAlertDialog.Popup)( + ({ theme }) => ` + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: IBM Plex Sans; + padding: 16px; + z-index: 2100; + transition-property: opacity, transform; + transition-duration: 150ms; + transition-timing-function: ease-in; + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + + &[data-state='open'] { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + transition-timing-function: ease-out; + } + + &[data-entering] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +`, +); + +const Backdrop = styled(BaseAlertDialog.Backdrop)` + background-color: rgb(0 0 0 / 0.2); + position: fixed; + inset: 0; + z-index: 2000; + backdrop-filter: blur(0); + opacity: 0; + transition-property: opacity, backdrop-filter; + transition-duration: 250ms; + transition-timing-function: ease-in; + + &[data-state='open'] { + backdrop-filter: blur(6px); + opacity: 1; + transition-timing-function: ease-out; + } + + &[data-entering] { + backdrop-filter: blur(0); + opacity: 0; + } +`; + +const Title = styled(BaseAlertDialog.Title)` + font-size: 1.25rem; +`; + +const Trigger = styled(BaseAlertDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: + "IBM Plex Sans", + sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Close = styled(BaseAlertDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: IBM Plex Sans, sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); diff --git a/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx new file mode 100644 index 0000000000..7e8e7fd592 --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx @@ -0,0 +1,135 @@ +import * as React from 'react'; +import * as BaseAlertDialog from '@base_ui/react/AlertDialog'; +import { styled } from '@mui/system'; + +export default function AlertDialogWithTransitions() { + return ( + + Open + + Animated alert dialog + + This alert dialog uses CSS transitions on entry and exit. + + + Close + + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const Popup = styled(BaseAlertDialog.Popup)( + ({ theme }) => ` + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: IBM Plex Sans; + padding: 16px; + z-index: 2100; + transition-property: opacity, transform; + transition-duration: 150ms; + transition-timing-function: ease-in; + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + + &[data-state='open'] { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + transition-timing-function: ease-out; + } + + &[data-entering] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +`, +); + +const Backdrop = styled(BaseAlertDialog.Backdrop)` + background-color: rgb(0 0 0 / 0.2); + position: fixed; + inset: 0; + z-index: 2000; + backdrop-filter: blur(0); + opacity: 0; + transition-property: opacity, backdrop-filter; + transition-duration: 250ms; + transition-timing-function: ease-in; + + &[data-state='open'] { + backdrop-filter: blur(6px); + opacity: 1; + transition-timing-function: ease-out; + } + + &[data-entering] { + backdrop-filter: blur(0); + opacity: 0; + } +`; + +const Title = styled(BaseAlertDialog.Title)` + font-size: 1.25rem; +`; + +const Trigger = styled(BaseAlertDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: + "IBM Plex Sans", + sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Close = styled(BaseAlertDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: IBM Plex Sans, sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); diff --git a/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx.preview b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx.preview new file mode 100644 index 0000000000..75f8ff39c9 --- /dev/null +++ b/docs/data/base/components/alert-dialog/AlertDialogWithTransitions.tsx.preview @@ -0,0 +1,13 @@ + + Open + + Animated alert dialog + + This alert dialog uses CSS transitions on entry and exit. + + + Close + + + + \ No newline at end of file diff --git a/docs/data/base/components/alert-dialog/NestedAlertDialogs.js b/docs/data/base/components/alert-dialog/NestedAlertDialogs.js new file mode 100644 index 0000000000..5c1385314c --- /dev/null +++ b/docs/data/base/components/alert-dialog/NestedAlertDialogs.js @@ -0,0 +1,163 @@ +import * as React from 'react'; +import * as BaseAlertDialog from '@base_ui/react/AlertDialog'; +import { styled } from '@mui/system'; + +export default function NestedAlertDialogs() { + return ( + + Open + + + Alert Dialog 1 + + + Open Nested + + + Alert Dialog 2 + + + Open Nested + + + Alert Dialog 3 + + Close + + + + Close + + + + Close + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const Popup = styled(BaseAlertDialog.Popup)( + ({ theme }) => ` + --transition-duration: 150ms; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: IBM Plex Sans; + padding: 16px; + z-index: 2100; + transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + visibility: hidden; + opacity: 0.5; + transition: + transform var(--transition-duration) ease-in, + opacity var(--transition-duration) ease-in, + visibility var(--transition-duration) step-end; + + &[data-state='open'] { + @starting-style { + & { + transform: translate(-50%, -35%) scale(0.8) translateY(0); + opacity: 0.5; + } + } + + visibility: visible; + opacity: 1; + transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + transition: + transform var(--transition-duration) ease-out, + opacity var(--transition-duration) ease-out, + visibility var(--transition-duration) step-start; + } +`, +); + +const Title = styled(BaseAlertDialog.Title)` + font-size: 1.25rem; +`; + +const Trigger = styled(BaseAlertDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: + "IBM Plex Sans", + sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Backdrop = styled(BaseAlertDialog.Backdrop)` + background-color: rgb(0 0 0 / 0.2); + position: fixed; + inset: 0; + z-index: 2000; + backdrop-filter: blur(0); + opacity: 0; + transition-property: opacity, backdrop-filter; + transition-duration: 250ms; + transition-timing-function: ease-in; + + &[data-state='open'] { + backdrop-filter: blur(6px); + opacity: 1; + transition-timing-function: ease-out; + } + + &[data-entering] { + backdrop-filter: blur(0); + opacity: 0; + } +`; + +const Close = styled(BaseAlertDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: IBM Plex Sans, sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); diff --git a/docs/data/base/components/alert-dialog/NestedAlertDialogs.tsx b/docs/data/base/components/alert-dialog/NestedAlertDialogs.tsx new file mode 100644 index 0000000000..5c1385314c --- /dev/null +++ b/docs/data/base/components/alert-dialog/NestedAlertDialogs.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; +import * as BaseAlertDialog from '@base_ui/react/AlertDialog'; +import { styled } from '@mui/system'; + +export default function NestedAlertDialogs() { + return ( + + Open + + + Alert Dialog 1 + + + Open Nested + + + Alert Dialog 2 + + + Open Nested + + + Alert Dialog 3 + + Close + + + + Close + + + + Close + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const Popup = styled(BaseAlertDialog.Popup)( + ({ theme }) => ` + --transition-duration: 150ms; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: IBM Plex Sans; + padding: 16px; + z-index: 2100; + transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + visibility: hidden; + opacity: 0.5; + transition: + transform var(--transition-duration) ease-in, + opacity var(--transition-duration) ease-in, + visibility var(--transition-duration) step-end; + + &[data-state='open'] { + @starting-style { + & { + transform: translate(-50%, -35%) scale(0.8) translateY(0); + opacity: 0.5; + } + } + + visibility: visible; + opacity: 1; + transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + transition: + transform var(--transition-duration) ease-out, + opacity var(--transition-duration) ease-out, + visibility var(--transition-duration) step-start; + } +`, +); + +const Title = styled(BaseAlertDialog.Title)` + font-size: 1.25rem; +`; + +const Trigger = styled(BaseAlertDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: + "IBM Plex Sans", + sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Backdrop = styled(BaseAlertDialog.Backdrop)` + background-color: rgb(0 0 0 / 0.2); + position: fixed; + inset: 0; + z-index: 2000; + backdrop-filter: blur(0); + opacity: 0; + transition-property: opacity, backdrop-filter; + transition-duration: 250ms; + transition-timing-function: ease-in; + + &[data-state='open'] { + backdrop-filter: blur(6px); + opacity: 1; + transition-timing-function: ease-out; + } + + &[data-entering] { + backdrop-filter: blur(0); + opacity: 0; + } +`; + +const Close = styled(BaseAlertDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: IBM Plex Sans, sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); diff --git a/docs/data/base/components/alert-dialog/alert-dialog.md b/docs/data/base/components/alert-dialog/alert-dialog.md new file mode 100644 index 0000000000..ad67481d4b --- /dev/null +++ b/docs/data/base/components/alert-dialog/alert-dialog.md @@ -0,0 +1,298 @@ +--- +productId: base-ui +title: React Alert Dialog component +components: AlertDialogBackdrop, AlertDialogClose, AlertDialogDescription, AlertDialogPopup, AlertDialogRoot, AlertDialogTitle, AlertDialogTrigger +githubLabel: 'component: alert-dialog' +waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/ +--- + +# Alert Dialog + +

Dialogs inform users about a task and can contain critical information, require decisions, or involve multiple tasks.

+ +{{"component": "@mui/docs/ComponentLinkHeader", "design": false}} + +{{"component": "modules/components/ComponentPageTabs.js"}} + +{{"demo": "AlertDialogIntroduction", "defaultCodeOpen": false, "bg": "gradient"}} + +## Installation + +Base UI components are all available as a single package. + + + +```bash npm +npm install @base_ui/react +``` + +```bash yarn +yarn add @base_ui/react +``` + +```bash pnpm +pnpm add @base_ui/react +``` + + + +Once you have the package installed, import the component. + +```ts +import * as AlertDialog from '@base_ui/react/AlertDialog'; +``` + +## Anatomy + +Alert Dialogs are implemented using a collection of related components: + +- `` is a top-level component that facilitates communication between other components. It does not render to the DOM. +- `` is the alert dialog panel itself. +- `` is the background element appearing when a popup is visible. Use it to indicate that the page is inert. The Backdrop must be a sibling of the Popup component. +- `` is the component (a button by default) that, when clicked, shows the popup. When it's not provided, the visibility of the Alert Dialog can be controlled with its `open` prop (see [Controlled vs. uncontrolled behavior](#controlled-vs-uncontrolled-behavior)). +- `` renders a button that closes the popup. You can attach your own click handlers to it to perform additional actions. +- `` is an header element displaying the title of the alert dialog. It is referenced in the Dialog's ARIA attributes to properly announce it. +- `` is an element describing of the dialog. It is referenced in the Dialog's ARIA attributes to properly announce it. + +```tsx + + + + + + + + + + + +``` + +## Alert dialogs vs. dialogs + +The Alert Dialog is in many ways similar to the [Dialog](/base-ui/react-dialog/) component. +Alert dialogs should be used in cases where the normal user's workflow needs to be interrupted to get a response. +Therefore alert dialogs are always modal and cannot be dismissed any other way than by pressing a button inside them. + +## Controlled vs. uncontrolled behavior + +The simplest way to control the visibility of the alert dialog is to use the `AlertDialog.Trigger` and `AlertDialog.Close` components. + +You can set the initial state with the `defaultOpen` prop. + +```tsx + + Open + + Demo dialog + Close + + +``` + +Doing so ensures that the accessibity attributes are set correctly so that the trigger button is approriately announced by assistive technologies. + +If you need to control the visibility programmatically from the outside, use the `value` prop. +You can still use the `AlertDialog.Trigger` and `AlertDialog.Close` components (though it's not necessary), but you need to make sure to create a handler for the `onOpenChange` event and update the state manually. + +```tsx +const [open, setOpen] = React.useState(false); + +return ( + + Open + + Demo dialog + Close + + +); +``` + +## Nested dialogs + +An alert dialog can open another dialog (or alert dialog). +At times, it may be useful to know how may open sub-dialogs a given alert dialog has. +One example of this could be styling the bottom dialog in a way they appear below the top-most one. + +The number of open child dialogs is present in the `data-nested-dialogs` attribute and in the `--nested-dialogs` CSS variable on the `` component. + +{{"demo": "NestedAlertDialogs.js"}} + +Note that when dialogs are nested, only the bottom-most backdrop is rendered. + +## Animation + +The `` and `` components support transitions on entry and exit. + +CSS animations and transitions are supported out of the box. +If a component has a transition or animation applied to it when it closes, it will be unmounted only after the animation finishes. + +As this detection of exit animations requires an extra render, you may opt out of it by setting the `animated` prop on Popup and Backdrop to `false`. +We also recommend doing so in automated tests, to avoid asynchronous behavior and make testing easier. + +Alternatively, you can use JS-based animations with a library like framer-motion, React Spring, or similar. +With this approach set the `keepMounted` to `true` and let the animation library control mounting and unmounting. + +### CSS transitions + +Here is an example of how to apply a symmetric scale and fade transition with the default conditionally-rendered behavior: + +```jsx +Alert +``` + +```css +.AlertDialogPopup { + transition-property: opacity, transform; + transition-duration: 0.2s; + /* Represents the final styles once exited */ + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); +} + +/* Represents the final styles once entered */ +.AlertDialogPopup[data-state='open'] { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + +/* Represents the initial styles when entering */ +.AlertDialogPopup[data-entering] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); +} +``` + +Styles need to be applied in three states: + +- The exiting styles, placed on the base element class +- The open styles, placed on the base element class with `[data-state="open"]` +- The entering styles, placed on the base element class with `[data-entering]` + +{{"demo": "AlertDialogWithTransitions.js"}} + +In newer browsers, there is a feature called `@starting-style` which allows transitions to occur on open for conditionally-mounted components: + +```css +/* Base UI API - Polyfill */ +.AlertDialogPopup[data-entering] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); +} + +/* Official Browser API - no Firefox support as of May 2024 */ +@starting-style { + .AlertDialogPopup[data-state='open'] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +} +``` + +### CSS animations + +CSS animations can also be used, requiring only two separate declarations: + +```css +@keyframes scale-in { + from { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +} + +@keyframes scale-out { + to { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +} + +.AlertDialogPopup { + animation: scale-in 0.2s forwards; +} + +.AlertDialogPopup[data-exiting] { + animation: scale-out 0.2s forwards; +} +``` + +### JavaScript animations + +The `keepMounted` prop lets an external library control the mounting, for example `framer-motion`'s `AnimatePresence` component. + +```js +function App() { + const [open, setOpen] = useState(false); + return ( + + Trigger + + {open && ( + + } + > + Alert Dialog + + )} + + + ); +} +``` + +### Animation states + +Four states are available as data attributes to animate the dialog, which enables full control depending on whether the popup is being animated with CSS transitions or animations, JavaScript, or is using the `keepMounted` prop. + +- `[data-state="open"]` - `open` state is `true`. +- `[data-state="closed"]` - `open` state is `false`. Can still be mounted to the DOM if closing. +- `[data-entering]` - the popup was just inserted to the DOM. The attribute is removed 1 animation frame later. Enables "starting styles" upon insertion for conditional rendering. +- `[data-exiting]` - the popup is in the process of being removed from the DOM, but is still mounted. + +## Composing a custom React component + +Use the `render` prop to override the rendered element: + +```jsx +} /> +// or + } /> +``` + +## Accessibility + +Using the `` sets the required accessibility attributes on the trigger button. +If you prefer controlling the open state differently, you need to apply these attributes on your own: + +```tsx +const [open, setOpen] = React.useState(false); + +return ( +
+ + + + + Demo dialog + Close + + +
+); +``` diff --git a/docs/data/base/components/dialog/DialogWithTransitions.js b/docs/data/base/components/dialog/DialogWithTransitions.js new file mode 100644 index 0000000000..901f682060 --- /dev/null +++ b/docs/data/base/components/dialog/DialogWithTransitions.js @@ -0,0 +1,135 @@ +import * as React from 'react'; +import * as BaseDialog from '@base_ui/react/Dialog'; +import { styled } from '@mui/system'; + +export default function DialogWithTransitions() { + return ( + + Open + + Animated dialog + + This dialog uses CSS transitions on entry and exit. + + + Close + + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const Popup = styled(BaseDialog.Popup)( + ({ theme }) => ` + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: IBM Plex Sans; + padding: 16px; + z-index: 2100; + transition-property: opacity, transform; + transition-duration: 150ms; + transition-timing-function: ease-in; + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + + &[data-state='open'] { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + transition-timing-function: ease-out; + } + + &[data-entering] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +`, +); + +const Backdrop = styled(BaseDialog.Backdrop)` + background-color: rgb(0 0 0 / 0.2); + position: fixed; + inset: 0; + z-index: 2000; + backdrop-filter: blur(0); + opacity: 0; + transition-property: opacity, backdrop-filter; + transition-duration: 250ms; + transition-timing-function: ease-in; + + &[data-state='open'] { + backdrop-filter: blur(6px); + opacity: 1; + transition-timing-function: ease-out; + } + + &[data-entering] { + backdrop-filter: blur(0); + opacity: 0; + } +`; + +const Title = styled(BaseDialog.Title)` + font-size: 1.25rem; +`; + +const Trigger = styled(BaseDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: + "IBM Plex Sans", + sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Close = styled(BaseDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: IBM Plex Sans, sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); diff --git a/docs/data/base/components/dialog/DialogWithTransitions.tsx b/docs/data/base/components/dialog/DialogWithTransitions.tsx new file mode 100644 index 0000000000..901f682060 --- /dev/null +++ b/docs/data/base/components/dialog/DialogWithTransitions.tsx @@ -0,0 +1,135 @@ +import * as React from 'react'; +import * as BaseDialog from '@base_ui/react/Dialog'; +import { styled } from '@mui/system'; + +export default function DialogWithTransitions() { + return ( + + Open + + Animated dialog + + This dialog uses CSS transitions on entry and exit. + + + Close + + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const Popup = styled(BaseDialog.Popup)( + ({ theme }) => ` + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: IBM Plex Sans; + padding: 16px; + z-index: 2100; + transition-property: opacity, transform; + transition-duration: 150ms; + transition-timing-function: ease-in; + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + + &[data-state='open'] { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + transition-timing-function: ease-out; + } + + &[data-entering] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +`, +); + +const Backdrop = styled(BaseDialog.Backdrop)` + background-color: rgb(0 0 0 / 0.2); + position: fixed; + inset: 0; + z-index: 2000; + backdrop-filter: blur(0); + opacity: 0; + transition-property: opacity, backdrop-filter; + transition-duration: 250ms; + transition-timing-function: ease-in; + + &[data-state='open'] { + backdrop-filter: blur(6px); + opacity: 1; + transition-timing-function: ease-out; + } + + &[data-entering] { + backdrop-filter: blur(0); + opacity: 0; + } +`; + +const Title = styled(BaseDialog.Title)` + font-size: 1.25rem; +`; + +const Trigger = styled(BaseDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: + "IBM Plex Sans", + sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Close = styled(BaseDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: IBM Plex Sans, sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); diff --git a/docs/data/base/components/dialog/DialogWithTransitions.tsx.preview b/docs/data/base/components/dialog/DialogWithTransitions.tsx.preview new file mode 100644 index 0000000000..15e757d86a --- /dev/null +++ b/docs/data/base/components/dialog/DialogWithTransitions.tsx.preview @@ -0,0 +1,13 @@ + + Open + + Animated dialog + + This dialog uses CSS transitions on entry and exit. + + + Close + + + + \ No newline at end of file diff --git a/docs/data/base/components/dialog/NestedDialogs.js b/docs/data/base/components/dialog/NestedDialogs.js new file mode 100644 index 0000000000..3a42cef5d3 --- /dev/null +++ b/docs/data/base/components/dialog/NestedDialogs.js @@ -0,0 +1,163 @@ +import * as React from 'react'; +import * as BaseDialog from '@base_ui/react/Dialog'; +import { styled } from '@mui/system'; + +export default function NestedDialogs() { + return ( + + Open + + + Dialog 1 + + + Open Nested + + + Dialog 2 + + + Open Nested + + + Dialog 3 + + Close + + + + Close + + + + Close + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const Popup = styled(BaseDialog.Popup)( + ({ theme }) => ` + --transition-duration: 150ms; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: IBM Plex Sans; + padding: 16px; + z-index: 2100; + transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + visibility: hidden; + opacity: 0.5; + transition: + transform var(--transition-duration) ease-in, + opacity var(--transition-duration) ease-in, + visibility var(--transition-duration) step-end; + + &[data-state='open'] { + @starting-style { + & { + transform: translate(-50%, -35%) scale(0.8) translateY(0); + opacity: 0.5; + } + } + + visibility: visible; + opacity: 1; + transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + transition: + transform var(--transition-duration) ease-out, + opacity var(--transition-duration) ease-out, + visibility var(--transition-duration) step-start; + } +`, +); + +const Title = styled(BaseDialog.Title)` + font-size: 1.25rem; +`; + +const Trigger = styled(BaseDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: + "IBM Plex Sans", + sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Close = styled(BaseDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: IBM Plex Sans, sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Backdrop = styled(BaseDialog.Backdrop)` + background-color: rgb(0 0 0 / 0.2); + position: fixed; + inset: 0; + z-index: 2000; + backdrop-filter: blur(0); + opacity: 0; + transition-property: opacity, backdrop-filter; + transition-duration: 250ms; + transition-timing-function: ease-in; + + &[data-state='open'] { + backdrop-filter: blur(6px); + opacity: 1; + transition-timing-function: ease-out; + } + + &[data-entering] { + backdrop-filter: blur(0); + opacity: 0; + } +`; + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); diff --git a/docs/data/base/components/dialog/NestedDialogs.tsx b/docs/data/base/components/dialog/NestedDialogs.tsx new file mode 100644 index 0000000000..3a42cef5d3 --- /dev/null +++ b/docs/data/base/components/dialog/NestedDialogs.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; +import * as BaseDialog from '@base_ui/react/Dialog'; +import { styled } from '@mui/system'; + +export default function NestedDialogs() { + return ( + + Open + + + Dialog 1 + + + Open Nested + + + Dialog 2 + + + Open Nested + + + Dialog 3 + + Close + + + + Close + + + + Close + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const Popup = styled(BaseDialog.Popup)( + ({ theme }) => ` + --transition-duration: 150ms; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: IBM Plex Sans; + padding: 16px; + z-index: 2100; + transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + visibility: hidden; + opacity: 0.5; + transition: + transform var(--transition-duration) ease-in, + opacity var(--transition-duration) ease-in, + visibility var(--transition-duration) step-end; + + &[data-state='open'] { + @starting-style { + & { + transform: translate(-50%, -35%) scale(0.8) translateY(0); + opacity: 0.5; + } + } + + visibility: visible; + opacity: 1; + transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + transition: + transform var(--transition-duration) ease-out, + opacity var(--transition-duration) ease-out, + visibility var(--transition-duration) step-start; + } +`, +); + +const Title = styled(BaseDialog.Title)` + font-size: 1.25rem; +`; + +const Trigger = styled(BaseDialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: + "IBM Plex Sans", + sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Close = styled(BaseDialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: IBM Plex Sans, sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Backdrop = styled(BaseDialog.Backdrop)` + background-color: rgb(0 0 0 / 0.2); + position: fixed; + inset: 0; + z-index: 2000; + backdrop-filter: blur(0); + opacity: 0; + transition-property: opacity, backdrop-filter; + transition-duration: 250ms; + transition-timing-function: ease-in; + + &[data-state='open'] { + backdrop-filter: blur(6px); + opacity: 1; + transition-timing-function: ease-out; + } + + &[data-entering] { + backdrop-filter: blur(0); + opacity: 0; + } +`; + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.js b/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.js new file mode 100644 index 0000000000..f50a332498 --- /dev/null +++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.js @@ -0,0 +1,133 @@ +import * as React from 'react'; +import * as Dialog from '@base_ui/react/Dialog'; +import { useTheme } from '@mui/system'; + +export default function UnstyledDialogIntroduction() { + return ( + + + Subscribe + + + Subscribe + + Enter your email address to subscribe to our newsletter. + + +
+ Subscribe + Cancel +
+
+
+ +
+ ); +} + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +function Styles() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( + + ); +} diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.tsx b/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.tsx new file mode 100644 index 0000000000..f50a332498 --- /dev/null +++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/css/index.tsx @@ -0,0 +1,133 @@ +import * as React from 'react'; +import * as Dialog from '@base_ui/react/Dialog'; +import { useTheme } from '@mui/system'; + +export default function UnstyledDialogIntroduction() { + return ( + + + Subscribe + + + Subscribe + + Enter your email address to subscribe to our newsletter. + + +
+ Subscribe + Cancel +
+
+
+ +
+ ); +} + +function useIsDarkMode() { + const theme = useTheme(); + return theme.palette.mode === 'dark'; +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +function Styles() { + // Replace this with your app logic for determining dark mode + const isDarkMode = useIsDarkMode(); + + return ( + + ); +} diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.js b/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.js new file mode 100644 index 0000000000..f52c7b9977 --- /dev/null +++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.js @@ -0,0 +1,121 @@ +import * as React from 'react'; +import * as Dialog from '@base_ui/react/Dialog'; +import { styled } from '@mui/system'; + +export default function UnstyledDialogIntroduction() { + return ( + + Subscribe + + + Subscribe + + Enter your email address to subscribe to our newsletter. + + + + Subscribe + Cancel + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const TriggerButton = styled(Dialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: "IBM Plex Sans", sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Popup = styled(Dialog.Popup)( + ({ theme }) => ` + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: "IBM Plex Sans", sans-serif; + transform: translate(-50%, -50%); + padding: 16px; + z-index: 2100; +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); + +const CloseButton = styled(Dialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: "IBM Plex Sans", sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Title = styled(Dialog.Title)` + font-size: 1.25rem; +`; + +const Description = styled(Dialog.Description)``; + +const Backdrop = styled(Dialog.Backdrop)` + background: rgb(0 0 0 / 0.35); + position: fixed; + inset: 0; + backdrop-filter: blur(4px); + z-index: 2000; +`; + +const TextField = styled('input')` + padding: 8px; + border-radius: 4px; + border: 1px solid ${grey[300]}; + font-family: 'IBM Plex Sans', sans-serif; + margin: 16px 0; + width: 100%; + box-sizing: border-box; +`; diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.tsx b/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.tsx new file mode 100644 index 0000000000..f52c7b9977 --- /dev/null +++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/system/index.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import * as Dialog from '@base_ui/react/Dialog'; +import { styled } from '@mui/system'; + +export default function UnstyledDialogIntroduction() { + return ( + + Subscribe + + + Subscribe + + Enter your email address to subscribe to our newsletter. + + + + Subscribe + Cancel + + + + ); +} + +const grey = { + 900: '#0f172a', + 800: '#1e293b', + 700: '#334155', + 500: '#64748b', + 300: '#cbd5e1', + 200: '#e2e8f0', + 100: '#f1f5f9', + 50: '#f8fafc', +}; + +const TriggerButton = styled(Dialog.Trigger)( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + color: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + padding: 8px 16px; + border-radius: 4px; + border: none; + font-family: "IBM Plex Sans", sans-serif; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[200] : grey[700]}; + } +`, +); + +const Popup = styled(Dialog.Popup)( + ({ theme }) => ` + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[100]}; + min-width: 400px; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 18px 50px -10px; + position: fixed; + top: 50%; + left: 50%; + font-family: "IBM Plex Sans", sans-serif; + transform: translate(-50%, -50%); + padding: 16px; + z-index: 2100; +`, +); + +const Controls = styled('div')( + ({ theme }) => ` + display: flex; + flex-direction: row-reverse; + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + gap: 8px; + padding: 16px; + margin: 32px -16px -16px; +`, +); + +const CloseButton = styled(Dialog.Close)( + ({ theme }) => ` + background-color: transparent; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[300] : grey[500]}; + color: ${theme.palette.mode === 'dark' ? grey[50] : grey[900]}; + padding: 8px 16px; + border-radius: 4px; + font-family: "IBM Plex Sans", sans-serif; + min-width: 80px; + + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; + } +`, +); + +const Title = styled(Dialog.Title)` + font-size: 1.25rem; +`; + +const Description = styled(Dialog.Description)``; + +const Backdrop = styled(Dialog.Backdrop)` + background: rgb(0 0 0 / 0.35); + position: fixed; + inset: 0; + backdrop-filter: blur(4px); + z-index: 2000; +`; + +const TextField = styled('input')` + padding: 8px; + border-radius: 4px; + border: 1px solid ${grey[300]}; + font-family: 'IBM Plex Sans', sans-serif; + margin: 16px 0; + width: 100%; + box-sizing: border-box; +`; diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.js b/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.js new file mode 100644 index 0000000000..ddcd4a4dc3 --- /dev/null +++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.js @@ -0,0 +1,87 @@ +import * as React from 'react'; +import * as Dialog from '@base_ui/react/Dialog'; + +export default function UnstyledDialogIntroduction() { + return ( + + Subscribe + + + Subscribe + + Enter your email address to subscribe to our newsletter. + + + + Subscribe + Cancel + + + + ); +} + +function TriggerButton(props) { + const className = ` + bg-slate-900 dark:bg-slate-50 text-slate-50 dark:text-slate-900 + py-2 px-4 rounded min-w-[80px] border-none font-sans + hover:bg-slate-700 dark:hover:bg-slate-200`; + + return ; +} + +function Popup(props) { + const className = ` + bg-slate-50 dark:bg-slate-900 border-[1px] border-solid border-slate-100 dark:border-slate-700 + min-w-[400px] rounded shadow-xl fixed top-2/4 left-2/4 z-[2100] + -translate-x-2/4 -translate-y-2/4 p-4`; + + return ; +} + +function Controls(props) { + return ( +
+ ); +} + +function CloseButton(props) { + const className = ` + bg-transparent border-[1px] border-solid border-slate-500 dark:border-slate-300 + text-slate-900 dark:text-slate-50 py-2 px-4 rounded font-sans min-w-[80px] + hover:bg-slate-200 dark:hover:bg-slate-700`; + + return ; +} + +function Title(props) { + return ; +} + +function Description(props) { + return ; +} + +function Backdrop(props) { + return ( + + ); +} + +function TextField(props) { + const className = ` + w-full p-2 mt-4 font-sans + border-[1px] border-solid border-slate-300 dark:border-slate-700 rounded + `; + return ; +} diff --git a/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.tsx b/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.tsx new file mode 100644 index 0000000000..ecae78b79e --- /dev/null +++ b/docs/data/base/components/dialog/UnstyledDialogIntroduction/tailwind/index.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import * as Dialog from '@base_ui/react/Dialog'; + +export default function UnstyledDialogIntroduction() { + return ( + + Subscribe + + + Subscribe + + Enter your email address to subscribe to our newsletter. + + + + Subscribe + Cancel + + + + ); +} + +function TriggerButton(props: Dialog.TriggerProps) { + const className = ` + bg-slate-900 dark:bg-slate-50 text-slate-50 dark:text-slate-900 + py-2 px-4 rounded min-w-[80px] border-none font-sans + hover:bg-slate-700 dark:hover:bg-slate-200`; + + return ; +} + +function Popup(props: Dialog.PopupProps) { + const className = ` + bg-slate-50 dark:bg-slate-900 border-[1px] border-solid border-slate-100 dark:border-slate-700 + min-w-[400px] rounded shadow-xl fixed top-2/4 left-2/4 z-[2100] + -translate-x-2/4 -translate-y-2/4 p-4`; + + return ; +} + +function Controls(props: React.ComponentPropsWithoutRef<'div'>) { + return ( +
+ ); +} + +function CloseButton(props: Dialog.CloseProps) { + const className = ` + bg-transparent border-[1px] border-solid border-slate-500 dark:border-slate-300 + text-slate-900 dark:text-slate-50 py-2 px-4 rounded font-sans min-w-[80px] + hover:bg-slate-200 dark:hover:bg-slate-700`; + + return ; +} + +function Title(props: Dialog.TitleProps) { + return ; +} + +function Description(props: Dialog.DescriptionProps) { + return ; +} + +function Backdrop(props: Dialog.BackdropProps) { + return ( + + ); +} + +function TextField(props: React.ComponentPropsWithoutRef<'input'>) { + const className = ` + w-full p-2 mt-4 font-sans + border-[1px] border-solid border-slate-300 dark:border-slate-700 rounded + `; + return ; +} diff --git a/docs/data/base/components/dialog/dialog.md b/docs/data/base/components/dialog/dialog.md new file mode 100644 index 0000000000..ac4f7c7e66 --- /dev/null +++ b/docs/data/base/components/dialog/dialog.md @@ -0,0 +1,320 @@ +--- +productId: base-ui +title: React Dialog component and hook +components: DialogBackdrop, DialogClose, DialogDescription, DialogPopup, DialogRoot, DialogTitle, DialogTrigger +hooks: useDialogClose, useDialogPopup, useDialogRoot, useDialogTrigger +githubLabel: 'component: dialog' +waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ +--- + +# Dialog + +

Dialogs inform users about a task and can contain critical information, require decisions, or involve multiple tasks.

+ +{{"component": "@mui/docs/ComponentLinkHeader", "design": false}} + +{{"component": "modules/components/ComponentPageTabs.js"}} + +{{"demo": "UnstyledDialogIntroduction", "defaultCodeOpen": false, "bg": "gradient"}} + +## Installation + +Base UI components are all available as a single package. + + + +```bash npm +npm install @base_ui/react +``` + +```bash yarn +yarn add @base_ui/react +``` + +```bash pnpm +pnpm add @base_ui/react +``` + + + +Once you have the package installed, import the component. + +```ts +import * as Dialog from '@base_ui/react/Dialog'; +``` + +## Anatomy + +Dialogs are implemented using a collection of related components: + +- `` is a top-level component that facilitates communication between other components. It does not render to the DOM. +- `` is the dialog panel itself. +- `` is the background element appearing when a popup is visible. Use it to indicate that the page is inert when using a modal dialog. The Backdrop must be a sibling of the Popup component. It is mandatory for modal dialogs. +- `` is the component (a button by default) that, when clicked, shows the popup. When it's not provided, the visibility of the Dialog can be controlled with its `open` prop (see [Controlled vs. uncontrolled behavior](#controlled-vs-uncontrolled-behavior)). +- `` renders a button that closes the popup. You can attach your own click handlers to it to perform additional actions. +- `` is an header element displaying the title of the dialog. It is referenced in the Dialog's ARIA attributes to properly announce the dialog. +- `` is an element describing of the dialog. It is referenced in the Dialog's ARIA attributes to properly announce the dialog. + +```tsx + + + + + + + + + + + +``` + +## Modal and non-modal dialogs + +Dialogs can be either modal (rendering the rest of the page inert) or non-modal. +A non-modal dialog can be used to implement tool windows. + +The `modal` prop of the `` controls this. +By default Dialogs are modal. + +```tsx +{/* ... */} +``` + +:::warning +To make the Dialog fully modal, you must have a Backdrop component and style it so it covers the entire viewport, blocking pointer interaction with other elements on the page. +::: + +## Closing the dialog + +The default way to close the dialog is clicking on the `` component. +Dialogs also close when the user clicks outside of them or presses the Esc key. + +Closing on outside click can be disabled with a `dismissible` prop on the `Dialog.Root`: + +```tsx +{/* ... */} +``` + +## Controlled vs. uncontrolled behavior + +The simplest way to control the visibility of the dialog is to use the `` and `` components. + +You can set the initial state with the `defaultOpen` prop. + +```tsx + + Open + + Demo dialog + Close + + +``` + +Doing so ensures that the accessibity attributes are set correctly so that the trigger button is approriately announced by assistive technologies. + +If you need to control the visibility programmatically from the outside, use the `value` prop. +You can still use the `` and `` components (though it's not necessary), but you need to make sure to create a handler for the `onOpenChange` event and update the state manually. + +```tsx +const [open, setOpen] = React.useState(false); + +return ( + + Open + + Demo dialog + Close + + +); +``` + +## Nested dialogs + +A dialog can open another dialog. +At times, it may be useful to know how may open sub-dialogs a given dialog has. +One example of this could be styling the bottom dialog in a way they appear below the top-most one. + +The number of open child dialogs is present in the `data-nested-dialogs` attribute and in the `--nested-dialogs` CSS variable on the `` component. + +{{"demo": "NestedDialogs.js"}} + +Note that when dialogs are nested, only the bottom-most backdrop is rendered. + +## Animation + +The `` and `` components support transitions on entry and exit. + +CSS animations and transitions are supported out of the box. +If a component has a transition or animation applied to it when it closes, it will be unmounted only after the animation finishes. + +As this detection of exit animations requires an extra render, you may opt out of it by setting the `animated` prop on Root to `false`. +We also recommend doing so in automated tests, to avoid asynchronous behavior and make testing easier. + +Alternatively, you can use JS-based animations with a library like framer-motion, React Spring, or similar. +With this approach set the `keepMounted` to `true` and let the animation library control mounting and unmounting. + +### CSS transitions + +Here is an example of how to apply a symmetric scale and fade transition with the default conditionally-rendered behavior: + +```jsx +Dialog +``` + +```css +.DialogPopup { + transition-property: opacity, transform; + transition-duration: 0.2s; + /* Represents the final styles once exited */ + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); +} + +/* Represents the final styles once entered */ +.DialogPopup[data-state='open'] { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + +/* Represents the initial styles when entering */ +.DialogPopup[data-entering] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); +} +``` + +Styles need to be applied in three states: + +- The exiting styles, placed on the base element class +- The open styles, placed on the base element class with `[data-state="open"]` +- The entering styles, placed on the base element class with `[data-entering]` + +{{"demo": "DialogWithTransitions.js"}} + +In newer browsers, there is a feature called `@starting-style` which allows transitions to occur on open for conditionally-mounted components: + +```css +/* Base UI API - Polyfill */ +.DialogPopup[data-entering] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); +} + +/* Official Browser API - no Firefox support as of May 2024 */ +@starting-style { + .DialogPopup[data-state='open'] { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +} +``` + +### CSS animations + +CSS animations can also be used, requiring only two separate declarations: + +```css +@keyframes scale-in { + from { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +} + +@keyframes scale-out { + to { + opacity: 0; + transform: translate(-50%, -35%) scale(0.8); + } +} + +.DialogPopup { + animation: scale-in 0.2s forwards; +} + +.DialogPopup[data-exiting] { + animation: scale-out 0.2s forwards; +} +``` + +### JavaScript animations + +The `keepMounted` prop lets an external library control the mounting, for example `framer-motion`'s `AnimatePresence` component. + +```js +function App() { + const [open, setOpen] = useState(false); + return ( + + Trigger + + {open && ( + + } + > + Dialog + + )} + + + ); +} +``` + +### Animation states + +Four states are available as data attributes to animate the dialog, which enables full control depending on whether the popup is being animated with CSS transitions or animations, JavaScript, or is using the `keepMounted` prop. + +- `[data-state="open"]` - `open` state is `true`. +- `[data-state="closed"]` - `open` state is `false`. Can still be mounted to the DOM if closing. +- `[data-entering]` - the popup was just inserted to the DOM. The attribute is removed 1 animation frame later. Enables "starting styles" upon insertion for conditional rendering. +- `[data-exiting]` - the popup is in the process of being removed from the DOM, but is still mounted. + +## Composing a custom React component + +Use the `render` prop to override the rendered element: + +```jsx +} /> +// or + } /> +``` + +## Accessibility + +Using the `` sets the required accessibility attributes on the trigger button. +If you prefer controlling the open state differently, you need to apply these attributes on your own: + +```tsx +const [open, setOpen] = React.useState(false); + +return ( +
+ + + + + Demo dialog + Close + + +
+); +``` diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts index 9f049a2afa..38036691a8 100644 --- a/docs/data/base/pages.ts +++ b/docs/data/base/pages.ts @@ -43,13 +43,15 @@ const pages: readonly MuiPage[] = [ { pathname: '/base-ui/react-tooltip', title: 'Tooltip' }, ], }, - // { - // pathname: '/base-ui/components/feedback', - // subheader: 'feedback', - // children: [ - // { pathname: '/base-ui/react-snackbar', title: 'Snackbar' }, - // ], - // }, + { + pathname: '/base-ui/components/feedback', + subheader: 'feedback', + children: [ + { pathname: '/base-ui/react-alert-dialog', title: 'Alert Dialog' }, + { pathname: '/base-ui/react-dialog', title: 'Dialog' }, + // { pathname: '/base-ui/react-snackbar', title: 'Snackbar' }, + ], + }, // { // pathname: '/base-ui/components/surfaces', // subheader: 'surfaces', diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 17e5e1f9eb..1f00bed512 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -1,4 +1,32 @@ module.exports = [ + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-backdrop', + title: 'AlertDialogBackdrop', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-close', + title: 'AlertDialogClose', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-description', + title: 'AlertDialogDescription', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-popup', + title: 'AlertDialogPopup', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-root', + title: 'AlertDialogRoot', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-title', + title: 'AlertDialogTitle', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-trigger', + title: 'AlertDialogTrigger', + }, { pathname: '/base-ui/react-badge/components-api/#badge', title: 'Badge' }, { pathname: '/base-ui/react-button/components-api/#button', title: 'Button' }, { @@ -22,6 +50,34 @@ module.exports = [ pathname: '/base-ui/react-transitions/components-api/#css-transition', title: 'CssTransition', }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-backdrop', + title: 'DialogBackdrop', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-close', + title: 'DialogClose', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-description', + title: 'DialogDescription', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-popup', + title: 'DialogPopup', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-root', + title: 'DialogRoot', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-title', + title: 'DialogTitle', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-trigger', + title: 'DialogTrigger', + }, { pathname: '/base-ui/react-menu/components-api/#dropdown', title: 'Dropdown' }, { pathname: '/base-ui/react-focus-trap/components-api/#focus-trap', @@ -141,6 +197,22 @@ module.exports = [ pathname: '/base-ui/react-checkbox/hooks-api/#use-checkbox-root', title: 'useCheckboxRoot', }, + { + pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-close', + title: 'useDialogClose', + }, + { + pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-popup', + title: 'useDialogPopup', + }, + { + pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-root', + title: 'useDialogRoot', + }, + { + pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-trigger', + title: 'useDialogTrigger', + }, { pathname: '/base-ui/react-menu/hooks-api/#use-dropdown', title: 'useDropdown' }, { pathname: '/base-ui/react-form-control/hooks-api/#use-form-control-context', diff --git a/docs/pages/base-ui/api/alert-dialog-backdrop.json b/docs/pages/base-ui/api/alert-dialog-backdrop.json new file mode 100644 index 0000000000..ced30b97d5 --- /dev/null +++ b/docs/pages/base-ui/api/alert-dialog-backdrop.json @@ -0,0 +1,20 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "AlertDialogBackdrop", + "imports": [ + "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogBackdrop = AlertDialog.Backdrop;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "AlertDialogBackdrop", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/AlertDialog/Backdrop/AlertDialogBackdrop.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/alert-dialog-close.json b/docs/pages/base-ui/api/alert-dialog-close.json new file mode 100644 index 0000000000..3ae0ccd4b4 --- /dev/null +++ b/docs/pages/base-ui/api/alert-dialog-close.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "AlertDialogClose", + "imports": [ + "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogClose = AlertDialog.Close;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "AlertDialogClose", + "forwardsRefTo": "HTMLButtonElement", + "filename": "/packages/mui-base/src/AlertDialog/Close/AlertDialogClose.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/alert-dialog-description.json b/docs/pages/base-ui/api/alert-dialog-description.json new file mode 100644 index 0000000000..2875bcc0a9 --- /dev/null +++ b/docs/pages/base-ui/api/alert-dialog-description.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "AlertDialogDescription", + "imports": [ + "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogDescription = AlertDialog.Description;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "AlertDialogDescription", + "forwardsRefTo": "HTMLParagraphElement", + "filename": "/packages/mui-base/src/AlertDialog/Description/AlertDialogDescription.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/alert-dialog-popup.json b/docs/pages/base-ui/api/alert-dialog-popup.json new file mode 100644 index 0000000000..ea9878c52a --- /dev/null +++ b/docs/pages/base-ui/api/alert-dialog-popup.json @@ -0,0 +1,21 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "container": { "type": { "name": "union", "description": "HTML element
| ref" } }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "AlertDialogPopup", + "imports": [ + "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogPopup = AlertDialog.Popup;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "AlertDialogPopup", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/AlertDialog/Popup/AlertDialogPopup.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/alert-dialog-root.json b/docs/pages/base-ui/api/alert-dialog-root.json new file mode 100644 index 0000000000..a733c1d74f --- /dev/null +++ b/docs/pages/base-ui/api/alert-dialog-root.json @@ -0,0 +1,20 @@ +{ + "props": { + "animated": { "type": { "name": "bool" }, "default": "true" }, + "defaultOpen": { "type": { "name": "bool" } }, + "onOpenChange": { "type": { "name": "func" } }, + "open": { "type": { "name": "bool" } } + }, + "name": "AlertDialogRoot", + "imports": [ + "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogRoot = AlertDialog.Root;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": null, + "muiName": "AlertDialogRoot", + "filename": "/packages/mui-base/src/AlertDialog/Root/AlertDialogRoot.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/alert-dialog-title.json b/docs/pages/base-ui/api/alert-dialog-title.json new file mode 100644 index 0000000000..cbea492a58 --- /dev/null +++ b/docs/pages/base-ui/api/alert-dialog-title.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "AlertDialogTitle", + "imports": [ + "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogTitle = AlertDialog.Title;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "AlertDialogTitle", + "forwardsRefTo": "HTMLHeadingElement", + "filename": "/packages/mui-base/src/AlertDialog/Title/AlertDialogTitle.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/alert-dialog-trigger.json b/docs/pages/base-ui/api/alert-dialog-trigger.json new file mode 100644 index 0000000000..a2ba533cf7 --- /dev/null +++ b/docs/pages/base-ui/api/alert-dialog-trigger.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "AlertDialogTrigger", + "imports": [ + "import * as AlertDialog from '@base_ui/react/AlertDialog';\nconst AlertDialogTrigger = AlertDialog.Trigger;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "AlertDialogTrigger", + "forwardsRefTo": "HTMLButtonElement", + "filename": "/packages/mui-base/src/AlertDialog/Trigger/AlertDialogTrigger.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/dialog-backdrop.json b/docs/pages/base-ui/api/dialog-backdrop.json new file mode 100644 index 0000000000..1256870dac --- /dev/null +++ b/docs/pages/base-ui/api/dialog-backdrop.json @@ -0,0 +1,20 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "DialogBackdrop", + "imports": [ + "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogBackdrop = Dialog.Backdrop;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "DialogBackdrop", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Dialog/Backdrop/DialogBackdrop.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/dialog-close.json b/docs/pages/base-ui/api/dialog-close.json new file mode 100644 index 0000000000..0f0791e752 --- /dev/null +++ b/docs/pages/base-ui/api/dialog-close.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "DialogClose", + "imports": [ + "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogClose = Dialog.Close;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "DialogClose", + "forwardsRefTo": "HTMLButtonElement", + "filename": "/packages/mui-base/src/Dialog/Close/DialogClose.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/dialog-description.json b/docs/pages/base-ui/api/dialog-description.json new file mode 100644 index 0000000000..995ec0670b --- /dev/null +++ b/docs/pages/base-ui/api/dialog-description.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "DialogDescription", + "imports": [ + "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogDescription = Dialog.Description;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "DialogDescription", + "forwardsRefTo": "HTMLParagraphElement", + "filename": "/packages/mui-base/src/Dialog/Description/DialogDescription.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/dialog-popup.json b/docs/pages/base-ui/api/dialog-popup.json new file mode 100644 index 0000000000..6cb5afbd1f --- /dev/null +++ b/docs/pages/base-ui/api/dialog-popup.json @@ -0,0 +1,21 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "container": { "type": { "name": "union", "description": "HTML element
| ref" } }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "DialogPopup", + "imports": [ + "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogPopup = Dialog.Popup;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "DialogPopup", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Dialog/Popup/DialogPopup.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/dialog-root.json b/docs/pages/base-ui/api/dialog-root.json new file mode 100644 index 0000000000..478fe8acbe --- /dev/null +++ b/docs/pages/base-ui/api/dialog-root.json @@ -0,0 +1,20 @@ +{ + "props": { + "animated": { "type": { "name": "bool" }, "default": "true" }, + "defaultOpen": { "type": { "name": "bool" } }, + "dismissible": { "type": { "name": "bool" }, "default": "true" }, + "modal": { "type": { "name": "bool" }, "default": "true" }, + "onOpenChange": { "type": { "name": "func" } }, + "open": { "type": { "name": "bool" } } + }, + "name": "DialogRoot", + "imports": ["import * as Dialog from '@base_ui/react/Dialog';\nconst DialogRoot = Dialog.Root;"], + "classes": [], + "spread": true, + "themeDefaultProps": null, + "muiName": "DialogRoot", + "filename": "/packages/mui-base/src/Dialog/Root/DialogRoot.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/dialog-title.json b/docs/pages/base-ui/api/dialog-title.json new file mode 100644 index 0000000000..c686160ca8 --- /dev/null +++ b/docs/pages/base-ui/api/dialog-title.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "DialogTitle", + "imports": [ + "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogTitle = Dialog.Title;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "DialogTitle", + "forwardsRefTo": "HTMLHeadingElement", + "filename": "/packages/mui-base/src/Dialog/Title/DialogTitle.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/dialog-trigger.json b/docs/pages/base-ui/api/dialog-trigger.json new file mode 100644 index 0000000000..04cc9b2212 --- /dev/null +++ b/docs/pages/base-ui/api/dialog-trigger.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, + "name": "DialogTrigger", + "imports": [ + "import * as Dialog from '@base_ui/react/Dialog';\nconst DialogTrigger = Dialog.Trigger;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "DialogTrigger", + "forwardsRefTo": "HTMLButtonElement", + "filename": "/packages/mui-base/src/Dialog/Trigger/DialogTrigger.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/use-dialog-backdrop.json b/docs/pages/base-ui/api/use-dialog-backdrop.json new file mode 100644 index 0000000000..586c0c6532 --- /dev/null +++ b/docs/pages/base-ui/api/use-dialog-backdrop.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useDialogBackdrop", + "filename": "/packages/mui-base/src/Dialog/Backdrop/useDialogBackdrop.ts", + "imports": ["import { useDialogBackdrop } from '@base_ui/react/Dialog';"], + "demos": "
    " +} diff --git a/docs/pages/base-ui/api/use-dialog-close.json b/docs/pages/base-ui/api/use-dialog-close.json new file mode 100644 index 0000000000..e211a103dc --- /dev/null +++ b/docs/pages/base-ui/api/use-dialog-close.json @@ -0,0 +1,22 @@ +{ + "parameters": { + "onOpenChange": { + "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" }, + "required": true + }, + "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true } + }, + "returnValue": { + "getRootProps": { + "type": { + "name": "(externalProps: React.HTMLAttributes<any>) => React.HTMLAttributes<any>", + "description": "(externalProps: React.HTMLAttributes<any>) => React.HTMLAttributes<any>" + }, + "required": true + } + }, + "name": "useDialogClose", + "filename": "/packages/mui-base/src/Dialog/Close/useDialogClose.ts", + "imports": ["import { useDialogClose } from '@base_ui/react/Dialog';"], + "demos": "" +} diff --git a/docs/pages/base-ui/api/use-dialog-popup.json b/docs/pages/base-ui/api/use-dialog-popup.json new file mode 100644 index 0000000000..de4a07c643 --- /dev/null +++ b/docs/pages/base-ui/api/use-dialog-popup.json @@ -0,0 +1,58 @@ +{ + "parameters": { + "animated": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "descriptionElementId": { + "type": { "name": "string | undefined", "description": "string | undefined" }, + "required": true + }, + "isTopmost": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "modal": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "onOpenChange": { + "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" }, + "required": true + }, + "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "ref": { + "type": { + "name": "React.Ref<HTMLElement>", + "description": "React.Ref<HTMLElement>" + }, + "required": true + }, + "setPopupElementId": { + "type": { + "name": "(id: string | undefined) => void", + "description": "(id: string | undefined) => void" + }, + "required": true + }, + "titleElementId": { + "type": { "name": "string | undefined", "description": "string | undefined" }, + "required": true + }, + "dismissible": { "type": { "name": "boolean", "description": "boolean" }, "default": "true" }, + "id": { "type": { "name": "string", "description": "string" } } + }, + "returnValue": { + "floatingContext": { + "type": { "name": "FloatingContext", "description": "FloatingContext" }, + "required": true + }, + "getRootProps": { + "type": { + "name": "(externalProps: React.ComponentPropsWithRef<'div'>) => React.ComponentPropsWithRef<'div'>", + "description": "(externalProps: React.ComponentPropsWithRef<'div'>) => React.ComponentPropsWithRef<'div'>" + }, + "required": true + }, + "mounted": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "transitionStatus": { + "type": { "name": "TransitionStatus", "description": "TransitionStatus" }, + "required": true + } + }, + "name": "useDialogPopup", + "filename": "/packages/mui-base/src/Dialog/Popup/useDialogPopup.tsx", + "imports": ["import { useDialogPopup } from '@base_ui/react/Dialog';"], + "demos": "" +} diff --git a/docs/pages/base-ui/api/use-dialog-root.json b/docs/pages/base-ui/api/use-dialog-root.json new file mode 100644 index 0000000000..322d329f22 --- /dev/null +++ b/docs/pages/base-ui/api/use-dialog-root.json @@ -0,0 +1,82 @@ +{ + "parameters": { + "animated": { "type": { "name": "boolean", "description": "boolean" }, "default": "true" }, + "defaultOpen": { "type": { "name": "boolean", "description": "boolean" } }, + "dismissible": { "type": { "name": "boolean", "description": "boolean" }, "default": "true" }, + "modal": { "type": { "name": "boolean", "description": "boolean" }, "default": "true" }, + "onNestedDialogClose": { "type": { "name": "() => void", "description": "() => void" } }, + "onNestedDialogOpen": { + "type": { + "name": "(ownChildrenCount: number) => void", + "description": "(ownChildrenCount: number) => void" + } + }, + "onOpenChange": { + "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" } + }, + "open": { "type": { "name": "boolean", "description": "boolean" } } + }, + "returnValue": { + "descriptionElementId": { + "type": { "name": "string | undefined", "description": "string | undefined" }, + "required": true + }, + "modal": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "nestedOpenDialogCount": { + "type": { "name": "number", "description": "number" }, + "required": true + }, + "onOpenChange": { + "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" }, + "required": true + }, + "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "popupElementId": { + "type": { "name": "string | undefined", "description": "string | undefined" }, + "required": true + }, + "setBackdropPresent": { + "type": { + "name": "(present: boolean) => void", + "description": "(present: boolean) => void" + }, + "required": true + }, + "setDescriptionElementId": { + "type": { + "name": "(elementId: string | undefined) => void", + "description": "(elementId: string | undefined) => void" + }, + "required": true + }, + "setPopupElementId": { + "type": { + "name": "(elementId: string | undefined) => void", + "description": "(elementId: string | undefined) => void" + }, + "required": true + }, + "setTitleElementId": { + "type": { + "name": "(elementId: string | undefined) => void", + "description": "(elementId: string | undefined) => void" + }, + "required": true + }, + "titleElementId": { + "type": { "name": "string | undefined", "description": "string | undefined" }, + "required": true + }, + "onNestedDialogClose": { "type": { "name": "() => void", "description": "() => void" } }, + "onNestedDialogOpen": { + "type": { + "name": "(ownChildrenCount: number) => void", + "description": "(ownChildrenCount: number) => void" + } + } + }, + "name": "useDialogRoot", + "filename": "/packages/mui-base/src/Dialog/Root/useDialogRoot.ts", + "imports": ["import { useDialogRoot } from '@base_ui/react/Dialog';"], + "demos": "" +} diff --git a/docs/pages/base-ui/api/use-dialog-trigger.json b/docs/pages/base-ui/api/use-dialog-trigger.json new file mode 100644 index 0000000000..db4e01530d --- /dev/null +++ b/docs/pages/base-ui/api/use-dialog-trigger.json @@ -0,0 +1,26 @@ +{ + "parameters": { + "onOpenChange": { + "type": { "name": "(open: boolean) => void", "description": "(open: boolean) => void" }, + "required": true + }, + "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, + "popupElementId": { + "type": { "name": "string | undefined", "description": "string | undefined" }, + "required": true + } + }, + "returnValue": { + "getRootProps": { + "type": { + "name": "(externalProps?: React.HTMLAttributes<any>) => React.HTMLAttributes<any>", + "description": "(externalProps?: React.HTMLAttributes<any>) => React.HTMLAttributes<any>" + }, + "required": true + } + }, + "name": "useDialogTrigger", + "filename": "/packages/mui-base/src/Dialog/Trigger/useDialogTrigger.ts", + "imports": ["import { useDialogTrigger } from '@base_ui/react/Dialog';"], + "demos": "" +} diff --git a/docs/pages/base-ui/api/use-scroll-lock.json b/docs/pages/base-ui/api/use-scroll-lock.json new file mode 100644 index 0000000000..fd34aaa559 --- /dev/null +++ b/docs/pages/base-ui/api/use-scroll-lock.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useScrollLock", + "filename": "/packages/mui-base/src/utils/useScrollLock.ts", + "imports": ["import { useScrollLock } from '@base_ui/react/utils';"], + "demos": "
      " +} diff --git a/docs/pages/base-ui/react-alert-dialog/[docsTab]/index.js b/docs/pages/base-ui/react-alert-dialog/[docsTab]/index.js new file mode 100644 index 0000000000..04a6930be0 --- /dev/null +++ b/docs/pages/base-ui/react-alert-dialog/[docsTab]/index.js @@ -0,0 +1,106 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/alert-dialog/alert-dialog.md?@mui/markdown'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import AlertDialogBackdropApiJsonPageContent from '../../api/alert-dialog-backdrop.json'; +import AlertDialogCloseApiJsonPageContent from '../../api/alert-dialog-close.json'; +import AlertDialogDescriptionApiJsonPageContent from '../../api/alert-dialog-description.json'; +import AlertDialogPopupApiJsonPageContent from '../../api/alert-dialog-popup.json'; +import AlertDialogRootApiJsonPageContent from '../../api/alert-dialog-root.json'; +import AlertDialogTitleApiJsonPageContent from '../../api/alert-dialog-title.json'; +import AlertDialogTriggerApiJsonPageContent from '../../api/alert-dialog-trigger.json'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; + +export const getStaticPaths = () => { + return { + paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }], + fallback: false, // can also be true or 'blocking' + }; +}; + +export const getStaticProps = () => { + const AlertDialogBackdropApiReq = require.context( + 'docs-base/translations/api-docs/alert-dialog-backdrop', + false, + /\.\/alert-dialog-backdrop.*.json$/, + ); + const AlertDialogBackdropApiDescriptions = mapApiPageTranslations(AlertDialogBackdropApiReq); + + const AlertDialogCloseApiReq = require.context( + 'docs-base/translations/api-docs/alert-dialog-close', + false, + /\.\/alert-dialog-close.*.json$/, + ); + const AlertDialogCloseApiDescriptions = mapApiPageTranslations(AlertDialogCloseApiReq); + + const AlertDialogDescriptionApiReq = require.context( + 'docs-base/translations/api-docs/alert-dialog-description', + false, + /\.\/alert-dialog-description.*.json$/, + ); + const AlertDialogDescriptionApiDescriptions = mapApiPageTranslations( + AlertDialogDescriptionApiReq, + ); + + const AlertDialogPopupApiReq = require.context( + 'docs-base/translations/api-docs/alert-dialog-popup', + false, + /\.\/alert-dialog-popup.*.json$/, + ); + const AlertDialogPopupApiDescriptions = mapApiPageTranslations(AlertDialogPopupApiReq); + + const AlertDialogRootApiReq = require.context( + 'docs-base/translations/api-docs/alert-dialog-root', + false, + /\.\/alert-dialog-root.*.json$/, + ); + const AlertDialogRootApiDescriptions = mapApiPageTranslations(AlertDialogRootApiReq); + + const AlertDialogTitleApiReq = require.context( + 'docs-base/translations/api-docs/alert-dialog-title', + false, + /\.\/alert-dialog-title.*.json$/, + ); + const AlertDialogTitleApiDescriptions = mapApiPageTranslations(AlertDialogTitleApiReq); + + const AlertDialogTriggerApiReq = require.context( + 'docs-base/translations/api-docs/alert-dialog-trigger', + false, + /\.\/alert-dialog-trigger.*.json$/, + ); + const AlertDialogTriggerApiDescriptions = mapApiPageTranslations(AlertDialogTriggerApiReq); + + return { + props: { + componentsApiDescriptions: { + AlertDialogBackdrop: AlertDialogBackdropApiDescriptions, + AlertDialogClose: AlertDialogCloseApiDescriptions, + AlertDialogDescription: AlertDialogDescriptionApiDescriptions, + AlertDialogPopup: AlertDialogPopupApiDescriptions, + AlertDialogRoot: AlertDialogRootApiDescriptions, + AlertDialogTitle: AlertDialogTitleApiDescriptions, + AlertDialogTrigger: AlertDialogTriggerApiDescriptions, + }, + componentsApiPageContents: { + AlertDialogBackdrop: AlertDialogBackdropApiJsonPageContent, + AlertDialogClose: AlertDialogCloseApiJsonPageContent, + AlertDialogDescription: AlertDialogDescriptionApiJsonPageContent, + AlertDialogPopup: AlertDialogPopupApiJsonPageContent, + AlertDialogRoot: AlertDialogRootApiJsonPageContent, + AlertDialogTitle: AlertDialogTitleApiJsonPageContent, + AlertDialogTrigger: AlertDialogTriggerApiJsonPageContent, + }, + hooksApiDescriptions: {}, + hooksApiPageContents: {}, + }, + }; +}; diff --git a/docs/pages/base-ui/react-alert-dialog/index.js b/docs/pages/base-ui/react-alert-dialog/index.js new file mode 100644 index 0000000000..fe63bb661a --- /dev/null +++ b/docs/pages/base-ui/react-alert-dialog/index.js @@ -0,0 +1,13 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/alert-dialog/alert-dialog.md?@mui/markdown'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; diff --git a/docs/pages/base-ui/react-dialog/[docsTab]/index.js b/docs/pages/base-ui/react-dialog/[docsTab]/index.js new file mode 100644 index 0000000000..db2a57a965 --- /dev/null +++ b/docs/pages/base-ui/react-dialog/[docsTab]/index.js @@ -0,0 +1,146 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/dialog/dialog.md?@mui/markdown'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import DialogBackdropApiJsonPageContent from '../../api/dialog-backdrop.json'; +import DialogCloseApiJsonPageContent from '../../api/dialog-close.json'; +import DialogDescriptionApiJsonPageContent from '../../api/dialog-description.json'; +import DialogPopupApiJsonPageContent from '../../api/dialog-popup.json'; +import DialogRootApiJsonPageContent from '../../api/dialog-root.json'; +import DialogTitleApiJsonPageContent from '../../api/dialog-title.json'; +import DialogTriggerApiJsonPageContent from '../../api/dialog-trigger.json'; +import useDialogCloseApiJsonPageContent from '../../api/use-dialog-close.json'; +import useDialogPopupApiJsonPageContent from '../../api/use-dialog-popup.json'; +import useDialogRootApiJsonPageContent from '../../api/use-dialog-root.json'; +import useDialogTriggerApiJsonPageContent from '../../api/use-dialog-trigger.json'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; + +export const getStaticPaths = () => { + return { + paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }], + fallback: false, // can also be true or 'blocking' + }; +}; + +export const getStaticProps = () => { + const DialogBackdropApiReq = require.context( + 'docs-base/translations/api-docs/dialog-backdrop', + false, + /\.\/dialog-backdrop.*.json$/, + ); + const DialogBackdropApiDescriptions = mapApiPageTranslations(DialogBackdropApiReq); + + const DialogCloseApiReq = require.context( + 'docs-base/translations/api-docs/dialog-close', + false, + /\.\/dialog-close.*.json$/, + ); + const DialogCloseApiDescriptions = mapApiPageTranslations(DialogCloseApiReq); + + const DialogDescriptionApiReq = require.context( + 'docs-base/translations/api-docs/dialog-description', + false, + /\.\/dialog-description.*.json$/, + ); + const DialogDescriptionApiDescriptions = mapApiPageTranslations(DialogDescriptionApiReq); + + const DialogPopupApiReq = require.context( + 'docs-base/translations/api-docs/dialog-popup', + false, + /\.\/dialog-popup.*.json$/, + ); + const DialogPopupApiDescriptions = mapApiPageTranslations(DialogPopupApiReq); + + const DialogRootApiReq = require.context( + 'docs-base/translations/api-docs/dialog-root', + false, + /\.\/dialog-root.*.json$/, + ); + const DialogRootApiDescriptions = mapApiPageTranslations(DialogRootApiReq); + + const DialogTitleApiReq = require.context( + 'docs-base/translations/api-docs/dialog-title', + false, + /\.\/dialog-title.*.json$/, + ); + const DialogTitleApiDescriptions = mapApiPageTranslations(DialogTitleApiReq); + + const DialogTriggerApiReq = require.context( + 'docs-base/translations/api-docs/dialog-trigger', + false, + /\.\/dialog-trigger.*.json$/, + ); + const DialogTriggerApiDescriptions = mapApiPageTranslations(DialogTriggerApiReq); + + const useDialogCloseApiReq = require.context( + 'docs-base/translations/api-docs/use-dialog-close', + false, + /\.\/use-dialog-close.*.json$/, + ); + const useDialogCloseApiDescriptions = mapApiPageTranslations(useDialogCloseApiReq); + + const useDialogPopupApiReq = require.context( + 'docs-base/translations/api-docs/use-dialog-popup', + false, + /\.\/use-dialog-popup.*.json$/, + ); + const useDialogPopupApiDescriptions = mapApiPageTranslations(useDialogPopupApiReq); + + const useDialogRootApiReq = require.context( + 'docs-base/translations/api-docs/use-dialog-root', + false, + /\.\/use-dialog-root.*.json$/, + ); + const useDialogRootApiDescriptions = mapApiPageTranslations(useDialogRootApiReq); + + const useDialogTriggerApiReq = require.context( + 'docs-base/translations/api-docs/use-dialog-trigger', + false, + /\.\/use-dialog-trigger.*.json$/, + ); + const useDialogTriggerApiDescriptions = mapApiPageTranslations(useDialogTriggerApiReq); + + return { + props: { + componentsApiDescriptions: { + DialogBackdrop: DialogBackdropApiDescriptions, + DialogClose: DialogCloseApiDescriptions, + DialogDescription: DialogDescriptionApiDescriptions, + DialogPopup: DialogPopupApiDescriptions, + DialogRoot: DialogRootApiDescriptions, + DialogTitle: DialogTitleApiDescriptions, + DialogTrigger: DialogTriggerApiDescriptions, + }, + componentsApiPageContents: { + DialogBackdrop: DialogBackdropApiJsonPageContent, + DialogClose: DialogCloseApiJsonPageContent, + DialogDescription: DialogDescriptionApiJsonPageContent, + DialogPopup: DialogPopupApiJsonPageContent, + DialogRoot: DialogRootApiJsonPageContent, + DialogTitle: DialogTitleApiJsonPageContent, + DialogTrigger: DialogTriggerApiJsonPageContent, + }, + hooksApiDescriptions: { + useDialogClose: useDialogCloseApiDescriptions, + useDialogPopup: useDialogPopupApiDescriptions, + useDialogRoot: useDialogRootApiDescriptions, + useDialogTrigger: useDialogTriggerApiDescriptions, + }, + hooksApiPageContents: { + useDialogClose: useDialogCloseApiJsonPageContent, + useDialogPopup: useDialogPopupApiJsonPageContent, + useDialogRoot: useDialogRootApiJsonPageContent, + useDialogTrigger: useDialogTriggerApiJsonPageContent, + }, + }, + }; +}; diff --git a/docs/pages/base-ui/react-dialog/index.js b/docs/pages/base-ui/react-dialog/index.js new file mode 100644 index 0000000000..8e8543319f --- /dev/null +++ b/docs/pages/base-ui/react-dialog/index.js @@ -0,0 +1,13 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/dialog/dialog.md?@mui/markdown'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; diff --git a/docs/pages/experiments/dialog.module.css b/docs/pages/experiments/dialog.module.css new file mode 100644 index 0000000000..a9f4706804 --- /dev/null +++ b/docs/pages/experiments/dialog.module.css @@ -0,0 +1,285 @@ +@keyframes dialog-opening-transform { + from { + transform: translate(-50%, -35%) scale(0.8) translateY(0); + } + to { + transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + } +} + +@keyframes dialog-opening-opacity { + from { + opacity: 0; + } + to { + opacity: 1; + visibility: visible; + } +} + +@keyframes dialog-closing { + from { + transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + opacity: 1; + visibility: visible; + } + + to { + transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + opacity: 0.5; + visibility: hidden; + } +} + +@keyframes backdrop-opening { + from { + backdrop-filter: blur(1px); + opacity: 0; + } + + to { + backdrop-filter: blur(6px); + opacity: 1; + visibility: visible; + } +} + +@keyframes backdrop-closing { + from { + opacity: 1; + backdrop-filter: blur(6px); + } + + to { + backdrop-filter: blur(1px); + opacity: 0; + visibility: hidden; + } +} + +.dialog { + --transition-duration: 150ms; + + 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; + position: fixed; + top: 50%; + left: 50%; + padding: 16px; + font-family: IBM Plex Sans; + z-index: 1; + transform: translate(-50%, -50%); + opacity: calc(pow(0.95, var(--nested-dialogs))); + + &.withTransitions { + transform: translate(-50%, -35%) scale(0.8, calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + visibility: hidden; + opacity: 0; + transition: + transform var(--transition-duration) ease-in, + opacity var(--transition-duration) ease-in, + visibility var(--transition-duration) step-end; + + &[data-state='open'] { + @starting-style { + & { + transform: translate(-50%, -35%) scale(0.8) translateY(0); + opacity: 0; + } + } + + visibility: visible; + opacity: 1; + transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + transition: + transform var(--transition-duration) ease-out, + opacity var(--transition-duration) ease-out, + visibility var(--transition-duration) step-start; + } + } + + &.withAnimations { + transform: translate(-50%, -35%) scale(0.8, 0.9) translateY(0); + visibility: hidden; + opacity: 0; + + &[data-state='open'] { + animation: + dialog-opening-transform var(--transition-duration) ease-out, + dialog-opening-opacity var(--transition-duration) ease-out forwards; + transform: translate(-50%, -50%) scale(calc(pow(0.95, var(--nested-dialogs)))) + translateY(calc(-30px * var(--nested-dialogs))); + transition: transform var(--transition-duration) ease-out; + } + + &[data-exiting] { + animation: dialog-closing var(--transition-duration) ease-in forwards; + } + } + + &.withReactSpringTransition { + top: 50vh; + left: 50vw; + visibility: visible; + opacity: 1; + } +} + +.backdrop { + background: radial-gradient(#cecdcf36, #8b94ab47); + z-index: 0; + position: fixed; + inset: 0; + opacity: 0; + visibility: hidden; + backdrop-filter: blur(1px); + + &.withTransitions { + transition: + backdrop-filter 300ms ease-in, + opacity 300ms ease-in, + visibility 300ms step-end; + + &[data-state='open'] { + @starting-style { + & { + opacity: 0; + backdrop-filter: blur(1px); + } + } + + backdrop-filter: blur(6px); + opacity: 1; + visibility: visible; + transition: + backdrop-filter 500ms ease-out, + opacity 500ms ease-out; + } + } + + &.withAnimations { + &[data-state='open'] { + animation: backdrop-opening 500ms ease-out forwards; + } + + &[data-exiting] { + animation: backdrop-closing 500ms ease-in forwards; + } + } +} + +.title { + font-size: 1.5rem; + font-weight: 600; +} + +.page { + 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; + } + + label { + font-size: 0.8333rem; + } + + label + label { + margin-left: 16px; + } +} + +.springWrapper { + position: fixed; + top: 0; + left: 0; + right: 0; +} + +.button { + background: #eee; + padding: 8px 16px; + border: 1px solid #d8d8d8; + border-radius: 4px; + font-family: inherit; + + &:hover { + background: #ffbf2b; + } + + &:focus-visible { + outline: 2px solid #ffbf2b; + } + + &:active { + background: #cc9922; + border-color: #cc9922; + + &:focus-visible { + outline-color: #cc9922; + } + } +} + +.form { + display: flex; + gap: 16px; + margin-top: 24px; + flex-wrap: wrap; + + input[type='text'], + textarea { + padding: 8px; + border: 1px solid #d8d8d8; + border-radius: 4px; + font-family: inherit; + box-sizing: border-box; + } + + textarea { + resize: vertical; + min-height: 100px; + width: 100%; + } + + & > * { + flex: 1 0 auto; + margin: 0; + } +} + +.controls { + display: flex; + gap: 16px; + margin-top: 16px; + border-top: 1px solid #d8d8d8; + padding-top: 16px; + + & > * { + flex: 1 1 50%; + margin: 0; + } +} + +.demo { + margin-right: 8px; +} diff --git a/docs/pages/experiments/dialog.tsx b/docs/pages/experiments/dialog.tsx new file mode 100644 index 0000000000..3244285b89 --- /dev/null +++ b/docs/pages/experiments/dialog.tsx @@ -0,0 +1,225 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import * as Dialog from '@base_ui/react/Dialog'; +// eslint-disable-next-line no-restricted-imports +import { useTransitionStatus } from '@base_ui/react/utils/useTransitionStatus'; +import { animated as springAnimated, useSpring, useSpringRef } from '@react-spring/web'; +import classes from './dialog.module.css'; + +const NESTED_DIALOGS = 8; + +interface DemoProps { + keepMounted: boolean; + modal: boolean; + dismissible: boolean; +} + +function renderContent( + title: string, + includeNested: number, + nestedClassName: string, + modal: boolean, + dismissible: boolean, +) { + return ( + + {title} + 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. +

      + +
      +