From 13a63897f4ccdf186d59bef6c48a6e94d9d1ef64 Mon Sep 17 00:00:00 2001 From: Jake Laderman Date: Mon, 19 Aug 2024 19:54:47 -0400 Subject: [PATCH] feat: new modal --- package.json | 3 +- src/components/Modal.tsx | 80 ++++++++--------- src/components/ModalWrapper.tsx | 96 ++++++++++++++++++++ src/index.ts | 2 +- src/stories/Modal.stories.tsx | 105 +--------------------- yarn.lock | 150 +++++++++++++++++++++++++++++++- 6 files changed, 290 insertions(+), 146 deletions(-) create mode 100644 src/components/ModalWrapper.tsx diff --git a/package.json b/package.json index 7ac8ee31..37d19493 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 260cf0e5..83db4891 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -1,18 +1,26 @@ -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', @@ -20,13 +28,14 @@ export const SEVERITIES = [ 'success', 'danger', ] as const satisfies Readonly -const SIZES = ['medium', 'large'] as const +const SIZES = ['medium', 'large', 'custom'] as const type ModalSeverity = Extract type ModalSize = (typeof SIZES)[number] -type ModalPropsType = Omit & { +type ModalPropsType = ModalWrapperProps & { + onClose?: Nullable<() => void> form?: boolean size?: ModalSize header?: ReactNode @@ -35,8 +44,6 @@ type ModalPropsType = Omit & { lockBody?: boolean asForm?: boolean formProps?: StyledComponentPropsWithRef<'form'> - scrollable?: boolean - [x: string]: unknown } const severityToIconColorKey = { @@ -61,35 +68,27 @@ const severityToIcon = { const sizeToWidth = { medium: 480, large: 608, -} as const satisfies Record + custom: undefined as undefined, +} as const satisfies Record -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((_) => ({ @@ -154,25 +153,28 @@ function ModalRef( setBodyLocked(lockBody && open) }, [lockBody, open, setBodyLocked]) + const triggerClose = useCallback( + (open: boolean) => { + if (!open) onClose?.() + }, + [onClose] + ) + return ( - - + {!!header && ( {HeaderIcon && ( @@ -193,7 +195,7 @@ function ModalRef( )} - + ) } diff --git a/src/components/ModalWrapper.tsx b/src/components/ModalWrapper.tsx new file mode 100644 index 00000000..0682d33b --- /dev/null +++ b/src/components/ModalWrapper.tsx @@ -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 ( + + + + + {children} + + + + + ) +} + +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) diff --git a/src/index.ts b/src/index.ts index c493f9bd..1e79383f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/stories/Modal.stories.tsx b/src/stories/Modal.stories.tsx index 198311dd..0b0fe67c 100644 --- a/src/stories/Modal.stories.tsx +++ b/src/stories/Modal.stories.tsx @@ -10,6 +10,9 @@ import { jsCode } from '../constants' export default { title: 'Modal', component: Modal, + args: { + scrollable: true, + }, argTypes: { size: { options: ['medium', 'large'], @@ -186,96 +189,6 @@ function NonScrollTemplate(args: any) { ) } -function PinnedToTopTemplate(args: any) { - const [open, setOpen] = useState(false) - - return ( - <> -

{args.header} Modal

