Skip to content

Commit f725abd

Browse files
authored
Merge pull request #2309 from lindapaiste/refactor/overlay
Convert `Overlay` to a function component, shares logic with `Modal`
2 parents 8569845 + 6dbc012 commit f725abd

File tree

7 files changed

+118
-150
lines changed

7 files changed

+118
-150
lines changed

client/common/useModalClose.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useEffect, useRef } from 'react';
2+
import useKeyDownHandlers from './useKeyDownHandlers';
3+
4+
/**
5+
* Common logic for Modal, Overlay, etc.
6+
*
7+
* Pass in the `onClose` handler.
8+
*
9+
* Can optionally pass in a ref, in case the `onClose` function needs to use the ref.
10+
*
11+
* Calls the provided `onClose` function on:
12+
* - Press Escape key.
13+
* - Click outside the element.
14+
*
15+
* Returns a ref to attach to the outermost element of the modal.
16+
*
17+
* @param {() => void} onClose
18+
* @param {React.MutableRefObject<HTMLElement | null>} [passedRef]
19+
* @return {React.MutableRefObject<HTMLElement | null>}
20+
*/
21+
export default function useModalClose(onClose, passedRef) {
22+
const createdRef = useRef(null);
23+
const modalRef = passedRef || createdRef;
24+
25+
useEffect(() => {
26+
modalRef.current?.focus();
27+
28+
function handleClick(e) {
29+
// ignore clicks on the component itself
30+
if (modalRef.current && !modalRef.current.contains(e.target)) {
31+
onClose?.();
32+
}
33+
}
34+
35+
document.addEventListener('click', handleClick, false);
36+
37+
return () => {
38+
document.removeEventListener('click', handleClick, false);
39+
};
40+
}, [onClose, modalRef]);
41+
42+
useKeyDownHandlers({ escape: onClose });
43+
44+
return modalRef;
45+
}

client/components/Nav/NavBar.jsx

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,18 @@
11
import PropTypes from 'prop-types';
2-
import React, {
3-
useCallback,
4-
useEffect,
5-
useMemo,
6-
useRef,
7-
useState
8-
} from 'react';
9-
import useKeyDownHandlers from '../../modules/IDE/hooks/useKeyDownHandlers';
2+
import React, { useCallback, useMemo, useRef, useState } from 'react';
3+
import useModalClose from '../../common/useModalClose';
104
import { MenuOpenContext, NavBarContext } from './contexts';
115

