Skip to content

Commit

Permalink
feat: new modal component (#635)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsladerman authored Aug 21, 2024
1 parent d1821aa commit 8592fab
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 147 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"@loomhq/loom-embed": "1.5.0",
"@markdoc/markdoc": "0.4.0",
"@monaco-editor/react": "4.6.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-accordion": "1.2.0",
"@radix-ui/react-dialog": "1.1.1",
"@react-aria/utils": "3.23.0",
"@react-hooks-library/core": "0.6.0",
"@react-spring/web": "9.7.3",
Expand Down
80 changes: 41 additions & 39 deletions src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import { type ReactNode, type Ref, forwardRef, useEffect } from 'react'
// this is just styling, actual modal logic is in ModalWrapper

import {
type ReactNode,
type Ref,
forwardRef,
useCallback,
useEffect,
} from 'react'

import styled, { type StyledComponentPropsWithRef } from 'styled-components'

import { type ColorKey, type SeverityExt } from '../types'
import { type ColorKey, type Nullable, type SeverityExt } from '../types'

import useLockedBody from '../hooks/useLockedBody'

import { HonorableModal, type ModalProps } from './HonorableModal'

import CheckRoundedIcon from './icons/CheckRoundedIcon'
import type createIcon from './icons/createIcon'
import ErrorIcon from './icons/ErrorIcon'
import WarningIcon from './icons/WarningIcon'
import InfoIcon from './icons/InfoIcon'
import WarningIcon from './icons/WarningIcon'
import { ModalWrapper, type ModalWrapperProps } from './ModalWrapper'
import Card from './Card'

export const SEVERITIES = [
'info',
'warning',
'success',
'danger',
] as const satisfies Readonly<SeverityExt[]>
const SIZES = ['medium', 'large'] as const
const SIZES = ['medium', 'large', 'custom'] as const

type ModalSeverity = Extract<SeverityExt, (typeof SEVERITIES)[number]>

type ModalSize = (typeof SIZES)[number]

type ModalPropsType = Omit<ModalProps, 'size'> & {
type ModalPropsType = ModalWrapperProps & {
onClose?: Nullable<() => void>
form?: boolean
size?: ModalSize
header?: ReactNode
Expand All @@ -35,8 +44,6 @@ type ModalPropsType = Omit<ModalProps, 'size'> & {
lockBody?: boolean
asForm?: boolean
formProps?: StyledComponentPropsWithRef<'form'>
scrollable?: boolean
[x: string]: unknown
}

const severityToIconColorKey = {
Expand All @@ -61,35 +68,27 @@ const severityToIcon = {
const sizeToWidth = {
medium: 480,
large: 608,
} as const satisfies Record<ModalSize, number>
custom: undefined as undefined,
} as const satisfies Record<ModalSize, number | undefined>

const ModalSC = styled.div<{ $scrollable: boolean }>(({ $scrollable }) => ({
const ModalSC = styled(Card)<{
$width: number
$maxWidth: number
}>(({ theme, $width, $maxWidth }) => ({
position: 'relative',
...($scrollable
? {}
: {
display: 'flex',
flexDirection: 'column',
height: '100%',
}),
width: $width,
maxWidth: $maxWidth,
backgroundColor: theme.colors['fill-one'],
}))

const ModalContentSC = styled.div<{
$scrollable: boolean
$hasActions: boolean
}>(({ theme, $scrollable, $hasActions }) => ({
}>(({ theme, $hasActions }) => ({
position: 'relative',
zIndex: 0,
margin: theme.spacing.large,
marginBottom: $hasActions ? 0 : theme.spacing.large,
...theme.partials.text.body1,
...($scrollable
? {}
: {
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}),
}))

const ModalActionsSC = styled.div((_) => ({
Expand Down Expand Up @@ -154,25 +153,28 @@ function ModalRef(
setBodyLocked(lockBody && open)
}, [lockBody, open, setBodyLocked])

const triggerClose = useCallback(
(open: boolean) => {
if (!open) onClose?.()
},
[onClose]
)

return (
<HonorableModal
open={open}
onClose={onClose}
<ModalWrapper
ref={ref}
width={sizeToWidth[size]}
maxWidth={sizeToWidth[size]}
open={open}
onOpenChange={triggerClose}
scrollable={scrollable}
{...props}
>
<ModalSC
as={asForm ? 'form' : undefined}
$scrollable={scrollable}
forwardedAs={asForm ? 'form' : undefined}
$width={sizeToWidth[size]}
$maxWidth={sizeToWidth[size]}
{...(asForm ? formProps : {})}
>
<ModalContentSC
$scrollable={scrollable}
$hasActions={!!actions}
>
<ModalContentSC $hasActions={!!actions}>
{!!header && (
<ModalHeaderWrapSC ref={ref}>
{HeaderIcon && (
Expand All @@ -193,7 +195,7 @@ function ModalRef(
</ModalActionsSC>
)}
</ModalSC>
</HonorableModal>
</ModalWrapper>
)
}

Expand Down
96 changes: 96 additions & 0 deletions src/components/ModalWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// styling here mostly just for the overlay and animations
import * as Dialog from '@radix-ui/react-dialog'

import { type ComponentProps, type ReactNode, forwardRef } from 'react'
import styled, { useTheme } from 'styled-components'

const ANIMATION_SPEED = '150ms'

export type ModalWrapperProps = {
open: boolean
onOpenChange?: (open: boolean) => void
scrollable?: boolean
children?: ReactNode
} & ComponentProps<'div'>

function ModalWrapperRef(
{
open,
onOpenChange,
scrollable = true,
children,
...props
}: ModalWrapperProps,
ref: any
) {
const theme = useTheme()
const portalElement = document.getElementById(theme.portals.default.id)

return (
<Dialog.Root
open={open}
onOpenChange={onOpenChange}
>
<Dialog.Portal container={portalElement}>
<OverlaySC>
<ContentSC
ref={ref}
$scrollable={scrollable}
{...props}
>
{children}
</ContentSC>
</OverlaySC>
</Dialog.Portal>
</Dialog.Root>
)
}

const ContentSC = styled(Dialog.Content)<{ $scrollable?: boolean }>(
({ $scrollable }) => ({
overflowY: $scrollable ? 'auto' : 'hidden',
maxHeight: '100%',
'@keyframes popIn': {
from: { transform: 'scale(0.8)' },
to: { transform: 'scale(1)' },
},
'@keyframes popOut': {
from: { transform: 'scale(1)' },
to: { transform: 'scale(0.9)' },
},
'&[data-state="open"]': {
animation: `popIn ${ANIMATION_SPEED} ease-out`,
},
'&[data-state="closed"]': {
animation: `popOut ${ANIMATION_SPEED} ease-out`,
},
})
)
const OverlaySC = styled(Dialog.Overlay)(({ theme }) => ({
background: theme.colors['modal-backdrop'],
position: 'fixed',
padding: theme.spacing.xlarge,
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'grid',
placeItems: 'center',
zIndex: theme.zIndexes.modal,
'@keyframes fadeIn': {
from: { opacity: 0 },
to: { opacity: 1 },
},
'@keyframes fadeOut': {
from: { opacity: 1 },
to: { opacity: 0 },
},
'&[data-state="open"]': {
animation: `fadeIn ${ANIMATION_SPEED} ease-out`,
},
'&[data-state="closed"]': {
animation: `fadeOut ${ANIMATION_SPEED} ease-out`,
},
}))

export const ModalWrapper = forwardRef(ModalWrapperRef)
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export {
export { default as SidebarItem } from './components/SidebarItem'
export { default as Modal } from './components/Modal'
export { default as Flyover } from './components/Flyover'
export { HonorableModal } from './components/HonorableModal'
export { ModalWrapper } from './components/ModalWrapper'
export type {
ChecklistProps,
ChecklistStateProps,
Expand Down
Loading

0 comments on commit 8592fab

Please sign in to comment.