- - setOpen(false)} - actions={ - args.hasActions && ( - <> - - - - ) - } - {...args} - > - {!args.form && ( - <> -

- Uninstalling this application will disable all future upgrades. -

-

- If you'd also like to remove the running instance from your - cluster, be sure to run `plural destroy` from this application's - repository. -

- - )} - - {args.form && ( - <> - - - - - - - - - - - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus - tempor, mi pulvinar vestibulum viverra, magnan ipsum suscipit - turpis, molestie imperdiet nisi lorem id erat. Vestibulum - pellentesque vel odio et consequat. Sed lacinia leo sit amet velit - consequat lobortis. Vivamus facilisis sagittis est vel - pellentesque. Sed quis ipsum ullamcorper, posuere ipsum a, - tincidunt tellus. Cras tortor purus, dictum sit amet facilisis - vitae, commodo vitae elit. Duis a diam blandit, hendrerit velit - non, tincidunt turpis. Ut at lectus ornare, volutpat elit - interdum, placerat dolor. Pellentesque et semper massa. Aliquam - nec nisl eu nibh fringilla vehicula. Suspendisse a purus quam. -

- - )} -
- - ) -} - export const Default = Template.bind({}) Default.args = { @@ -283,7 +196,6 @@ Default.args = { form: false, size: 'medium', hasActions: true, - scrollable: true, } export const Form = Template.bind({}) @@ -292,17 +204,6 @@ Form.args = { header: 'Form', form: true, hasActions: true, - scrollable: true, -} - -export const PinnedToTop = PinnedToTopTemplate.bind({}) - -PinnedToTop.args = { - header: 'Default', - form: false, - size: 'medium', - hasActions: true, - scrollable: true, } export const NonScrollable = NonScrollTemplate.bind({}) diff --git a/yarn.lock b/yarn.lock index 1e304dab..0e967052 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3046,7 +3046,8 @@ __metadata: "@markdoc/markdoc": 0.4.0 "@monaco-editor/react": 4.6.0 "@pluralsh/eslint-config-typescript": 2.5.147 - "@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 @@ -3306,7 +3307,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-accordion@npm:^1.2.0": +"@radix-ui/react-accordion@npm:1.2.0": version: 1.2.0 resolution: "@radix-ui/react-accordion@npm:1.2.0" dependencies: @@ -3480,6 +3481,38 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-dialog@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-dialog@npm:1.1.1" + dependencies: + "@radix-ui/primitive": 1.1.0 + "@radix-ui/react-compose-refs": 1.1.0 + "@radix-ui/react-context": 1.1.0 + "@radix-ui/react-dismissable-layer": 1.1.0 + "@radix-ui/react-focus-guards": 1.1.0 + "@radix-ui/react-focus-scope": 1.1.0 + "@radix-ui/react-id": 1.1.0 + "@radix-ui/react-portal": 1.1.1 + "@radix-ui/react-presence": 1.1.0 + "@radix-ui/react-primitive": 2.0.0 + "@radix-ui/react-slot": 1.1.0 + "@radix-ui/react-use-controllable-state": 1.1.0 + aria-hidden: ^1.1.1 + react-remove-scroll: 2.5.7 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 5f270518b61e0b570a321f1db09ed95939969e9bff71fad02bce02126f047f5305d74ff79bb4e763677062db881b1e4ecd297b1556a917fed3d7a77cc0a7c235 + languageName: node + linkType: hard + "@radix-ui/react-direction@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-direction@npm:1.0.1" @@ -3532,6 +3565,29 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-dismissable-layer@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.0" + dependencies: + "@radix-ui/primitive": 1.1.0 + "@radix-ui/react-compose-refs": 1.1.0 + "@radix-ui/react-primitive": 2.0.0 + "@radix-ui/react-use-callback-ref": 1.1.0 + "@radix-ui/react-use-escape-keydown": 1.1.0 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 857feab2d5184a72df4e6dd9430c8e4b9fe7304790ef69512733346eee5fc33a6527256fc135d4bee6d94e8cc9c1b83c3d91da96cb4bf8300f88e9c660b71b08 + languageName: node + linkType: hard + "@radix-ui/react-focus-guards@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-focus-guards@npm:1.0.1" @@ -3547,6 +3603,19 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-focus-guards@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-focus-guards@npm:1.1.0" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 199717e7da1ba9b3fa74b04f6a245aaebf6bdb8ae7d6f4b5f21f95f4086414a3587beebc77399a99be7d3a4b2499eaa52bf72bef660f8e69856b0fd0593b074f + languageName: node + linkType: hard + "@radix-ui/react-focus-scope@npm:1.0.3": version: 1.0.3 resolution: "@radix-ui/react-focus-scope@npm:1.0.3" @@ -3569,6 +3638,27 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-focus-scope@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-focus-scope@npm:1.1.0" + dependencies: + "@radix-ui/react-compose-refs": 1.1.0 + "@radix-ui/react-primitive": 2.0.0 + "@radix-ui/react-use-callback-ref": 1.1.0 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: bea6c993752780c46c69f0c21a0fd96f11b9ed7edac23deb0953fbd8524d90938bf4c8060ccac7cad14caba3eb493f2642be7f8933910f4b6fa184666b7fcb40 + languageName: node + linkType: hard + "@radix-ui/react-id@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-id@npm:1.0.1" @@ -3649,6 +3739,26 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-portal@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-portal@npm:1.1.1" + dependencies: + "@radix-ui/react-primitive": 2.0.0 + "@radix-ui/react-use-layout-effect": 1.1.0 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 84dab64ce9c9f4ed7d75df6d1d82877dc7976a98cc192287d39ba2ea512415ed7bf34caf02d579a18fe21766403fa9ae41d2482a14dee5514179ee1b09cc333c + languageName: node + linkType: hard + "@radix-ui/react-presence@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-presence@npm:1.1.0" @@ -3971,6 +4081,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-escape-keydown@npm:1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.1.0" + dependencies: + "@radix-ui/react-use-callback-ref": 1.1.0 + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 9bf88ea272b32ea0f292afd336780a59c5646f795036b7e6105df2d224d73c54399ee5265f61d571eb545d28382491a8b02dc436e3088de8dae415d58b959b71 + languageName: node + linkType: hard + "@radix-ui/react-use-layout-effect@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1" @@ -17360,7 +17485,7 @@ __metadata: languageName: node linkType: hard -"react-remove-scroll-bar@npm:^2.3.3": +"react-remove-scroll-bar@npm:^2.3.3, react-remove-scroll-bar@npm:^2.3.4": version: 2.3.6 resolution: "react-remove-scroll-bar@npm:2.3.6" dependencies: @@ -17395,6 +17520,25 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll@npm:2.5.7": + version: 2.5.7 + resolution: "react-remove-scroll@npm:2.5.7" + dependencies: + react-remove-scroll-bar: ^2.3.4 + react-style-singleton: ^2.2.1 + tslib: ^2.1.0 + use-callback-ref: ^1.3.0 + use-sidecar: ^1.1.2 + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: e0dbb6856beaed2cff4996d9ca62d775686ff72e3e9de34043034d932223b588993b2fc7a18644750dd3d73eb19bd3f2cedb8d91f0e424c1ef8403010da24b1d + languageName: node + linkType: hard + "react-simple-player@npm:^1.1.0": version: 1.1.0 resolution: "react-simple-player@npm:1.1.0"