♻️(dialogs) make it easier to have dialogs unrelated to their trigger

- use the default react-aria DialogTrigger when we want to build buttons
triggering dialogs
- use custom Dialog component as a wrapper to Dialog content
This commit is contained in:
Emmanuel Pelletier
2024-07-24 17:48:17 +02:00
parent c0d490f549
commit 47c133cc64
10 changed files with 89 additions and 125 deletions

View File

@@ -1,11 +1,11 @@
import { useTranslation } from 'react-i18next'
import { Field, Ul, H, P, Form, DialogContent } from '@/primitives'
import { Field, Ul, H, P, Form, Dialog } from '@/primitives'
import { isRoomValid, navigateToRoom } from '@/features/rooms'
export const JoinMeetingDialogContent = () => {
export const JoinMeetingDialog = () => {
const { t } = useTranslation('home')
return (
<DialogContent title={t('joinMeeting')}>
<Dialog title={t('joinMeeting')}>
<Form
onSubmit={(data) => {
navigateToRoom((data.roomId as string).trim())
@@ -34,6 +34,6 @@ export const JoinMeetingDialogContent = () => {
</Form>
<H lvl={2}>{t('joinMeetingTipHeading')}</H>
<P last>{t('joinMeetingTipContent')}</P>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,11 +1,12 @@
import { useTranslation } from 'react-i18next'
import { Button, Div, Text, VerticallyOffCenter, Dialog } from '@/primitives'
import { DialogTrigger } from 'react-aria-components'
import { Button, Div, Text, VerticallyOffCenter } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
import { authUrl, useUser } from '@/features/auth'
import { navigateToNewRoom } from '@/features/rooms'
import { Screen } from '@/layout/Screen'
import { JoinMeetingDialogContent } from '../components/JoinMeetingDialogContent'
import { SettingsButton } from '@/features/settings'
import { Screen } from '@/layout/Screen'
import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
export const Home = () => {
const { t } = useTranslation('home')
@@ -34,12 +35,12 @@ export const Home = () => {
{isLoggedIn ? t('createMeeting') : t('login', { ns: 'global' })}
</Button>
<Dialog>
<DialogTrigger>
<Button variant="primary" outline>
{t('joinMeeting')}
</Button>
<JoinMeetingDialogContent />
</Dialog>
<JoinMeetingDialog />
</DialogTrigger>
<SettingsButton />
</HStack>

View File

@@ -1,11 +1,13 @@
import { useTranslation } from 'react-i18next'
import { DialogTrigger } from 'react-aria-components'
import { RiSettings3Line } from '@remixicon/react'
import { Dialog, Button } from '@/primitives'
import { Button } from '@/primitives'
import { SettingsDialog } from './SettingsDialog'
export const SettingsButton = () => {
const { t } = useTranslation('settings')
return (
<Dialog>
<DialogTrigger>
<Button
square
invisible
@@ -15,6 +17,6 @@ export const SettingsButton = () => {
<RiSettings3Line />
</Button>
<SettingsDialog />
</Dialog>
</DialogTrigger>
)
}

View File

@@ -1,6 +1,6 @@
import { Trans, useTranslation } from 'react-i18next'
import { useLanguageLabels } from '@/i18n/useLanguageLabels'
import { A, Badge, DialogContent, Field, H, P } from '@/primitives'
import { A, Badge, Dialog, Field, H, P } from '@/primitives'
import { authUrl, logoutUrl, useUser } from '@/features/auth'
export const SettingsDialog = () => {
@@ -8,7 +8,7 @@ export const SettingsDialog = () => {
const { user, isLoggedIn } = useUser()
const { languagesList, currentLanguage } = useLanguageLabels()
return (
<DialogContent title={t('dialog.heading')}>
<Dialog title={t('dialog.heading')}>
<H lvl={2}>{t('account.heading')}</H>
{isLoggedIn ? (
<>
@@ -41,6 +41,6 @@ export const SettingsDialog = () => {
i18n.changeLanguage(lang as string)
}}
/>
</DialogContent>
</Dialog>
)
}

View File

@@ -1 +1,2 @@
export { SettingsButton } from './components/SettingsButton'
export { SettingsDialog } from './components/SettingsDialog'

View File

@@ -1,5 +0,0 @@
import { Dialog, type DialogProps } from './Dialog'
export const AlertDialog = (props: DialogProps) => {
return <Dialog role="alertdialog" {...props} />
}

View File

@@ -1,16 +1,15 @@
import { useContext, type ReactNode } from 'react'
import { styled } from '@/styled-system/jsx'
import { RiCloseLine } from '@remixicon/react'
import { t } from 'i18next'
import {
Dialog as RACDialog,
DialogTrigger,
Modal,
type DialogProps as RACDialogProps,
ModalOverlay,
OverlayTriggerStateContext,
Modal,
type DialogProps,
Heading,
} from 'react-aria-components'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { styled } from '@/styled-system/jsx'
import { Button, Box, Div, VerticallyOffCenter } from '@/primitives'
import { Div, Button, Box, VerticallyOffCenter } from '@/primitives'
import { text } from './Text'
const StyledModalOverlay = styled(ModalOverlay, {
base: {
@@ -47,71 +46,56 @@ const StyledRACDialog = styled(RACDialog, {
pointerEvents: 'none',
},
})
export type DialogProps = {
children: [
trigger: ReactNode,
dialogContent:
| (({ close }: { close: () => void }) => ReactNode)
| ReactNode,
]
} & RACDialogProps
/**
* a Dialog is a tuple of a trigger component (most usually a Button) that toggles some interactive content in a Dialog on top of the app. You should mostly use a DialogContent as second child.
*/
export const Dialog = ({ children, ...dialogProps }: DialogProps) => {
const { t } = useTranslation()
export const Dialog = ({
title,
children,
...dialogProps
}: DialogProps & { title: string }) => {
const isAlert = dialogProps['role'] === 'alertdialog'
const [trigger, dialogContent] = children
return (
<DialogTrigger>
{trigger}
<StyledModalOverlay
isKeyboardDismissDisabled={isAlert}
isDismissable={!isAlert}
>
<StyledModal>
<StyledRACDialog {...dialogProps}>
{({ close }) => (
<VerticallyOffCenter>
<Div
width="fit-content"
maxWidth="full"
margin="auto"
pointerEvents="auto"
>
<Box size="sm" type="dialog">
{typeof dialogContent === 'function'
? dialogContent({ close })
: dialogContent}
{!isAlert && (
<Div position="absolute" top="0" right="0">
<Button
invisible
size="xs"
onPress={() => close()}
aria-label={t('closeDialog')}
>
<RiCloseLine />
</Button>
</Div>
)}
</Box>
</Div>
</VerticallyOffCenter>
)}
</StyledRACDialog>
</StyledModal>
</StyledModalOverlay>
</DialogTrigger>
<StyledModalOverlay
isKeyboardDismissDisabled={isAlert}
isDismissable={!isAlert}
>
<StyledModal>
<StyledRACDialog {...dialogProps}>
{({ close }) => (
<VerticallyOffCenter>
<Div
width="fit-content"
maxWidth="full"
margin="auto"
pointerEvents="auto"
>
<Box size="sm" type="dialog">
<Heading
slot="title"
level={1}
className={text({ variant: 'h1' })}
>
{title}
</Heading>
{typeof children === 'function'
? children({ close })
: children}
{!isAlert && (
<Div position="absolute" top="0" right="0">
<Button
invisible
size="xs"
onPress={() => close()}
aria-label={t('closeDialog')}
>
<RiCloseLine />
</Button>
</Div>
)}
</Box>
</Div>
</VerticallyOffCenter>
)}
</StyledRACDialog>
</StyledModal>
</StyledModalOverlay>
)
}
export const useCloseDialog = () => {
const dialogState = useContext(OverlayTriggerStateContext)
if (dialogState) {
return dialogState.close
}
return null
}

View File

@@ -1,28 +0,0 @@
import { type ReactNode } from 'react'
import { Heading } from 'react-aria-components'
import { text } from './Text'
/**
* Dialog content Wrapper.
*
* Use this in a <Dialog>, next to the button triggering the dialog.
* This is helpful to easily add the title to the dialog and keep it
* next to the actual content code, if you happen to separate the dialog
* trigger from the dialog content in the code.
*/
export const DialogContent = ({
children,
title,
}: {
title: ReactNode
children: ReactNode
}) => {
return (
<>
<Heading slot="title" level={1} className={text({ variant: 'h1' })}>
{title}
</Heading>
{children}
</>
)
}

View File

@@ -1,11 +1,10 @@
export { A } from './A'
export { AlertDialog } from './AlertDialog'
export { Badge } from './Badge'
export { Bold } from './Bold'
export { Box } from './Box'
export { Button } from './Button'
export { Dialog, useCloseDialog } from './Dialog'
export { DialogContent } from './DialogContent'
export { useCloseDialog } from './useCloseDialog'
export { Dialog } from './Dialog'
export { Div } from './Div'
export { Field } from './Field'
export { Form } from './Form'

View File

@@ -0,0 +1,10 @@
import { useContext } from 'react'
import { OverlayTriggerStateContext } from 'react-aria-components'
export const useCloseDialog = () => {
const dialogState = useContext(OverlayTriggerStateContext)
if (dialogState) {
return dialogState.close
}
return null
}