Skip to content

Commit 1576575

Browse files
authored
Merge pull request #2049 from lindapaiste/refactor/modal
Combined Modal component for New File, New Folder, and Upload.
2 parents 6bd9d4a + b530cff commit 1576575

File tree

5 files changed

+133
-207
lines changed

5 files changed

+133
-207
lines changed

Diff for: client/modules/IDE/components/Modal.jsx

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import classNames from 'classnames';
2+
import PropTypes from 'prop-types';
3+
import React, { useEffect, useRef } from 'react';
4+
import ExitIcon from '../../../images/exit.svg';
5+
6+
// Common logic from NewFolderModal, NewFileModal, UploadFileModal
7+
8+
const Modal = ({
9+
title,
10+
onClose,
11+
closeAriaLabel,
12+
contentClassName,
13+
children
14+
}) => {
15+
const modalRef = useRef(null);
16+
17+
const handleOutsideClick = (e) => {
18+
// ignore clicks on the component itself
19+
if (modalRef.current?.contains?.(e.target)) return;
20+
21+
onClose();
22+
};
23+
24+
useEffect(() => {
25+
modalRef.current?.focus();
26+
document.addEventListener('click', handleOutsideClick, false);
27+
28+
return () => {
29+
document.removeEventListener('click', handleOutsideClick, false);
30+
};
31+
}, []);
32+
33+
return (
34+
<section className="modal" ref={modalRef}>
35+
<div className={classNames('modal-content', contentClassName)}>
36+
<div className="modal__header">
37+
<h2 className="modal__title">{title}</h2>
38+
<button
39+
className="modal__exit-button"
40+
onClick={onClose}
41+
aria-label={closeAriaLabel}
42+
>
43+
<ExitIcon focusable="false" aria-hidden="true" />
44+
</button>
45+
</div>
46+
{children}
47+
</div>
48+
</section>
49+
);
50+
};
51+
52+
Modal.propTypes = {
53+
title: PropTypes.string.isRequired,
54+
onClose: PropTypes.func.isRequired,
55+
closeAriaLabel: PropTypes.string.isRequired,
56+
contentClassName: PropTypes.string,
57+
children: PropTypes.node.isRequired
58+
};
59+
60+
Modal.defaultProps = {
61+
contentClassName: ''
62+
};
63+
64+
export default Modal;

Diff for: client/modules/IDE/components/NewFileModal.jsx

+16-77
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,22 @@
1-
import PropTypes from 'prop-types';
21
import React from 'react';
3-
import { connect } from 'react-redux';
4-
import { bindActionCreators } from 'redux';
5-
import { withTranslation } from 'react-i18next';
2+
import { useDispatch } from 'react-redux';
3+
import { useTranslation } from 'react-i18next';
4+
import Modal from './Modal';
65
import NewFileForm from './NewFileForm';
76
import { closeNewFileModal } from '../actions/ide';
8-
import ExitIcon from '../../../images/exit.svg';
97

