Skip to content

Commit 2d3b660

Browse files
committed
Add types modal scaffold
1 parent 3af1af3 commit 2d3b660

File tree

15 files changed

+399
-19
lines changed

15 files changed

+399
-19
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@date-fns/docs": "0.12.1",
1818
"@sentry/browser": "^5.30.0",
1919
"@sentry/tracing": "^5.30.0",
20-
"@switcher/preact": "^1.1.2",
20+
"@switcher/preact": "2.3.0",
2121
"@types/assets-webpack-plugin": "^7.1.0",
2222
"@types/copy-webpack-plugin": "^10.1.0",
2323
"@types/cors": "^2.8.7",
@@ -60,7 +60,7 @@
6060
"mocha": "^8.2.1",
6161
"null-loader": "^4.0.1",
6262
"power-assert": "^1.6.1",
63-
"preact": "^10.4.8",
63+
"preact": "^10.11.3",
6464
"preact-render-to-string": "^5.1.10",
6565
"prettier": "2.1.1",
6666
"prismjs": "^1.23.0",

src/server/template/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const template = ({ body }: Params = {}) =>
5858
</head>
5959
<body>
6060
<div id="root">${body ?? ''}</div>
61+
<div id="portals"></div>
6162
6263
<script src="${entryPath('main', 'js')}"></script>
6364

src/ui/components/DocType/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ interface Props {
66
}
77

