Skip to content

Commit

Permalink
Merge pull request processing#2309 from lindapaiste/refactor/overlay
Browse files Browse the repository at this point in the history
Convert `Overlay` to a function component, shares logic with `Modal`
  • Loading branch information
raclim authored Nov 29, 2023
2 parents 8569845 + 6dbc012 commit f725abd
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 150 deletions.
File renamed without changes.
45 changes: 45 additions & 0 deletions client/common/useModalClose.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect, useRef } from 'react';
import useKeyDownHandlers from './useKeyDownHandlers';

/**
* Common logic for Modal, Overlay, etc.
*
* Pass in the `onClose` handler.
*
* Can optionally pass in a ref, in case the `onClose` function needs to use the ref.
*
* Calls the provided `onClose` function on:
* - Press Escape key.
* - Click outside the element.
*
* Returns a ref to attach to the outermost element of the modal.
*
* @param {() => void} onClose
* @param {React.MutableRefObject<HTMLElement | null>} [passedRef]
* @return {React.MutableRefObject<HTMLElement | null>}
*/
export default function useModalClose(onClose, passedRef) {
const createdRef = useRef(null);
const modalRef = passedRef || createdRef;

useEffect(() => {
modalRef.current?.focus();

function handleClick(e) {
// ignore clicks on the component itself
if (modalRef.current && !modalRef.current.contains(e.target)) {
onClose?.();
}
}

document.addEventListener('click', handleClick, false);

return () => {
document.removeEventListener('click', handleClick, false);
};
}, [onClose, modalRef]);

useKeyDownHandlers({ escape: onClose });

return modalRef;
}
34 changes: 6 additions & 28 deletions client/components/Nav/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,18 @@
import PropTypes from 'prop-types';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import useKeyDownHandlers from '../../modules/IDE/hooks/useKeyDownHandlers';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import useModalClose from '../../common/useModalClose';
import { MenuOpenContext, NavBarContext } from './contexts';

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

const timerRef = useRef(null);

const nodeRef = useRef(null);
const handleClose = useCallback(() => {
setDropdownOpen('none');
}, [setDropdownOpen]);

useEffect(() => {
function handleClick(e) {
if (!nodeRef.current) {
return;
}
if (nodeRef.current.contains(e.target)) {
return;
}
setDropdownOpen('none');
}
document.addEventListener('mousedown', handleClick, false);
return () => {
document.removeEventListener('mousedown', handleClick, false);
};
}, [nodeRef, setDropdownOpen]);

useKeyDownHandlers({
escape: () => setDropdownOpen('none')
});
const nodeRef = useModalClose(handleClose);

