Skip to content
This repository was archived by the owner on Jun 20, 2022. It is now read-only.

Commit 0262a1c

Browse files
authored
Merge pull request #9 from smooth-code/modals
feat: add Modal
2 parents 3935f6a + 88bc002 commit 0262a1c

18 files changed

+551
-188
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
'react/forbid-prop-types': 'off',
1212
'react/prop-types': 'off',
1313
'react/require-default-props': 'off',
14+
'react/sort-comp': 'off',
1415

1516
'import/prefer-default-export': 'off',
1617

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@
7878
"recompact": "^3.3.0"
7979
},
8080
"peerDependencies": {
81-
"react": "^16.2.0",
82-
"react-dom": "^16.2.0",
83-
"styled-components": "^3.1.6"
81+
"react": ">=16.0.0",
82+
"react-dom": ">=16.0.0",
83+
"styled-components": "=>3.0.0"
8484
}
8585
}

src/Modal.js

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import styled from 'styled-components'
66
import classNames from 'classnames'
77
import * as defaultTheme from './theme/defaultTheme'
88
import { th } from './utils'
9+
import Transition from './Transition'
910

10-
class Portal extends React.Component {
11+
class ModalComponent extends React.Component {
1112
constructor(props) {
1213
super(props)
1314
if (!this.container && typeof document !== 'undefined') {
@@ -16,41 +17,57 @@ class Portal extends React.Component {
1617
}
1718
}
1819

20+
handleKeyup = ({ keyCode }) => {
21+
if (keyCode === 27 /* Escape */) {
22+
this.props.onClose()
23+
}
24+
}
25+
1926
componentWillUnmount() {
2027
document.body.removeChild(this.container)
28+
document.removeEventListener('keyup', this.handleKeyup)
29+
}
30+
31+
componentDidUpdate() {
32+
if (this.props.opened) {
33+
document.body.style.overflow = 'hidden'
34+
document.addEventListener('keyup', this.handleKeyup)
35+
} else {
36+
document.body.style.overflow = null
37+
document.removeEventListener('keyup', this.handleKeyup)
38+
}
2139
}
2240

2341
render() {
42+
const { className, theme, opened, onClose, children, ...props } = this.props
2443
if (!this.container) return null
25-
return createPortal(this.props.children, this.container)
44+
return createPortal(
45+
<Transition ms={theme.modalTransitionDuration} toggle={this.props.opened}>
46+
{({ entering, exiting }) => (
47+
<div
48+
role="dialog"
49+
tabIndex="-1"
50+
className={classNames(
51+
'sui-modal',
52+
{
53+
'sui-modal-opened': opened || exiting || entering,
54+
'sui-modal-fade-in': entering,
55+
'sui-modal-fade-out': exiting,
56+
},
57+
className,
58+
)}
59+
{...props}
60+
>
61+
<div className="sui-modal-backdrop" onClick={onClose} />
62+
{children}
63+
</div>
64+
)}
65+
</Transition>,
66+
this.container,
67+
)
2668
}
2769
}
2870

29-
const ModalComponent = ({
30-
className,
31-
theme,
32-
opened,
33-
onClose,
34-
children,
35-
...props
36-
}) => (
37-
<Portal>
38-
<div
39-
role="dialog"
40-
tabIndex="-1"
41-
className={classNames(
42-
'sui-modal',
43-
{ 'sui-modal-opened': opened },
44-
className,
45-
)}
46-
{...props}
47-
>
48-
<div className="sui-modal-backdrop" onClick={onClose} />
49-
{children}
50-
</div>
51-
</Portal>
52-
)
53-
5471
const Modal = styled(ModalComponent)`
5572
position: fixed;
5673
top: 0;
@@ -62,15 +79,22 @@ const Modal = styled(ModalComponent)`
6279
overflow: hidden;
6380
outline: 0;
6481
opacity: 0;
65-
transition: opacity 1000ms;
82+
transition: opacity ${th('modalTransitionDuration')}ms;
6683
6784
&.sui-modal-opened {
6885
display: block;
6986
overflow-x: hidden;
7087
overflow-y: auto;
88+
}
89+
90+
&.sui-modal-fade-in {
7191
opacity: 1;
7292
}
7393
94+
&.sui-modal-fade-out {
95+
opacity: 0;
96+
}
97+
7498
.sui-modal-backdrop {
7599
position: fixed;
76100
top: 0;
@@ -84,6 +108,8 @@ const Modal = styled(ModalComponent)`
84108

85109
Modal.propTypes = {
86110
children: PropTypes.node,
111+
opened: PropTypes.bool,
112+
onClose: PropTypes.func,
87113
}
88114

89115
Modal.defaultProps = {

src/Modal.md

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const ModalDialogExample = ModalDialog.extend`
3636

3737
### Live example
3838

39+
You can toggle a modal from a `Button` using the `Toggler`.
40+
3941
```js
4042
const ModalExample = () => (
4143
<Toggler>
@@ -55,7 +57,96 @@ const ModalExample = () => (
5557
<ModalBody>Modal body</ModalBody>
5658
<ModalFooter>
5759
<Button variant="primary">Save changes</Button>
58-
<Button variant="secondary">Close</Button>
60+
<Button variant="secondary" onClick={() => onToggle(false)}>
61+
Close
62+
</Button>
63+
</ModalFooter>
64+
</ModalContent>
65+
</ModalDialog>
66+
</Modal>
67+
</div>
68+
)}
69+
</Toggler>
70+
)
71+
;<ModalExample />
72+
```
73+
74+
### Scrolling long content
75+
76+
When modals become too long for the user’s viewport or device, they scroll independent of the page itself. Try the demo below to see what we mean.
77+
78+
```js
79+
const ModalExample = () => (
80+
<Toggler>
81+
{({ toggled, onToggle }) => (
82+
<div>
83+
<Button variant="primary" onClick={() => onToggle(true)}>
84+
Open modal
85+
</Button>
86+
<Modal opened={toggled} onClose={() => onToggle(false)}>
87+
<ModalDialog>
88+
<ModalContent>
89+
<ModalHeader>
90+
<Typography variant="h5" margin={false}>
91+
Modal title
92+
</Typography>
93+
</ModalHeader>
94+
<ModalBody>
95+
Cras mattis consectetur purus sit amet fermentum. Cras justo
96+
odio, dapibus ac facilisis in, egestas eget quam. Morbi leo
97+
risus, porta ac consectetur ac, vestibulum at eros. Praesent
98+
commodo cursus magna, vel scelerisque nisl consectetur et.
99+
Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor
100+
auctor. Aenean lacinia bibendum nulla sed consectetur. Praesent
101+
commodo cursus magna, vel scelerisque nisl consectetur et. Donec
102+
sed odio dui. Donec ullamcorper nulla non metus auctor
103+
fringilla. Cras mattis consectetur purus sit amet fermentum.
104+
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
105+
Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
106+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
107+
et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus
108+
dolor auctor. Aenean lacinia bibendum nulla sed consectetur.
109+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
110+
et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor
111+
fringilla. Cras mattis consectetur purus sit amet fermentum.
112+
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
113+
Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
114+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
115+
et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus
116+
dolor auctor. Aenean lacinia bibendum nulla sed consectetur.
117+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
118+
et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor
119+
fringilla. Cras mattis consectetur purus sit amet fermentum.
120+
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
121+
Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
122+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
123+
et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus
124+
dolor auctor. Aenean lacinia bibendum nulla sed consectetur.
125+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
126+
et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor
127+
fringilla. Cras mattis consectetur purus sit amet fermentum.
128+
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
129+
Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
130+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
131+
et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus
132+
dolor auctor. Aenean lacinia bibendum nulla sed consectetur.
133+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
134+
et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor
135+
fringilla. Cras mattis consectetur purus sit amet fermentum.
136+
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
137+
Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
138+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
139+
et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus
140+
dolor auctor. Aenean lacinia bibendum nulla sed consectetur.
141+
Praesent commodo cursus magna, vel scelerisque nisl consectetur
142+
et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor
143+
fringilla.
144+
</ModalBody>
145+
<ModalFooter>
146+
<Button variant="primary">Save changes</Button>
147+
<Button variant="secondary" onClick={() => onToggle(false)}>
148+
Close
149+
</Button>
59150
</ModalFooter>
60151
</ModalContent>
61152
</ModalDialog>

src/ModalBody.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,40 @@ import React from 'react'
22
import PropTypes from 'prop-types'
33
import styled from 'styled-components'
44
import classNames from 'classnames'
5+
import handleRef from './internal/handleRef'
6+
import setWithComponent from './internal/setWithComponent'
57
import * as defaultTheme from './theme/defaultTheme'
6-
import { mixin } from './utils'
8+
import { mixin, th } from './utils'
79

8-
const ModalBodyComponent = ({ className, theme, ...props }) => (
9-
<div className={classNames('sui-modal-body', className)} {...props} />
10+
const ModalBodyComponent = ({
11+
className,
12+
component: Component = 'div',
13+
theme,
14+
...props
15+
}) => (
16+
<Component className={classNames('sui-modal-body', className)} {...props} />
1017
)
1118

12-
const ModalBody = styled(ModalBodyComponent)`
19+
const ModalBodyRefComponent = handleRef(ModalBodyComponent)
20+
21+
const ModalBody = styled(ModalBodyRefComponent)`
1322
${mixin('base')};
1423
position: relative;
1524
/* Enable "flex-grow: 1" so that the body take up as much space as possible */
1625
/* when should there be a fixed height on ModalDialog. */
1726
flex: 1 1 auto;
18-
padding: 1rem;
27+
padding: ${th('modalInnerPadding')};
1928
`
2029

2130
ModalBody.propTypes = {
2231
children: PropTypes.node,
23-
className: PropTypes.string,
24-
theme: PropTypes.object,
2532
}
2633

2734
ModalBody.defaultProps = {
2835
theme: defaultTheme,
2936
}
3037

38+
setWithComponent(ModalBody, ModalBodyRefComponent)
39+
3140
/** @component */
3241
export default ModalBody

src/ModalContent.js

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,61 @@
11
import React from 'react'
22
import PropTypes from 'prop-types'
3-
import styled from 'styled-components'
3+
import styled, { css } from 'styled-components'
44
import classNames from 'classnames'
5+
import handleRef from './internal/handleRef'
6+
import setWithComponent from './internal/setWithComponent'
57
import * as defaultTheme from './theme/defaultTheme'
6-
import { mixin } from './utils'
8+
import { mixin, th, up } from './utils'
79

8-
const ModalContentComponent = ({ className, theme, ...props }) => (
9-
<div className={classNames('sui-modal-content', className)} {...props} />
10+
const ModalContentComponent = ({
11+
className,
12+
component: Component = 'div',
13+
theme,
14+
...props
15+
}) => (
16+
<Component
17+
className={classNames('sui-modal-content', className)}
18+
{...props}
19+
/>
1020
)
1121

22+
const ModalContentRefComponent = handleRef(ModalContentComponent)
23+
1224
const ModalContent = styled(ModalContentComponent)`
1325
${mixin('base')};
1426
position: relative;
1527
display: flex;
1628
flex-direction: column;
29+
/* Ensure "ModalContent" extends the full width of the parent "ModalDialog" */
1730
width: 100%;
1831
/* Counteract the pointer-events: none; in the ModalDialog */
1932
pointer-events: auto;
20-
background-color: #fff;
33+
background-color: ${th('modalContentBg')};
2134
background-clip: padding-box;
22-
border: 1px solid rgba(0, 0, 0, 0.2);
23-
border-radius: 0.3rem;
35+
border: ${th('modalContentBorderWidth')} solid
36+
${th('modalContentBorderColor')};
37+
border-radius: ${th('modalContentBorderRadius')};
38+
box-shadow: ${th('modalContentBoxShadowXs')};
2439
/* Remove focus outline from opened modal */
2540
outline: 0;
41+
42+
${up(
43+
'sm',
44+
css`
45+
box-shadow: ${th('modalContentBoxShadowSmUp')};
46+
`,
47+
)};
2648
`
2749

2850
ModalContent.propTypes = {
2951
children: PropTypes.node,
30-
className: PropTypes.string,
31-
theme: PropTypes.object,
3252
}
3353

3454
ModalContent.defaultProps = {
3555
theme: defaultTheme,
3656
}
3757

58+
setWithComponent(ModalContent, ModalContentRefComponent)
59+
3860
/** @component */
3961
export default ModalContent

0 commit comments

Comments
 (0)