♻️(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 { 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { SettingsButton } from './components/SettingsButton'
|
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 {
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 { 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'
|
||||||
|
|||||||
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