♻️(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 { 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' import { isRoomValid, navigateToRoom } from '@/features/rooms'
export const JoinMeetingDialogContent = () => { export const JoinMeetingDialog = () => {
const { t } = useTranslation('home') const { t } = useTranslation('home')
return ( return (
<DialogContent title={t('joinMeeting')}> <Dialog title={t('joinMeeting')}>
<Form <Form
onSubmit={(data) => { onSubmit={(data) => {
navigateToRoom((data.roomId as string).trim()) navigateToRoom((data.roomId as string).trim())
@@ -34,6 +34,6 @@ export const JoinMeetingDialogContent = () => {
</Form> </Form>
<H lvl={2}>{t('joinMeetingTipHeading')}</H> <H lvl={2}>{t('joinMeetingTipHeading')}</H>
<P last>{t('joinMeetingTipContent')}</P> <P last>{t('joinMeetingTipContent')}</P>
</DialogContent> </Dialog>
) )
} }

View File

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

View File

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

View File

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

View File

@@ -1 +1,2 @@
export { SettingsButton } from './components/SettingsButton' 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 { import {
Dialog as RACDialog, Dialog as RACDialog,
DialogTrigger,
Modal,
type DialogProps as RACDialogProps,
ModalOverlay, ModalOverlay,
OverlayTriggerStateContext, Modal,
type DialogProps,
Heading,
} from 'react-aria-components' } from 'react-aria-components'
import { RiCloseLine } from '@remixicon/react' import { Div, Button, Box, VerticallyOffCenter } from '@/primitives'
import { useTranslation } from 'react-i18next' import { text } from './Text'
import { styled } from '@/styled-system/jsx'
import { Button, Box, Div, VerticallyOffCenter } from '@/primitives'
const StyledModalOverlay = styled(ModalOverlay, { const StyledModalOverlay = styled(ModalOverlay, {
base: { base: {
@@ -47,71 +46,56 @@ const StyledRACDialog = styled(RACDialog, {
pointerEvents: 'none', pointerEvents: 'none',
}, },
}) })
export const Dialog = ({
export type DialogProps = { title,
children: [ children,
trigger: ReactNode, ...dialogProps
dialogContent: }: DialogProps & { title: string }) => {
| (({ 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()
const isAlert = dialogProps['role'] === 'alertdialog' const isAlert = dialogProps['role'] === 'alertdialog'
const [trigger, dialogContent] = children
return ( return (
<DialogTrigger> <StyledModalOverlay
{trigger} isKeyboardDismissDisabled={isAlert}
<StyledModalOverlay isDismissable={!isAlert}
isKeyboardDismissDisabled={isAlert} >
isDismissable={!isAlert} <StyledModal>
> <StyledRACDialog {...dialogProps}>
<StyledModal> {({ close }) => (
<StyledRACDialog {...dialogProps}> <VerticallyOffCenter>
{({ close }) => ( <Div
<VerticallyOffCenter> width="fit-content"
<Div maxWidth="full"
width="fit-content" margin="auto"
maxWidth="full" pointerEvents="auto"
margin="auto" >
pointerEvents="auto" <Box size="sm" type="dialog">
> <Heading
<Box size="sm" type="dialog"> slot="title"
{typeof dialogContent === 'function' level={1}
? dialogContent({ close }) className={text({ variant: 'h1' })}
: dialogContent} >
{!isAlert && ( {title}
<Div position="absolute" top="0" right="0"> </Heading>
<Button {typeof children === 'function'
invisible ? children({ close })
size="xs" : children}
onPress={() => close()} {!isAlert && (
aria-label={t('closeDialog')} <Div position="absolute" top="0" right="0">
> <Button
<RiCloseLine /> invisible
</Button> size="xs"
</Div> onPress={() => close()}
)} aria-label={t('closeDialog')}
</Box> >
</Div> <RiCloseLine />
</VerticallyOffCenter> </Button>
)} </Div>
</StyledRACDialog> )}
</StyledModal> </Box>
</StyledModalOverlay> </Div>
</DialogTrigger> </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 { A } from './A'
export { AlertDialog } from './AlertDialog'
export { Badge } from './Badge' export { Badge } from './Badge'
export { Bold } from './Bold' export { Bold } from './Bold'
export { Box } from './Box' export { Box } from './Box'
export { Button } from './Button' export { Button } from './Button'
export { Dialog, useCloseDialog } from './Dialog' export { useCloseDialog } from './useCloseDialog'
export { DialogContent } from './DialogContent' export { Dialog } from './Dialog'
export { Div } from './Div' export { Div } from './Div'
export { Field } from './Field' export { Field } from './Field'
export { Form } from './Form' 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
}