From d96bd8b78407fde6d3e3cc06b1977ea3cf35bc04 Mon Sep 17 00:00:00 2001 From: Klink <85062+dogmar@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:21:08 -0800 Subject: [PATCH] feat: Enable non-scrollable modal content (#572) --- src/components/Card.tsx | 9 +- src/components/HonorableModal.tsx | 22 +--- src/components/Modal.tsx | 104 +++++++++++-------- src/index.ts | 2 + src/stories/Modal.stories.tsx | 160 ++++++++++++++++-------------- src/stories/Wizard.stories.tsx | 12 ++- 6 files changed, 168 insertions(+), 141 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index ae7cec09..e9d05bbc 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -71,15 +71,12 @@ const hueToFill = { lightest: 3, } as const satisfies Record -const fillToNeutralSelectedBgC: Record< - FillLevel, - keyof DefaultTheme['colors'] -> = { +const fillToNeutralSelectedBgC = { 0: 'fill-one-selected', 1: 'fill-one-selected', 2: 'fill-two-selected', 3: 'fill-three-selected', -} +} as const satisfies Record export function useDecideFillLevel({ hue, @@ -207,7 +204,7 @@ const CardSC = styled(Div)<{ })}`, borderRadius: theme.borderRadiuses[cornerSize], backgroundColor: selected - ? fillToNeutralSelectedBgC[fillLevel] + ? theme.colors[fillToNeutralSelectedBgC[fillLevel]] : getBgColor({ theme, fillLevel, severity }), '&:focus, &:focus-visible': { outline: 'none', diff --git a/src/components/HonorableModal.tsx b/src/components/HonorableModal.tsx index fdb920a1..0f662041 100644 --- a/src/components/HonorableModal.tsx +++ b/src/components/HonorableModal.tsx @@ -18,7 +18,6 @@ import { } from 'react' import { createPortal } from 'react-dom' import { Transition } from 'react-transition-group' -import PropTypes from 'prop-types' import { Div, useTheme } from 'honorable' import useRootStyles from 'honorable/dist/hooks/useRootStyles.js' @@ -29,29 +28,17 @@ import filterUndefinedValues from 'honorable/dist/utils/filterUndefinedValues.js export const modalParts = ['Backdrop'] as const -export const modalPropTypes = { - open: PropTypes.bool, - onClose: PropTypes.func, - fade: PropTypes.bool, - transitionDuration: PropTypes.number, - disableEscapeKey: PropTypes.bool, - portal: PropTypes.bool, -} - export type ModalBaseProps = { open?: boolean - onClose?: (event: MouseEvent | KeyboardEvent) => void + onClose?: (event?: MouseEvent | KeyboardEvent) => void fade?: boolean transitionDuration?: number disableEscapeKey?: boolean portal?: boolean } -export type ModalProps = ComponentProps< - ModalBaseProps, - 'div', - (typeof modalParts)[number] -> +export type ModalProps = ModalBaseProps & + ComponentProps function ModalRef(props: ModalProps, ref: Ref) { const { @@ -225,7 +212,7 @@ function ModalRef(props: ModalProps, ref: Ref) { ref={ref} backgroundColor="background" overflowY="auto" - margin={32} + margin="xlarge" {...rootStyles} {...filterUndefinedValues(otherProps)} /> @@ -238,6 +225,5 @@ function ModalRef(props: ModalProps, ref: Ref) { const BaseModal = forwardRef(ModalRef) BaseModal.displayName = 'Modal' -BaseModal.propTypes = modalPropTypes export const HonorableModal = memo(BaseModal) diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 0f35f9b6..710b2626 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -1,5 +1,4 @@ import { type ReactNode, type Ref, forwardRef, useEffect } from 'react' -import { Flex, H1, type ModalProps } from 'honorable' import PropTypes from 'prop-types' import styled, { type StyledComponentPropsWithRef } from 'styled-components' @@ -8,7 +7,7 @@ import { type ColorKey, type SeverityExt } from '../types' import useLockedBody from '../hooks/useLockedBody' -import { HonorableModal } from './HonorableModal' +import { HonorableModal, type ModalProps } from './HonorableModal' import CheckRoundedIcon from './icons/CheckRoundedIcon' import type createIcon from './icons/createIcon' @@ -37,6 +36,7 @@ type ModalPropsType = Omit & { lockBody?: boolean asForm?: boolean formProps?: StyledComponentPropsWithRef<'form'> + scrollable?: boolean [x: string]: unknown } @@ -73,17 +73,32 @@ const sizeToWidth = { large: 608, } as const satisfies Record -const ModalSC = styled.div((_) => ({ +const ModalSC = styled.div<{ $scrollable: boolean }>(({ $scrollable }) => ({ position: 'relative', + ...($scrollable + ? {} + : { + display: 'flex', + flexDirection: 'column', + height: '100%', + }), })) -const ModalContentSC = styled.div<{ $hasActions: boolean }>( - ({ theme, $hasActions }) => ({ - margin: theme.spacing.large, - marginBottom: $hasActions ? 0 : theme.spacing.large, - ...theme.partials.text.body1, - }) -) +const ModalContentSC = styled.div<{ + $scrollable: boolean + $hasActions: boolean +}>(({ theme, $scrollable, $hasActions }) => ({ + 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((_) => ({ display: 'flex', @@ -92,6 +107,34 @@ const ModalActionsSC = styled.div((_) => ({ bottom: '0', })) +const gradientHeight = 12 +const ModalActionsGradientSC = styled.div(({ theme }) => ({ + display: 'flex', + background: `linear-gradient(180deg, transparent 0%, ${theme.colors['fill-one']} 100%);`, + height: gradientHeight, +})) +const ModalActionsContentSC = styled.div(({ theme }) => ({ + display: 'flex', + padding: theme.spacing.large, + paddingTop: theme.spacing.large - gradientHeight, + alignItems: 'center', + justifyContent: 'flex-end', + backgroundColor: theme.colors['fill-one'], +})) + +const ModalHeaderWrapSC = styled.div(({ theme }) => ({ + alignItems: 'center', + justifyContent: 'start', + marginBottom: theme.spacing.large, + gap: theme.spacing.xsmall, +})) + +const ModalHeaderSC = styled.h1(({ theme }) => ({ + margin: 0, + ...theme.partials.text.overline, + color: theme.colors['text-xlight'], +})) + function ModalRef( { children, @@ -105,6 +148,7 @@ function ModalRef( lockBody = true, asForm = false, formProps = {}, + scrollable = true, ...props }: ModalPropsType, ref: Ref @@ -123,55 +167,37 @@ function ModalRef( open={open} onClose={onClose} ref={ref} - fontSize={16} - color="text" width={sizeToWidth[size]} maxWidth={sizeToWidth[size]} + scrollable={scrollable} {...props} > - + {!!header && ( - + {HeaderIcon && ( )} -

- {header} -

-
+ {header} + )} {children}
{!!actions && ( - - - {actions} - + + {actions} )}
diff --git a/src/index.ts b/src/index.ts index 5331961e..3bfec27c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ export { default as Highlight } from './components/Highlight' export type { IconFrameProps } from './components/IconFrame' export { default as IconFrame } from './components/IconFrame' export { default as Input } from './components/Input' +export { default as Input2 } from './components/Input2' export { default as Markdown } from './components/Markdown' export type { PageCardProps } from './components/PageCard' export { default as PageCard } from './components/PageCard' @@ -71,6 +72,7 @@ export { default as Sidebar } from './components/Sidebar' export { default as SidebarSection } from './components/SidebarSection' export { default as SidebarItem } from './components/SidebarItem' export { default as Modal } from './components/Modal' +export { HonorableModal } from './components/HonorableModal' export type { ChecklistProps, ChecklistStateProps, diff --git a/src/stories/Modal.stories.tsx b/src/stories/Modal.stories.tsx index ed9a0bdd..8a29f9de 100644 --- a/src/stories/Modal.stories.tsx +++ b/src/stories/Modal.stories.tsx @@ -1,8 +1,11 @@ import { Div, H3, P } from 'honorable' import { useState } from 'react' -import { Button, Card, FormField, Input, Modal } from '..' +import styled from 'styled-components' + +import { Button, Card, Code, FormField, Input, Modal } from '..' import { SEVERITIES } from '../components/Modal' +import { jsCode } from '../constants' export default { title: 'Modal', @@ -23,6 +26,31 @@ export default { }, } +function ExtraContent() { + return ( +
+

+ Some extra content to check that body scroll is disabled when Modal is + open. +

+ {Array.from({ length: 5 }).map(() => ( +

+ 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. +

+ ))} +
+ ) +} + function Template(args: any) { const [open, setOpen] = useState(false) @@ -118,77 +146,48 @@ function Template(args: any) { width="100%" padding="medium" > -
-

- Some extra content to check that body scroll is disabled when Modal - is open. -

-

- 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. -

-

- 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. -

-

- 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. -

-

- 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. -

-

- 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. -

-
+ + + + ) +} + +const NonScrollCode = styled(Code)((_) => ({ + overflow: 'hidden', +})) + +function NonScrollTemplate(args: any) { + const [open, setOpen] = useState(false) + + console.log('args', args) + + return ( + <> +

{args.header} Modal

+ + setOpen(false)} + actions={ + args.hasActions && ( + + ) + } + {...args} + > + {jsCode} + + + ) @@ -288,27 +287,36 @@ export const Default = Template.bind({}) Default.args = { header: 'Default', - title: 'Confirm Uninstall', form: false, size: 'medium', hasActions: true, + scrollable: true, } export const Form = Template.bind({}) Form.args = { header: 'Form', - title: 'Access Policy', form: true, hasActions: true, + scrollable: true, } export const PinnedToTop = PinnedToTopTemplate.bind({}) -Default.args = { +PinnedToTop.args = { header: 'Default', - title: 'Confirm Uninstall', form: false, size: 'medium', hasActions: true, + scrollable: true, +} + +export const NonScrollable = NonScrollTemplate.bind({}) + +NonScrollable.args = { + header: 'Non-scrollable', + size: 'large', + scrollable: false, + hasActions: true, } diff --git a/src/stories/Wizard.stories.tsx b/src/stories/Wizard.stories.tsx index 93e58345..1f9a3857 100644 --- a/src/stories/Wizard.stories.tsx +++ b/src/stories/Wizard.stories.tsx @@ -1,6 +1,8 @@ -import { Button, Flex, Modal as HonorableModal, P } from 'honorable' +import { Button, Flex, P } from 'honorable' import { type ReactElement, useEffect, useMemo, useState } from 'react' +import { useTheme } from 'styled-components' + import { type LayerPositionType } from '../components/Layer' import { Wizard } from '../components/wizard/Wizard' import { Picker, type StepConfig } from '../components/wizard/Picker' @@ -16,6 +18,7 @@ import { Toast } from '../components/Toast' import FormField from '../components/FormField' import Modal from '../components/Modal' import GlobeIcon from '../components/icons/GlobeIcon' +import { HonorableModal } from '../components/HonorableModal' export default { title: 'Wizard', @@ -130,6 +133,7 @@ const DEFAULT_STEPS: Array = [ ] function ModalTemplate() { + const theme = useTheme() const [open, setOpen] = useState(true) const [confirmClose, setConfirmClose] = useState(false) const [visible, setVisible] = useState(false) @@ -140,12 +144,16 @@ function ModalTemplate() { (inProgress ? setConfirmClose(true) : setOpen(false))}