10-
// At some point this will probably be generalized to a generic modal
11-
// in which you can insert different content
12-
// but for now, let's just make this work
13-
class NewFileModal extends React.Component {
14-
constructor(props) {
15-
super(props);
16-
this.focusOnModal = this.focusOnModal.bind(this);
17-
this.handleOutsideClick = this.handleOutsideClick.bind(this);
18-
}
19-
20-
componentDidMount() {
21-
this.focusOnModal();
22-
document.addEventListener('click', this.handleOutsideClick, false);
23-
}
24-
25-
componentWillUnmount() {
26-
document.removeEventListener('click', this.handleOutsideClick, false);
27-
}
28-
29-
handleOutsideClick(e) {
30-
// ignore clicks on the component itself
31-
if (e.path.includes(this.modal)) return;
32-
33-
this.props.closeNewFileModal();
34-
}
35-
36-
focusOnModal() {
37-
this.modal.focus();
38-
}
39-
40-
render() {
41-
return (
42-
<section
43-
className="modal"
44-
ref={(element) => {
45-
this.modal = element;
46-
}}
47-
>
48-
<div className="modal-content">
49-
<div className="modal__header">
50-
<h2 className="modal__title">
51-
{this.props.t('NewFileModal.Title')}
52-
</h2>
53-
<button
54-
className="modal__exit-button"
55-
onClick={this.props.closeNewFileModal}
56-
aria-label={this.props.t('NewFileModal.CloseButtonARIA')}
57-
>
58-
<ExitIcon focusable="false" aria-hidden="true" />
59-
</button>
60-
</div>
61-
<NewFileForm focusOnModal={this.focusOnModal} />
62-
</div>
63-
</section>
64-
);
65-
}
66-
}
67-
68-
NewFileModal.propTypes = {
69-
closeNewFileModal: PropTypes.func.isRequired,
70-
t: PropTypes.func.isRequired
8+
const NewFileModal = () => {
9+
const { t } = useTranslation();
10+
const dispatch = useDispatch();
11+
return (
12+
<Modal
13+
title={t('NewFileModal.Title')}
14+
closeAriaLabel={t('NewFileModal.CloseButtonARIA')}
15+
onClose={() => dispatch(closeNewFileModal())}
16+
>
17+
<NewFileForm />
18+
</Modal>
19+
);
7120
};
7221

73-
function mapStateToProps() {
74-
return {};
75-
}
76-
77-
function mapDispatchToProps(dispatch) {
78-
return bindActionCreators({ closeNewFileModal }, dispatch);
79-
}
80-
81-
export default withTranslation()(
82-
connect(mapStateToProps, mapDispatchToProps)(NewFileModal)
83-
);
22+
export default NewFileModal;

Diff for: client/modules/IDE/components/NewFolderModal.jsx

+18-57
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,23 @@
1-
import PropTypes from 'prop-types';
21
import React from 'react';
3-
import { withTranslation } from 'react-i18next';
2+
import { useTranslation } from 'react-i18next';
3+
import { useDispatch } from 'react-redux';
4+
import { closeNewFolderModal } from '../actions/ide';
5+
import Modal from './Modal';
46
import NewFolderForm from './NewFolderForm';
5-
import ExitIcon from '../../../images/exit.svg';
67

7-
class NewFolderModal extends React.Component {
8-
constructor(props) {
9-
super(props);
10-
this.handleOutsideClick = this.handleOutsideClick.bind(this);
11-
}
12-
13-
componentDidMount() {
14-
this.newFolderModal.focus();
15-
document.addEventListener('click', this.handleOutsideClick, false);
16-
}
17-
18-
componentWillUnmount() {
19-
document.removeEventListener('click', this.handleOutsideClick, false);
20-
}
21-
22-
handleOutsideClick(e) {
23-
// ignore clicks on the component itself
24-
if (e.path.includes(this.newFolderModal)) return;
25-
26-
this.props.closeModal();
27-
}
28-
29-
render() {
30-
return (
31-
<section
32-
className="modal"
33-
ref={(element) => {
34-
this.newFolderModal = element;
35-
}}
36-
>
37-
<div className="modal-content-folder">
38-
<div className="modal__header">
39-
<h2 className="modal__title">
40-
{this.props.t('NewFolderModal.Title')}
41-
</h2>
42-
<button
43-
className="modal__exit-button"
44-
onClick={this.props.closeModal}
45-
aria-label={this.props.t('NewFolderModal.CloseButtonARIA')}
46-
>
47-
<ExitIcon focusable="false" aria-hidden="true" />
48-
</button>
49-
</div>
50-
<NewFolderForm />
51-
</div>
52-
</section>
53-
);
54-
}
55-
}
56-
57-
NewFolderModal.propTypes = {
58-
closeModal: PropTypes.func.isRequired,
59-
t: PropTypes.func.isRequired
8+
const NewFolderModal = () => {
9+
const { t } = useTranslation();
10+
const dispatch = useDispatch();
11+
return (
12+
<Modal
13+
title={t('NewFolderModal.Title')}
14+
closeAriaLabel={t('NewFolderModal.CloseButtonARIA')}
15+
onClose={() => dispatch(closeNewFolderModal())}
16+
contentClassName="modal-content-folder"
17+
>
18+
<NewFolderForm />
19+
</Modal>
20+
);
6021
};
6122

62-
export default withTranslation()(NewFolderModal);
23+
export default NewFolderModal;

Diff for: client/modules/IDE/components/UploadFileModal.jsx

+33-67
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,45 @@
11
import React from 'react';
2-
import PropTypes from 'prop-types';
3-
import { connect } from 'react-redux';
2+
import { useDispatch, useSelector } from 'react-redux';
43
import { Link } from 'react-router';
5-
import { withTranslation } from 'react-i18next';
4+
import { useTranslation } from 'react-i18next';
65
import prettyBytes from 'pretty-bytes';
76
import getConfig from '../../../utils/getConfig';
7+
import { closeUploadFileModal } from '../actions/ide';
88
import FileUploader from './FileUploader';
99
import { getreachedTotalSizeLimit } from '../selectors/users';
10-
import ExitIcon from '../../../images/exit.svg';
10+
import Modal from './Modal';
1111

1212
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
1313
const limitText = prettyBytes(limit);
1414

15-
class UploadFileModal extends React.Component {
16-
static propTypes = {
17-
reachedTotalSizeLimit: PropTypes.bool.isRequired,
18-
closeModal: PropTypes.func.isRequired,
19-
t: PropTypes.func.isRequired
20-
};
21-
22-
componentDidMount() {
23-
this.focusOnModal();
24-
}
25-
26-
focusOnModal = () => {
27-
this.modal.focus();
28-
};
29-
30-
render() {
31-
return (
32-
<section
33-
className="modal"
34-
ref={(element) => {
35-
this.modal = element;
36-
}}
37-
>
38-
<div className="modal-content">
39-
<div className="modal__header">
40-
<h2 className="modal__title">
41-
{this.props.t('UploadFileModal.Title')}
42-
</h2>
43-
<button
44-
className="modal__exit-button"
45-
onClick={this.props.closeModal}
46-
aria-label={this.props.t('UploadFileModal.CloseButtonARIA')}
47-
>
48-
<ExitIcon focusable="false" aria-hidden="true" />
49-
</button>
50-
</div>
51-
{this.props.reachedTotalSizeLimit && (
52-
<p>
53-
{this.props.t('UploadFileModal.SizeLimitError', {
54-
sizeLimit: limitText
55-
})}
56-
<Link to="/assets" onClick={this.props.closeModal}>
57-
assets
58-
</Link>
59-
.
60-
</p>
61-
)}
62-
{!this.props.reachedTotalSizeLimit && (
63-
<div>
64-
<FileUploader />
65-
</div>
66-
)}
15+
const UploadFileModal = () => {
16+
const { t } = useTranslation();
17+
const dispatch = useDispatch();
18+
const reachedTotalSizeLimit = useSelector(getreachedTotalSizeLimit);
19+
const onClose = () => dispatch(closeUploadFileModal());
20+
return (
21+
<Modal
22+
title={t('UploadFileModal.Title')}
23+
closeAriaLabel={t('UploadFileModal.CloseButtonARIA')}
24+
onClose={onClose}
25+
>
26+
{reachedTotalSizeLimit ? (
27+
<p>
28+
{t('UploadFileModal.SizeLimitError', {
29+
sizeLimit: limitText
30+
})}
31+
<Link to="/assets" onClick={onClose}>
32+
assets
33+
</Link>
34+
.
35+
</p>
36+
) : (
37+
<div>
38+
<FileUploader />
6739
</div>
68-
</section>
69-
);
70-
}
71-
}
72-
73-
function mapStateToProps(state) {
74-
return {
75-
reachedTotalSizeLimit: getreachedTotalSizeLimit(state)
76-
};
77-
}
40+
)}
41+
</Modal>
42+
);
43+
};
7844

79-
export default withTranslation()(connect(mapStateToProps)(UploadFileModal));
45+
export default UploadFileModal;

Diff for: client/modules/IDE/pages/IDEView.jsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -378,12 +378,8 @@ class IDEView extends React.Component {
378378
</SplitPane>
379379
</main>
380380
{this.props.ide.modalIsVisible && <NewFileModal />}
381-
{this.props.ide.newFolderModalVisible && (
382-
<NewFolderModal closeModal={this.props.closeNewFolderModal} />
383-
)}
384-
{this.props.ide.uploadFileModalVisible && (
385-
<UploadFileModal closeModal={this.props.closeUploadFileModal} />
386-
)}
381+
{this.props.ide.newFolderModalVisible && <NewFolderModal />}
382+
{this.props.ide.uploadFileModalVisible && <UploadFileModal />}
387383
{this.props.location.pathname === '/about' && (
388384
<Overlay
389385
title={this.props.t('About.Title')}

0 commit comments

Comments
 (0)