126
function NavBar({ children, className }) {
137
const [dropdownOpen, setDropdownOpen] = useState('none');
148

159
const timerRef = useRef(null);
1610

17-
const nodeRef = useRef(null);
11+
const handleClose = useCallback(() => {
12+
setDropdownOpen('none');
13+
}, [setDropdownOpen]);
1814

19-
useEffect(() => {
20-
function handleClick(e) {
21-
if (!nodeRef.current) {
22-
return;
23-
}
24-
if (nodeRef.current.contains(e.target)) {
25-
return;
26-
}
27-
setDropdownOpen('none');
28-
}
29-
document.addEventListener('mousedown', handleClick, false);
30-
return () => {
31-
document.removeEventListener('mousedown', handleClick, false);
32-
};
33-
}, [nodeRef, setDropdownOpen]);
34-
35-
useKeyDownHandlers({
36-
escape: () => setDropdownOpen('none')
37-
});
15+
const nodeRef = useModalClose(handleClose);
3816

3917
const clearHideTimeout = useCallback(() => {
4018
if (timerRef.current) {
Lines changed: 58 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,83 @@
11
import PropTypes from 'prop-types';
2-
import React from 'react';
3-
import { withTranslation } from 'react-i18next';
2+
import React, { useCallback, useRef } from 'react';
3+
import { useSelector } from 'react-redux';
4+
import { useHistory } from 'react-router-dom';
5+
import { useTranslation } from 'react-i18next';
6+
import useModalClose from '../../../common/useModalClose';
47

5-
import browserHistory from '../../../browserHistory';
68
import ExitIcon from '../../../images/exit.svg';
7-
import { DocumentKeyDown } from '../../IDE/hooks/useKeyDownHandlers';
89

9-
class Overlay extends React.Component {
10-
constructor(props) {
11-
super(props);
12-
this.close = this.close.bind(this);
13-
this.handleClick = this.handleClick.bind(this);
14-
this.handleClickOutside = this.handleClickOutside.bind(this);
15-
}
10+
const Overlay = ({
11+
actions,
12+
ariaLabel,
13+
children,
14+
closeOverlay,
15+
isFixedHeight,
16+
title
17+
}) => {
18+
const { t } = useTranslation();
1619

17-
componentWillMount() {
18-
document.addEventListener('mousedown', this.handleClick, false);
19-
}
20+
const previousPath = useSelector((state) => state.ide.previousPath);
2021

21-
componentDidMount() {
22-
this.node.focus();
23-
}
22+
const ref = useRef(null);
2423

25-
componentWillUnmount() {
26-
document.removeEventListener('mousedown', this.handleClick, false);
27-
}
24+
const browserHistory = useHistory();
2825

29-
handleClick(e) {
30-
if (this.node.contains(e.target)) {
31-
return;
32-
}
33-
34-
this.handleClickOutside(e);
35-
}
36-
37-
handleClickOutside() {
38-
this.close();
39-
}
40-
41-
close() {
26+
const close = useCallback(() => {
27+
const node = ref.current;
28+
if (!node) return;
4229
// Only close if it is the last (and therefore the topmost overlay)
4330
const overlays = document.getElementsByClassName('overlay');
44-
if (this.node.parentElement.parentElement !== overlays[overlays.length - 1])
31+
if (node.parentElement.parentElement !== overlays[overlays.length - 1])
4532
return;
4633

47-
if (!this.props.closeOverlay) {
48-
browserHistory.push(this.props.previousPath);
34+
if (!closeOverlay) {
35+
browserHistory.push(previousPath);
4936
} else {
50-
this.props.closeOverlay();
37+
closeOverlay();
5138
}
52-
}
39+
}, [previousPath, closeOverlay, ref]);
40+
41+
useModalClose(close, ref);
5342

54-
render() {
55-
const { ariaLabel, title, children, actions, isFixedHeight } = this.props;
56-
return (
57-
<div
58-
className={`overlay ${isFixedHeight ? 'overlay--is-fixed-height' : ''}`}
59-
>
60-
<div className="overlay__content">
61-
<section
62-
role="main"
63-
aria-label={ariaLabel}
64-
ref={(node) => {
65-
this.node = node;
66-
}}
67-
className="overlay__body"
68-
>
69-
<header className="overlay__header">
70-
<h2 className="overlay__title">{title}</h2>
71-
<div className="overlay__actions">
72-
{actions}
73-
<button
74-
className="overlay__close-button"
75-
onClick={this.close}
76-
aria-label={this.props.t('Overlay.AriaLabel', { title })}
77-
>
78-
<ExitIcon focusable="false" aria-hidden="true" />
79-
</button>
80-
</div>
81-
</header>
82-
{children}
83-
<DocumentKeyDown handlers={{ escape: () => this.close() }} />
84-
</section>
85-
</div>
43+
return (
44+
<div
45+
className={`overlay ${isFixedHeight ? 'overlay--is-fixed-height' : ''}`}
46+
>
47+
<div className="overlay__content">
48+
<section
49+
role="main"
50+
aria-label={ariaLabel}
51+
ref={ref}
52+
className="overlay__body"
53+
>
54+
<header className="overlay__header">
55+
<h2 className="overlay__title">{title}</h2>
56+
<div className="overlay__actions">
57+
{actions}
58+
<button
59+
className="overlay__close-button"
60+
onClick={close}
61+
aria-label={t('Overlay.AriaLabel', { title })}
62+
>
63+
<ExitIcon focusable="false" aria-hidden="true" />
64+
</button>
65+
</div>
66+
</header>
67+
{children}
68+
</section>
8669
</div>
87-
);
88-
}
89-
}
70+
</div>
71+
);
72+
};
9073

9174
Overlay.propTypes = {
9275
children: PropTypes.element,
9376
actions: PropTypes.element,
9477
closeOverlay: PropTypes.func,
9578
title: PropTypes.string,
9679
ariaLabel: PropTypes.string,
97-
previousPath: PropTypes.string,
98-
isFixedHeight: PropTypes.bool,
99-
t: PropTypes.func.isRequired
80+
isFixedHeight: PropTypes.bool
10081
};
10182

10283
Overlay.defaultProps = {
@@ -105,8 +86,7 @@ Overlay.defaultProps = {
10586
title: 'Modal',
10687
closeOverlay: null,
10788
ariaLabel: 'modal',
108-
previousPath: '/',
10989
isFixedHeight: false
11090
};
11191

112-
export default withTranslation()(Overlay);
92+
export default Overlay;

client/modules/IDE/components/IDEKeyHandlers.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '../actions/ide';
1313
import { setAllAccessibleOutput } from '../actions/preferences';
1414
import { cloneProject, saveProject } from '../actions/project';
15-
import useKeyDownHandlers from '../hooks/useKeyDownHandlers';
15+
import useKeyDownHandlers from '../../../common/useKeyDownHandlers';
1616
import {
1717
getAuthenticated,
1818
getIsUserOwner,

client/modules/IDE/components/Modal.jsx

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import classNames from 'classnames';
22
import PropTypes from 'prop-types';
3-
import React, { useEffect, useRef } from 'react';
3+
import React from 'react';
4+
import useModalClose from '../../../common/useModalClose';
45
import ExitIcon from '../../../images/exit.svg';
5-
import useKeyDownHandlers from '../hooks/useKeyDownHandlers';
66

77
// Common logic from NewFolderModal, NewFileModal, UploadFileModal
88

@@ -13,25 +13,7 @@ const Modal = ({
1313
contentClassName,
1414
children
1515
}) => {
16-
const modalRef = useRef(null);
17-
18-
const handleOutsideClick = (e) => {
19-
// ignore clicks on the component itself
20-
if (modalRef.current?.contains?.(e.target)) return;
21-
22-
onClose();
23-
};
24-
25-
useEffect(() => {
26-
modalRef.current?.focus();
27-
document.addEventListener('click', handleOutsideClick, false);
28-
29-
return () => {
30-
document.removeEventListener('click', handleOutsideClick, false);
31-
};
32-
}, []);
33-
34-
useKeyDownHandlers({ escape: onClose });
16+
const modalRef = useModalClose(onClose);
3517

3618
return (
3719
<section className="modal" ref={modalRef}>

client/modules/User/components/CollectionShareButton.jsx

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,20 @@
11
import PropTypes from 'prop-types';
2-
import React, { useEffect, useRef, useState } from 'react';
2+
import React, { useCallback, useState } from 'react';
33
import { useTranslation } from 'react-i18next';
44

55
import Button from '../../../common/Button';
66
import { DropdownArrowIcon } from '../../../common/icons';
7+
import useModalClose from '../../../common/useModalClose';
78
import CopyableInput from '../../IDE/components/CopyableInput';
89

910
const ShareURL = ({ value }) => {
1011
const [showURL, setShowURL] = useState(false);
11-
const node = useRef();
1212
const { t } = useTranslation();
13-
14-
const handleClickOutside = (e) => {
15-
if (node.current?.contains(e.target)) {
16-
return;
17-
}
18-
setShowURL(false);
19-
};
20-
21-
useEffect(() => {
22-
if (showURL) {
23-
document.addEventListener('mousedown', handleClickOutside);
24-
} else {
25-
document.removeEventListener('mousedown', handleClickOutside);
26-
}
27-
28-
return () => {
29-
document.removeEventListener('mousedown', handleClickOutside);
30-
};
31-
}, [showURL]);
13+
const close = useCallback(() => setShowURL(false), [setShowURL]);
14+
const ref = useModalClose(close);
3215

3316
return (
34-
<div className="collection-share" ref={node}>
17+
<div className="collection-share" ref={ref}>
3518
<Button
3619
onClick={() => setShowURL(!showURL)}
3720
iconAfter={<DropdownArrowIcon />}

0 commit comments

Comments
 (0)