88
export const DocType: FunctionComponent<Props> = ({ type }) => {
9-
console.log({ type })
9+
// console.log({ type })
1010

1111
switch (type.type) {
1212
case 'intrinsic':
@@ -25,7 +25,7 @@ export const DocType: FunctionComponent<Props> = ({ type }) => {
2525
return (
2626
<span>
2727
<a
28-
href={`#types/${
28+
href={`#types/${type.name}/${
2929
/* TODO: Get rid of it one TypeDoc adds it */
3030
((type as unknown) as { id: number }).id
3131
}`}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { h } from 'preact'
2+
3+
export const ModalPortalCloseIcon = () => (
4+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
5+
<path
6+
fill="currentColor"
7+
d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"
8+
/>
9+
</svg>
10+
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import classNames from 'classnames'
2+
import { ComponentChildren, FunctionComponent, h } from 'preact'
3+
import { createPortal } from 'preact/compat'
4+
import { Ref, useEffect } from 'preact/hooks'
5+
import { ModalPortalCloseIcon } from './CloseIcon'
6+
import * as styles from './styles.css'
7+
8+
export interface ModalPortalProps {
9+
children: ComponentChildren
10+
close: () => void
11+
bare?: boolean
12+
size?: keyof typeof styles.window
13+
closeOnOverlayClick?: boolean
14+
adjusted?: boolean
15+
overlayRef?: Ref<HTMLDivElement | null>
16+
}
17+
18+
export const ModalPortal: FunctionComponent<ModalPortalProps> = ({
19+
children,
20+
close,
21+
bare,
22+
size = 'medium',
23+
closeOnOverlayClick,
24+
adjusted,
25+
overlayRef,
26+
}) => {
27+
const portals = document.getElementById('portals')
28+
29+
useEffect(() => {
30+
if (!portals) return
31+
document.body.style.overflow = 'hidden'
32+
return () => (document.body.style.overflow = 'auto')
33+
}, [portals])
34+
35+
if (!portals) return null
36+
37+
return createPortal(
38+
<div
39+
class={classNames(styles.overlay, !!adjusted && styles.overlayAdjusted)}
40+
ref={overlayRef}
41+
data-testid="modal-overlay"
42+
>
43+
{bare ? (
44+
children
45+
) : (
46+
<div
47+
class={styles.windowWrapper}
48+
onClick={(e: MouseEvent) => {
49+
if (closeOnOverlayClick) e.target === e.currentTarget && close()
50+
}}
51+
>
52+
<div class={styles.window[size]}>
53+
<button class={styles.close} onClick={close}>
54+
<ModalPortalCloseIcon />
55+
</button>
56+
57+
{children}
58+
</div>
59+
</div>
60+
)}
61+
</div>,
62+
portals
63+
)
64+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { globalStyle, style, styleVariants } from '@vanilla-extract/css'
2+
3+
export const overlay = style({
4+
zIndex: 20,
5+
position: 'fixed',
6+
left: '0',
7+
top: '0',
8+
right: '0',
9+
bottom: '0',
10+
background: '#080005d4',
11+
overflowY: 'auto',
12+
display: 'flex',
13+
justifyContent: 'center',
14+
})
15+
16+
export const overlayAdjusted = style({
17+
alignItems: 'center',
18+
})
19+
20+
export const windowWrapper = style({
21+
width: '100%',
22+
padding: '4rem',
23+
24+
'@media': {
25+
'(max-width: 1024px)': {
26+
padding: '1rem',
27+
},
28+
},
29+
})
30+
31+
export const windowBase = style({
32+
background: '#fffdf9',
33+
borderRadius: '4px',
34+
color: '#4a3142',
35+
width: '100%',
36+
margin: '0 auto',
37+
})
38+
39+
export const window = styleVariants({
40+
small: [
41+
windowBase,
42+
{
43+
maxWidth: '40rem',
44+
},
45+
],
46+
47+
medium: [
48+
windowBase,
49+
{
50+
maxWidth: '50rem',
51+
},
52+
],
53+
})
54+
55+
export const close = style({
56+
border: '0',
57+
background: 'transparent',
58+
position: 'fixed',
59+
width: '3rem',
60+
height: '3rem',
61+
color: '#5d3861',
62+
cursor: 'pointer',
63+
64+
':hover': {
65+
color: '#c482cb',
66+
},
67+
68+
selectors: {
69+
[`${window.small} &`]: {
70+
marginLeft: '41rem',
71+
},
72+
73+
[`${window.medium} &`]: {
74+
marginLeft: '51rem',
75+
},
76+
},
77+
})
78+
79+
globalStyle(`${close} svg`, {
80+
width: '100%',
81+
})

src/ui/components/Modals/index.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { ModalPortal, ModalPortalProps } from '~/ui/components/ModalPortal'
2+
import {
3+
ComponentChild,
4+
ComponentChildren,
5+
createContext,
6+
FunctionComponent,
7+
h,
8+
} from 'preact'
9+
import {
10+
MutableRef,
11+
Ref,
12+
useContext,
13+
useMemo,
14+
useRef,
15+
useState,
16+
} from 'preact/hooks'
17+
import { useRefState } from '~/utils/useRefState'
18+
19+
export interface ModalComponent {
20+
id: string
21+
component: ComponentChildren
22+
}
23+
24+
export type ModalOverlayRef = MutableRef<HTMLDivElement | null>
25+
26+
export type ModalOnCloseRef = MutableRef<(() => unknown) | null>
27+
28+
export interface ModalAPI {
29+
modal: ModalComponent | undefined
30+
props: ShowModalInnerProps
31+
showModal: ShowModal
32+
overlayRef: ModalOverlayRef
33+
onCloseRef: ModalOnCloseRef
34+
}
35+
36+
export type ShowModalInnerProps = Pick<
37+
ModalPortalProps,
38+
'size' | 'bare' | 'closeOnOverlayClick' | 'adjusted'
39+
>
40+
41+
export interface ShowModalProps {
42+
modalId: string
43+
component: ComponentChild | null
44+
props?: ShowModalInnerProps
45+
onClose?: () => unknown
46+
}
47+
48+
export type ShowModal = (props: ShowModalProps) => void
49+
50+
export const ModalsContext = createContext<ModalAPI>({
51+
modal: undefined,
52+
props: {},
53+
showModal: () => {},
54+
overlayRef: { current: null },
55+
onCloseRef: { current: null },
56+
})
57+
58+
export interface ModalsProps {
59+
api: ModalAPI
60+
}
61+
62+
export function Modals({
63+
api: { modal, props, showModal, overlayRef },
64+
}: ModalsProps) {
65+
if (!modal) return null
66+
return (
67+
<ModalPortal
68+
close={() => {
69+
showModal({ modalId: modal.id, component: undefined })
70+
}}
71+
overlayRef={overlayRef}
72+
{...props}
73+
>
74+
{modal.component}
75+
</ModalPortal>
76+
)
77+
}
78+
79+
export function useModals(): ModalAPI {
80+
const [modal, setModal] = useRefState<ModalComponent | undefined>(undefined)
81+
const [props, setProps] = useState<ShowModalInnerProps>({ size: 'medium' })
82+
const overlayRef: ModalOverlayRef = useRef(null)
83+
const onCloseRef: ModalOnCloseRef = useRef(null)
84+
85+
const showModal: ShowModal = ({
86+
modalId,
87+
component,
88+
props: newProps,
89+
onClose,
90+
}) => {
91+
// Call current modal onClose callback
92+
if (!component) onCloseRef.current?.()
93+
// Assign new callback
94+
onCloseRef.current = onClose || null
95+
96+
// Ignore close if another modal got opened
97+
if (!component && modalId !== modal.current?.id) return
98+
99+
setModal(component ? { id: modalId, component } : undefined)
100+
setProps(newProps || {})
101+
}
102+
103+
return { modal: modal.current, props, showModal, overlayRef, onCloseRef }
104+
}
105+
106+
export interface ModalPropsBase {
107+
close: () => void
108+
overlayRef: ModalOverlayRef
109+
}
110+
111+
export interface ModalPropsExtra {
112+
onClose: () => unknown
113+
}
114+
115+
export function createModal<Props extends Record<string, any>>(
116+
Component: FunctionComponent<Props & ModalPropsBase>,
117+
innerProps?: Omit<ModalPortalProps, keyof ModalPropsBase | 'children'>
118+
) {
119+
return (): ((props: Props & ModalPropsExtra) => void) => {
120+
const modalId = useMemo(() => Date.now().toString(), [])
121+
const { showModal, overlayRef } = useContext(ModalsContext)
122+
123+
return ({ onClose, ...props }) => {
124+
showModal({
125+
modalId,
126+
component: (
127+
// @ts-ignore: we're tricking TypeScript, this is ok
128+
<Component
129+
{...props}
130+
close={() => {
131+
showModal({ modalId, component: null })
132+
}}
133+
overlayRef={overlayRef}
134+
/>
135+
),
136+
props: innerProps,
137+
onClose,
138+
})
139+
}
140+
}
141+
}

src/ui/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { NotFound } from '~/ui/screens/NotFound'
77
import 'reset.css/reset.css?global'
88
import './global.css?global'
99
import { defaultSubmodule } from '@date-fns/docs/consts'
10+
import { Modals, ModalsContext, useModals } from './components/Modals'
1011

1112
const win = typeof window !== 'undefined' ? window : undefined
1213

@@ -17,9 +18,14 @@ export const UI = () => {
1718
win?.ga?.('send', 'pageview')
1819
}, [JSON.stringify(location)])
1920

21+
const modalsApi = useModals()
22+
2023
return (
2124
<>
22-
<Content location={location} />
25+
<ModalsContext.Provider value={modalsApi}>
26+
<Content location={location} />
27+
<Modals api={modalsApi} />
28+
</ModalsContext.Provider>
2329
</>
2430
)
2531
}

src/ui/screens/Docs/Doc/TSDoc/Returns/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ interface Props {
1111

1212
export const Returns: FunctionComponent<Props> = ({ fn }) => {
1313
const description = useMemo(() => findReturns(fn), [fn])
14-
console.log({ fn })
1514
return (
1615
<DocReturns
1716
returns={
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { h } from 'preact'
2+
import { DeclarationReflection } from 'typedoc'
3+
import { createModal } from '~/ui/components/Modals'
4+
import * as styles from './styles.css'
5+
6+
export interface TypesModalProps {
7+
typeId: number
8+
tsdoc: DeclarationReflection
9+
}
10+
11+
export const useTypesModal = createModal<TypesModalProps>(
12+
({ typeId, tsdoc }) => {
13+
return (
14+
<div class={styles.wrapper}>
15+
<div class={styles.inner}>
16+
<div class={styles.nav}>TODO: Navigation</div>
17+
<div class={styles.content}>TODO: Content</div>
18+
</div>
19+
</div>
20+
)
21+
},
22+
{ size: 'medium', closeOnOverlayClick: true }
23+
)

0 commit comments

Comments
 (0)