Skip to content

Commit b78be4c

Browse files
authored
feat: add flyover design system component (#581)
1 parent b772ded commit b78be4c

File tree

7 files changed

+408
-14
lines changed

7 files changed

+408
-14
lines changed

.eslintrc.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module.exports = {
77
'plugin:storybook/recommended',
88
'prettier',
99
],
10+
plugins: ['prettier'],
1011
globals: {
1112
JSX: true,
1213
},
@@ -19,6 +20,7 @@ module.exports = {
1920
'import-newlines/enforce': 'off',
2021
// Allow css prop for styled-components
2122
'react/no-unknown-property': ['error', { ignore: ['css'] }],
23+
'prettier/prettier': 'error',
2224
},
2325
ignorePatterns: ['/coverage/**/*'],
2426
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"eslint-plugin-import": "2.29.1",
101101
"eslint-plugin-import-newlines": "1.3.4",
102102
"eslint-plugin-jsx-a11y": "6.8.0",
103+
"eslint-plugin-prettier": "^5.1.3",
103104
"eslint-plugin-react": "7.33.2",
104105
"eslint-plugin-react-hooks": "4.6.0",
105106
"eslint-plugin-storybook": "0.6.15",

src/components/Flyover.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { type ReactNode, type Ref, forwardRef, useEffect } from 'react'
2+
import PropTypes from 'prop-types'
3+
4+
import styled, { type StyledComponentPropsWithRef } from 'styled-components'
5+
6+
import { type ModalProps } from 'honorable'
7+
8+
import useLockedBody from '../hooks/useLockedBody'
9+
10+
import { CloseIcon } from '../icons'
11+
12+
import { HonorableModal } from './HonorableModal'
13+
import IconFrame from './IconFrame'
14+
15+
type FlyoverPropsType = Omit<ModalProps, 'size'> & {
16+
header?: ReactNode
17+
lockBody?: boolean
18+
scrollable?: boolean
19+
asForm?: boolean
20+
formProps?: StyledComponentPropsWithRef<'form'>
21+
width?: string
22+
minWidth?: number
23+
[x: string]: unknown
24+
}
25+
26+
const propTypes = {
27+
header: PropTypes.node,
28+
lockBody: PropTypes.bool,
29+
scrollable: PropTypes.bool,
30+
asForm: PropTypes.bool,
31+
width: PropTypes.string,
32+
minWidth: PropTypes.number,
33+
} as const
34+
35+
const FlyoverSC = styled.div(({ theme }) => ({
36+
position: 'relative',
37+
backgroundColor: theme.colors['fill-zero'],
38+
height: '100%',
39+
display: 'flex',
40+
flexDirection: 'column',
41+
overflow: 'hidden',
42+
}))
43+
44+
const FlyoverContentSC = styled.div<{
45+
$scrollable: boolean
46+
}>(({ theme, $scrollable }) => ({
47+
position: 'relative',
48+
zIndex: 0,
49+
margin: 0,
50+
padding: theme.spacing.large,
51+
backgroundColor: theme.colors['fill-zero'],
52+
...theme.partials.text.body1,
53+
flexGrow: 1,
54+
...($scrollable
55+
? { overflow: 'auto' }
56+
: {
57+
display: 'flex',
58+
flexDirection: 'column',
59+
overflow: 'hidden',
60+
}),
61+
}))
62+
63+
const FlyoverHeaderWrapSC = styled.div(({ theme }) => ({
64+
alignItems: 'center',
65+
justifyContent: 'start',
66+
gap: theme.spacing.small,
67+
height: 56,
68+
borderBottom: `1px solid ${theme.colors.border}`,
69+
backgroundColor: theme.colors.grey[950],
70+
display: 'flex',
71+
padding: `${theme.spacing.small}px ${theme.spacing.medium}px`,
72+
}))
73+
74+
const FlyoverHeaderSC = styled.h1(({ theme }) => ({
75+
margin: 0,
76+
...theme.partials.text.subtitle1,
77+
color: theme.colors.semanticDefault,
78+
}))
79+
80+
function FlyoverRef(
81+
{
82+
children,
83+
header,
84+
open = false,
85+
onClose,
86+
lockBody = true,
87+
asForm = false,
88+
formProps = {},
89+
scrollable = true,
90+
width = '40%',
91+
minWidth = 570,
92+
...props
93+
}: FlyoverPropsType,
94+
ref: Ref<any>
95+
) {
96+
const [, setBodyLocked] = useLockedBody(open && lockBody)
97+
98+
useEffect(() => {
99+
setBodyLocked(lockBody && open)
100+
}, [lockBody, open, setBodyLocked])
101+
102+
return (
103+
<HonorableModal
104+
open={open}
105+
onClose={onClose}
106+
ref={ref}
107+
scrollable={scrollable}
108+
margin={0}
109+
padding={0}
110+
right="100%"
111+
height="100%"
112+
width={width}
113+
minWidth={minWidth}
114+
alignSelf="flex-end"
115+
BackdropProps={{ backgroundColor: 'transparent' }}
116+
InnerDefaultStyle={{
117+
opacity: 0,
118+
transform: 'translateX(0)',
119+
transition: 'transform 300ms ease, opacity 300ms ease',
120+
}}
121+
InnerTransitionStyle={{
122+
entering: { opacity: 1, transform: 'translateX(0)' },
123+
entered: { opacity: 1, transform: 'translateX(0)' },
124+
exiting: { opacity: 0, transform: 'translateX(1000px)' },
125+
exited: { opacity: 0, transform: 'translateX(1000px)' },
126+
}}
127+
{...props}
128+
>
129+
<FlyoverSC
130+
as={asForm ? 'form' : undefined}
131+
{...(asForm ? formProps : {})}
132+
>
133+
{!!header && (
134+
<FlyoverHeaderWrapSC ref={ref}>
135+
<IconFrame
136+
textValue=""
137+
display="flex"
138+
size="small"
139+
clickable
140+
onClick={onClose}
141+
icon={<CloseIcon />}
142+
/>
143+
<FlyoverHeaderSC>{header}</FlyoverHeaderSC>
144+
</FlyoverHeaderWrapSC>
145+
)}
146+
<FlyoverContentSC $scrollable={scrollable}>{children}</FlyoverContentSC>
147+
</FlyoverSC>
148+
</HonorableModal>
149+
)
150+
}
151+
152+
const Flyover = forwardRef(FlyoverRef)
153+
154+
Flyover.propTypes = propTypes
155+
156+
export default Flyover

src/components/HonorableModal.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ import {
1717
useState,
1818
} from 'react'
1919
import { createPortal } from 'react-dom'
20-
import { Transition } from 'react-transition-group'
20+
import { Transition, type TransitionStatus } from 'react-transition-group'
2121

2222
import { Div, useTheme } from 'honorable'
2323
import useRootStyles from 'honorable/dist/hooks/useRootStyles.js'
2424
import { useKeyDown } from '@react-hooks-library/core'
2525
import { type ComponentProps } from 'honorable/dist/types.js'
2626
import resolvePartStyles from 'honorable/dist/resolvers/resolvePartStyles.js'
2727
import filterUndefinedValues from 'honorable/dist/utils/filterUndefinedValues.js'
28+
import { type CSSObject } from 'styled-components'
2829

2930
export const modalParts = ['Backdrop'] as const
3031

@@ -33,13 +34,26 @@ export type ModalBaseProps = {
3334
onClose?: (event?: MouseEvent | KeyboardEvent) => void
3435
fade?: boolean
3536
transitionDuration?: number
37+
InnerTransitionStyle?: transitionConfig
38+
InnerDefaultStyle?: CSSObject
3639
disableEscapeKey?: boolean
3740
portal?: boolean
3841
}
3942

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

46+
type transitionConfig = {
47+
[key in TransitionStatus]?: CSSObject
48+
}
49+
50+
const defaultInnerTransitionStyle: transitionConfig = {
51+
entering: { opacity: 0 },
52+
entered: { opacity: 1 },
53+
exiting: { opacity: 0 },
54+
exited: { opacity: 0 },
55+
}
56+
4357
function ModalRef(props: ModalProps, ref: Ref<any>) {
4458
const {
4559
open = true,
@@ -48,6 +62,8 @@ function ModalRef(props: ModalProps, ref: Ref<any>) {
4862
transitionDuration = 250,
4963
disableEscapeKey = false,
5064
portal = false,
65+
InnerTransitionStyle,
66+
InnerDefaultStyle,
5167
...otherProps
5268
} = props
5369
const theme = useTheme()
@@ -159,30 +175,21 @@ function ModalRef(props: ModalProps, ref: Ref<any>) {
159175
function wrapFadeInner(element: ReactElement) {
160176
if (!fade) return element
161177

162-
const defaultStyle = {
178+
const defaultInnerStyle = {
163179
opacity: 0,
164180
transition: `opacity ${transitionDuration}ms ease`,
165-
...resolvePartStyles('InnerDefaultStyle', props, theme),
166-
}
167-
168-
const transitionStyles = {
169-
entering: { opacity: 1 },
170-
entered: { opacity: 1 },
171-
exiting: { opacity: 0 },
172-
exited: { opacity: 0 },
173-
...resolvePartStyles('InnerTransitionStyle', props, theme),
174181
}
175182

176183
return (
177184
<Transition
178185
in={isOpen && !isClosing}
179186
timeout={transitionDuration}
180187
>
181-
{(state: string) =>
188+
{(state) =>
182189
cloneElement(element, {
183190
...element.props,
184-
...defaultStyle,
185-
...transitionStyles[state as keyof typeof transitionStyles],
191+
...(InnerDefaultStyle || defaultInnerStyle),
192+
...(InnerTransitionStyle || defaultInnerTransitionStyle)[state],
186193
})
187194
}
188195
</Transition>

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export { default as Sidebar } from './components/Sidebar'
7272
export { default as SidebarSection } from './components/SidebarSection'
7373
export { default as SidebarItem } from './components/SidebarItem'
7474
export { default as Modal } from './components/Modal'
75+
export { default as Flyover } from './components/Flyover'
7576
export { HonorableModal } from './components/HonorableModal'
7677
export type {
7778
ChecklistProps,

0 commit comments

Comments
 (0)