Skip to content

Commit 21a9e08

Browse files
committed
Add withModal Higher Order Component
It adds Modal to a provided component Refactored LoginPrompt with it and added stub Modal to button Add Documents
1 parent ae1f1c1 commit 21a9e08

File tree

6 files changed

+173
-92
lines changed

6 files changed

+173
-92
lines changed

src/components/withMenu.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@ import { useState } from 'react'
33

44
type WithMenuHOC = <ButtonProps, MenuProps>(
55
Button: React.FC<ButtonProps>,
6-
Menu: React.FC<MenuProps>,
6+
Menu: React.FC<MenuProps & CloseHandler>,
77
) => React.FC<
88
React.HTMLAttributes<HTMLDivElement> & {
99
buttonProps: ButtonProps
1010
menuProps: MenuProps
1111
}
1212
>
1313

14+
interface CloseHandler {
15+
onClickClose: () => void
16+
}
17+
1418
const withMenu: WithMenuHOC = <
1519
ButtonProps extends React.HTMLAttributes<HTMLElement>,
1620
MenuProps extends React.HTMLAttributes<HTMLElement>,
1721
>(
1822
Button: React.FC<ButtonProps>,
19-
Menu: React.FC<MenuProps>,
23+
Menu: React.FC<MenuProps & CloseHandler>,
2024
) => {
2125
const HOC = ({
2226
buttonProps,
@@ -52,16 +56,21 @@ const withMenu: WithMenuHOC = <
5256
onMouseEnter={() => setOpen(true)}
5357
onFocus={() => setOpen(true)}
5458
/>
55-
{open && (
56-
<div style={{ position: 'relative', width: 0, height: 0 }}>
57-
<div
58-
className="box p-0"
59-
style={{ position: 'absolute', right: 0, overflow: 'hidden' }}
60-
>
61-
<Menu {...menuProps} />
62-
</div>
59+
<div
60+
style={{
61+
position: 'relative',
62+
width: 0,
63+
height: 0,
64+
visibility: open ? 'visible' : 'hidden',
65+
}}
66+
>
67+
<div
68+
className="box p-0"
69+
style={{ position: 'absolute', right: 0, overflow: 'hidden' }}
70+
>
71+
<Menu {...menuProps} onClickClose={() => setOpen(false)} />
6372
</div>
64-
)}
73+
</div>
6574
</div>
6675
)
6776
}

src/components/withModal.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import classNames from 'classnames'
2+
import React from 'react'
3+
import { useState } from 'react'
4+
import Modal from 'react-modal'
5+
6+
type WithModalHOC = <ButtonProps, ContentProps>(
7+
Button: React.FC<ButtonProps>,
8+
Content: React.FC<ContentProps>,
9+
title?: string,
10+
) => React.FC<
11+
React.HTMLAttributes<HTMLDivElement> & {
12+
buttonProps?: ButtonProps
13+
contentProps?: ContentProps
14+
title?: string
15+
}
16+
>
17+
18+
const withModal: WithModalHOC = <
19+
ButtonProps extends React.HTMLAttributes<HTMLElement>,
20+
ContentProps extends React.HTMLAttributes<HTMLElement>,
21+
>(
22+
Button: React.FC<ButtonProps>,
23+
Content: React.FC<ContentProps>,
24+
title = '',
25+
) => {
26+
const HOC = ({
27+
buttonProps = {} as ButtonProps,
28+
contentProps = {} as ContentProps,
29+
title: dynamicTitle = '',
30+
...rest
31+
}: {
32+
buttonProps?: ButtonProps
33+
contentProps?: ContentProps
34+
title?: string
35+
} & React.HTMLAttributes<HTMLElement>) => {
36+
const [promptOpen, setPromptOpen] = useState(false)
37+
38+
const finalTitle = dynamicTitle || title || ''
39+
40+
return (
41+
<>
42+
<Button
43+
{...buttonProps}
44+
{...rest}
45+
className={classNames(buttonProps?.className, rest?.className)}
46+
onClick={e => {
47+
e.preventDefault()
48+
setPromptOpen(true)
49+
if (buttonProps.onClick) buttonProps.onClick(e)
50+
}}
51+
/>
52+
53+
{promptOpen && (
54+
<Modal
55+
isOpen={promptOpen}
56+
onRequestClose={() => setPromptOpen(false)}
57+
contentLabel="Connect your Solid Pod"
58+
overlayClassName={{
59+
base: 'modal modal-background is-active',
60+
afterOpen: '',
61+
beforeClose: '',
62+
}}
63+
className={{
64+
base: 'modal-content',
65+
afterOpen: '',
66+
beforeClose: '',
67+
}}
68+
closeTimeoutMS={50}
69+
>
70+
<button
71+
className="modal-close"
72+
onClick={() => setPromptOpen(false)}
73+
>
74+
close
75+
</button>
76+
77+
<div className="card">
78+
<header className="card-header">
79+
<p className="card-header-title">{finalTitle}</p>
80+
</header>
81+
<div className="card-content">
82+
<Content {...contentProps} />
83+
</div>
84+
</div>
85+
</Modal>
86+
)}
87+
</>
88+
)
89+
}
90+
91+
HOC.displayName = `WithModal(${getDisplayName(Button)})`
92+
return HOC
93+
}
94+
95+
function getDisplayName<T>(WrappedComponent: React.FC<T>) {
96+
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
97+
}
98+
99+
export default withModal

src/features/control/AddButtonWithMenu.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react'
22
import withMenu from '../../components/withMenu'
33
import AppButton from '../../components/AppButton'
4+
import withModal from '../../components/withModal'
45

56
const AddButton = ({ ...props }) => (
67
<AppButton {...props}>
@@ -9,18 +10,32 @@ const AddButton = ({ ...props }) => (
910
)
1011

1112
interface MenuProps {
12-
onAddDocument: () => void
1313
onAddMath: () => void
1414
}
1515

16-
const Menu = ({ onAddDocument, onAddMath }: MenuProps) => (
16+
const Button = ({ ...props }) => <a {...props}>Add Document</a>
17+
const ModalContent = () => <div>Modal Content</div>
18+
19+
const AddDocument = withModal(Button, ModalContent, 'Add a document')
20+
21+
const Menu = ({
22+
onAddMath,
23+
onClickClose,
24+
}: MenuProps & { onClickClose: () => void }) => (
1725
<div className="menu box p-0">
1826
<ul className="menu-list">
1927
<li>
20-
<a onClick={onAddDocument}>Add Document</a>
28+
<AddDocument buttonProps={{ onClick: onClickClose }} />
2129
</li>
2230
<li>
23-
<a onClick={onAddMath}>Add Math</a>
31+
<a
32+
onClick={() => {
33+
onClickClose()
34+
onAddMath()
35+
}}
36+
>
37+
Add Math
38+
</a>
2439
</li>
2540
</ul>
2641
</div>

src/features/control/ControlPanel.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const ControlPanel: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
1919
<AddButtonWithMenu
2020
buttonProps={{}}
2121
menuProps={{
22-
onAddDocument: () => console.log('add document'),
2322
onAddMath: () => console.log('add math'),
2423
}}
2524
/>

src/features/login/Login.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ const Login = ({ ...props }) => {
4747
) : (
4848
<LoginPrompt
4949
{...props}
50-
onLogin={oidcIssuer => dispatch(login(oidcIssuer))}
50+
contentProps={{
51+
onLogin: (oidcIssuer: string) => {
52+
dispatch(login(oidcIssuer))
53+
},
54+
}}
55+
buttonProps={{}}
5156
/>
5257
)
5358
}

src/features/login/LoginPrompt.tsx

Lines changed: 29 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React, { useState } from 'react'
2-
import Modal from 'react-modal'
32
import AppButton from '../../components/AppButton'
3+
import withModal from '../../components/withModal'
44

5-
interface Props {
5+
interface LoginFormProps {
66
onLogin: (oidcIssuer: string) => void
77
}
88

9-
const LoginPrompt: React.FC<Props> = ({ onLogin, ...props }: Props) => {
10-
const [promptOpen, setPromptOpen] = useState(false)
9+
const LoginButton = ({ ...props }) => <AppButton {...props}>Login</AppButton>
10+
11+
const LoginForm = ({ onLogin }: LoginFormProps) => {
1112
const [idp, setIdp] = useState(
1213
localStorage.getItem('idp') ?? 'https://solidcommunity.net',
1314
)
@@ -23,80 +24,33 @@ const LoginPrompt: React.FC<Props> = ({ onLogin, ...props }: Props) => {
2324
const newValue = e.currentTarget.value
2425
setIdp(newValue)
2526
}
26-
27-
if (!promptOpen) {
28-
return (
29-
<>
30-
<AppButton
31-
{...props}
32-
onClick={e => {
33-
e.preventDefault()
34-
setPromptOpen(true)
35-
}}
36-
>
37-
Login
38-
</AppButton>
39-
</>
40-
)
41-
}
42-
4327
return (
44-
<>
45-
<Modal
46-
isOpen={promptOpen}
47-
onRequestClose={() => setPromptOpen(false)}
48-
contentLabel="Connect your Solid Pod"
49-
overlayClassName={{
50-
base: 'modal modal-background is-active',
51-
afterOpen: '',
52-
beforeClose: '',
53-
}}
54-
className={{
55-
base: 'modal-content',
56-
afterOpen: '',
57-
beforeClose: '',
58-
}}
59-
closeTimeoutMS={50}
60-
>
61-
<button className="modal-close" onClick={() => setPromptOpen(false)}>
62-
close
63-
</button>
64-
65-
<div className="card">
66-
<header className="card-header">
67-
<p className="card-header-title">
68-
Select your Solid identity provider
69-
</p>
70-
</header>
71-
<div className="card-content">
72-
<form onSubmit={onSubmit}>
73-
<div className="field">
74-
<div className="control">
75-
<input
76-
id="idp"
77-
className="input"
78-
type="url"
79-
value={idp}
80-
onChange={onChangeInput}
81-
placeholder="Where is your Solid Pod?"
82-
/>
83-
</div>
84-
</div>
85-
<div className="field">
86-
<div className="control">
87-
<input
88-
type="submit"
89-
value="Connect"
90-
className="button is-link"
91-
/>
92-
</div>
93-
</div>
94-
</form>
95-
</div>
28+
<form onSubmit={onSubmit}>
29+
<div className="field">
30+
<div className="control">
31+
<input
32+
id="idp"
33+
className="input"
34+
type="url"
35+
value={idp}
36+
onChange={onChangeInput}
37+
placeholder="Where is your Solid Pod?"
38+
/>
39+
</div>
40+
</div>
41+
<div className="field">
42+
<div className="control">
43+
<input type="submit" value="Connect" className="button is-link" />
9644
</div>
97-
</Modal>
98-
</>
45+
</div>
46+
</form>
9947
)
10048
}
10149

50+
const LoginPrompt = withModal(
51+
LoginButton,
52+
LoginForm,
53+
'Select your Solid identity provider',
54+
)
55+
10256
export default LoginPrompt

0 commit comments

Comments
 (0)