const clearHideTimeout = useCallback(() => {
if (timerRef.current) {
Expand Down
136 changes: 58 additions & 78 deletions client/modules/App/components/Overlay.jsx
Original file line number Diff line number Diff line change
@@ -1,102 +1,83 @@
import PropTypes from 'prop-types';
import React from 'react';
import { withTranslation } from 'react-i18next';
import React, { useCallback, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import useModalClose from '../../../common/useModalClose';

import browserHistory from '../../../browserHistory';
import ExitIcon from '../../../images/exit.svg';
import { DocumentKeyDown } from '../../IDE/hooks/useKeyDownHandlers';

class Overlay extends React.Component {
constructor(props) {
super(props);
this.close = this.close.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
const Overlay = ({
actions,
ariaLabel,
children,
closeOverlay,
isFixedHeight,
title
}) => {
const { t } = useTranslation();

componentWillMount() {
document.addEventListener('mousedown', this.handleClick, false);
}
const previousPath = useSelector((state) => state.ide.previousPath);

componentDidMount() {
this.node.focus();
}
const ref = useRef(null);

componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClick, false);
}
const browserHistory = useHistory();

handleClick(e) {
if (this.node.contains(e.target)) {
return;
}

this.handleClickOutside(e);
}

handleClickOutside() {
this.close();
}

close() {
const close = useCallback(() => {
const node = ref.current;
if (!node) return;
// Only close if it is the last (and therefore the topmost overlay)
const overlays = document.getElementsByClassName('overlay');
if (this.node.parentElement.parentElement !== overlays[overlays.length - 1])
if (node.parentElement.parentElement !== overlays[overlays.length - 1])
return;

if (!this.props.closeOverlay) {
browserHistory.push(this.props.previousPath);
if (!closeOverlay) {
browserHistory.push(previousPath);
} else {
this.props.closeOverlay();
closeOverlay();
}
}
}, [previousPath, closeOverlay, ref]);

useModalClose(close, ref);

render() {
const { ariaLabel, title, children, actions, isFixedHeight } = this.props;
return (
<div
className={`overlay ${isFixedHeight ? 'overlay--is-fixed-height' : ''}`}
>
<div className="overlay__content">
<section
role="main"
aria-label={ariaLabel}
ref={(node) => {
this.node = node;
}}
className="overlay__body"
>
<header className="overlay__header">
<h2 className="overlay__title">{title}</h2>
<div className="overlay__actions">
{actions}
<button
className="overlay__close-button"
onClick={this.close}
aria-label={this.props.t('Overlay.AriaLabel', { title })}
>
<ExitIcon focusable="false" aria-hidden="true" />
</button>
</div>
</header>
{children}
<DocumentKeyDown handlers={{ escape: () => this.close() }} />
</section>
</div>
return (
<div
className={`overlay ${isFixedHeight ? 'overlay--is-fixed-height' : ''}`}
>
<div className="overlay__content">
<section
role="main"
aria-label={ariaLabel}
ref={ref}
className="overlay__body"
>
<header className="overlay__header">
<h2 className="overlay__title">{title}</h2>
<div className="overlay__actions">
{actions}
<button
className="overlay__close-button"
onClick={close}
aria-label={t('Overlay.AriaLabel', { title })}
>
<ExitIcon focusable="false" aria-hidden="true" />
</button>
</div>
</header>
{children}
</section>
</div>
);
}
}
</div>
);
};

Overlay.propTypes = {
children: PropTypes.element,
actions: PropTypes.element,
closeOverlay: PropTypes.func,
title: PropTypes.string,
ariaLabel: PropTypes.string,
previousPath: PropTypes.string,
isFixedHeight: PropTypes.bool,
t: PropTypes.func.isRequired
isFixedHeight: PropTypes.bool
};

Overlay.defaultProps = {
Expand All @@ -105,8 +86,7 @@ Overlay.defaultProps = {
title: 'Modal',
closeOverlay: null,
ariaLabel: 'modal',
previousPath: '/',
isFixedHeight: false
};

export default withTranslation()(Overlay);
export default Overlay;
2 changes: 1 addition & 1 deletion client/modules/IDE/components/IDEKeyHandlers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '../actions/ide';
import { setAllAccessibleOutput } from '../actions/preferences';
import { cloneProject, saveProject } from '../actions/project';
import useKeyDownHandlers from '../hooks/useKeyDownHandlers';
import useKeyDownHandlers from '../../../common/useKeyDownHandlers';
import {
getAuthenticated,
getIsUserOwner,
Expand Down
24 changes: 3 additions & 21 deletions client/modules/IDE/components/Modal.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import React from 'react';
import useModalClose from '../../../common/useModalClose';
import ExitIcon from '../../../images/exit.svg';
import useKeyDownHandlers from '../hooks/useKeyDownHandlers';

// Common logic from NewFolderModal, NewFileModal, UploadFileModal

Expand All @@ -13,25 +13,7 @@ const Modal = ({
contentClassName,
children
}) => {
const modalRef = useRef(null);

const handleOutsideClick = (e) => {
// ignore clicks on the component itself
if (modalRef.current?.contains?.(e.target)) return;

onClose();
};

useEffect(() => {
modalRef.current?.focus();
document.addEventListener('click', handleOutsideClick, false);

return () => {
document.removeEventListener('click', handleOutsideClick, false);
};
}, []);

useKeyDownHandlers({ escape: onClose });
const modalRef = useModalClose(onClose);

return (
<section className="modal" ref={modalRef}>
Expand Down
27 changes: 5 additions & 22 deletions client/modules/User/components/CollectionShareButton.jsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,20 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';

import Button from '../../../common/Button';
import { DropdownArrowIcon } from '../../../common/icons';
import useModalClose from '../../../common/useModalClose';
import CopyableInput from '../../IDE/components/CopyableInput';

const ShareURL = ({ value }) => {
const [showURL, setShowURL] = useState(false);
const node = useRef();
const { t } = useTranslation();

const handleClickOutside = (e) => {
if (node.current?.contains(e.target)) {
return;
}
setShowURL(false);
};

useEffect(() => {
if (showURL) {
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
}

return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showURL]);
const close = useCallback(() => setShowURL(false), [setShowURL]);
const ref = useModalClose(close);

return (
<div className="collection-share" ref={node}>
<div className="collection-share" ref={ref}>
<Button
onClick={() => setShowURL(!showURL)}
iconAfter={<DropdownArrowIcon />}
Expand Down

0 comments on commit f725abd

Please sign in to comment.