♻️(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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { SettingsButton } from './components/SettingsButton'
|
||||
export { SettingsDialog } from './components/SettingsDialog'
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Dialog, type DialogProps } from './Dialog'
|
||||
|
||||
export const AlertDialog = (props: DialogProps) => {
|
||||
return <Dialog role="alertdialog" {...props} />
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
10
src/frontend/src/primitives/useCloseDialog.tsx
Normal file
10
src/frontend/src/primitives/useCloseDialog.tsx
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user