Skip to content

Commit

Permalink
feat: add flyover design system component (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
renemennab authored Apr 18, 2024
1 parent b772ded commit b78be4c
Show file tree
Hide file tree
Showing 7 changed files with 408 additions and 14 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
'plugin:storybook/recommended',
'prettier',
],
plugins: ['prettier'],
globals: {
JSX: true,
},
Expand All @@ -19,6 +20,7 @@ module.exports = {
'import-newlines/enforce': 'off',
// Allow css prop for styled-components
'react/no-unknown-property': ['error', { ignore: ['css'] }],
'prettier/prettier': 'error',
},
ignorePatterns: ['/coverage/**/*'],
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"eslint-plugin-import": "2.29.1",
"eslint-plugin-import-newlines": "1.3.4",
"eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-storybook": "0.6.15",
Expand Down
156 changes: 156 additions & 0 deletions src/components/Flyover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { type ReactNode, type Ref, forwardRef, useEffect } from 'react'
import PropTypes from 'prop-types'

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

import { type ModalProps } from 'honorable'

import useLockedBody from '../hooks/useLockedBody'

import { CloseIcon } from '../icons'

import { HonorableModal } from './HonorableModal'
import IconFrame from './IconFrame'

type FlyoverPropsType = Omit<ModalProps, 'size'> & {
header?: ReactNode
lockBody?: boolean
scrollable?: boolean
asForm?: boolean
formProps?: StyledComponentPropsWithRef<'form'>
width?: string
minWidth?: number
[x: string]: unknown
}

const propTypes = {
header: PropTypes.node,
lockBody: PropTypes.bool,
scrollable: PropTypes.bool,
asForm: PropTypes.bool,
width: PropTypes.string,
minWidth: PropTypes.number,
} as const

const FlyoverSC = styled.div(({ theme }) => ({
position: 'relative',
backgroundColor: theme.colors['fill-zero'],
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}))

const FlyoverContentSC = styled.div<{
$scrollable: boolean
}>(({ theme, $scrollable }) => ({
position: 'relative',
zIndex: 0,
margin: 0,
padding: theme.spacing.large,
backgroundColor: theme.colors['fill-zero'],
...theme.partials.text.body1,
flexGrow: 1,
...($scrollable
? { overflow: 'auto' }
: {
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}),
}))

const FlyoverHeaderWrapSC = styled.div(({ theme }) => ({
alignItems: 'center',
justifyContent: 'start',
gap: theme.spacing.small,
height: 56,
borderBottom: `1px solid ${theme.colors.border}`,
backgroundColor: theme.colors.grey[950],
display: 'flex',
padding: `${theme.spacing.small}px ${theme.spacing.medium}px`,
}))

const FlyoverHeaderSC = styled.h1(({ theme }) => ({
margin: 0,
...theme.partials.text.subtitle1,
color: theme.colors.semanticDefault,
}))

function FlyoverRef(
{
children,
header,
open = false,
onClose,
lockBody = true,
asForm = false,
formProps = {},
scrollable = true,
width = '40%',
minWidth = 570,
...props
}: FlyoverPropsType,
ref: Ref<any>
) {
const [, setBodyLocked] = useLockedBody(open && lockBody)

useEffect(() => {
setBodyLocked(lockBody && open)
}, [lockBody, open, setBodyLocked])

return (
<HonorableModal
open={open}
onClose={onClose}
ref={ref}
scrollable={scrollable}
margin={0}
padding={0}
right="100%"
height="100%"
width={width}
minWidth={minWidth}
alignSelf="flex-end"
BackdropProps={{ backgroundColor: 'transparent' }}
InnerDefaultStyle={{
opacity: 0,
transform: 'translateX(0)',
transition: 'transform 300ms ease, opacity 300ms ease',
}}
InnerTransitionStyle={{
entering: { opacity: 1, transform: 'translateX(0)' },
entered: { opacity: 1, transform: 'translateX(0)' },
exiting: { opacity: 0, transform: 'translateX(1000px)' },
exited: { opacity: 0, transform: 'translateX(1000px)' },
}}
{...props}
>
<FlyoverSC
as={asForm ? 'form' : undefined}
{...(asForm ? formProps : {})}
>
{!!header && (
<FlyoverHeaderWrapSC ref={ref}>
<IconFrame
textValue=""
display="flex"
size="small"
clickable
onClick={onClose}
icon={<CloseIcon />}
/>
<FlyoverHeaderSC>{header}</FlyoverHeaderSC>
</FlyoverHeaderWrapSC>
)}
<FlyoverContentSC $scrollable={scrollable}>{children}</FlyoverContentSC>
</FlyoverSC>
</HonorableModal>
)
}

const Flyover = forwardRef(FlyoverRef)

Flyover.propTypes = propTypes

export default Flyover
35 changes: 21 additions & 14 deletions src/components/HonorableModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import {
useState,
} from 'react'
import { createPortal } from 'react-dom'
import { Transition } from 'react-transition-group'
import { Transition, type TransitionStatus } from 'react-transition-group'

import { Div, useTheme } from 'honorable'
import useRootStyles from 'honorable/dist/hooks/useRootStyles.js'
import { useKeyDown } from '@react-hooks-library/core'
import { type ComponentProps } from 'honorable/dist/types.js'
import resolvePartStyles from 'honorable/dist/resolvers/resolvePartStyles.js'
import filterUndefinedValues from 'honorable/dist/utils/filterUndefinedValues.js'
import { type CSSObject } from 'styled-components'

export const modalParts = ['Backdrop'] as const

Expand All @@ -33,13 +34,26 @@ export type ModalBaseProps = {
onClose?: (event?: MouseEvent | KeyboardEvent) => void
fade?: boolean
transitionDuration?: number
InnerTransitionStyle?: transitionConfig
InnerDefaultStyle?: CSSObject
disableEscapeKey?: boolean
portal?: boolean
}

export type ModalProps = ModalBaseProps &
ComponentProps<ModalBaseProps, 'div', (typeof modalParts)[number]>

type transitionConfig = {
[key in TransitionStatus]?: CSSObject
}

const defaultInnerTransitionStyle: transitionConfig = {
entering: { opacity: 0 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
}

function ModalRef(props: ModalProps, ref: Ref<any>) {
const {
open = true,
Expand All @@ -48,6 +62,8 @@ function ModalRef(props: ModalProps, ref: Ref<any>) {
transitionDuration = 250,
disableEscapeKey = false,
portal = false,
InnerTransitionStyle,
InnerDefaultStyle,
...otherProps
} = props
const theme = useTheme()
Expand Down Expand Up @@ -159,30 +175,21 @@ function ModalRef(props: ModalProps, ref: Ref<any>) {
function wrapFadeInner(element: ReactElement) {
if (!fade) return element

const defaultStyle = {
const defaultInnerStyle = {
opacity: 0,
transition: `opacity ${transitionDuration}ms ease`,
...resolvePartStyles('InnerDefaultStyle', props, theme),
}

const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
...resolvePartStyles('InnerTransitionStyle', props, theme),
}

return (
<Transition
in={isOpen && !isClosing}
timeout={transitionDuration}
>
{(state: string) =>
{(state) =>
cloneElement(element, {
...element.props,
...defaultStyle,
...transitionStyles[state as keyof typeof transitionStyles],
...(InnerDefaultStyle || defaultInnerStyle),
...(InnerTransitionStyle || defaultInnerTransitionStyle)[state],
})
}
</Transition>
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,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 { default as Flyover } from './components/Flyover'
export { HonorableModal } from './components/HonorableModal'
export type {
ChecklistProps,
Expand Down
Loading

0 comments on commit b78be4c

Please sign in to comment.