Popup component reimplemented

This commit is contained in:
NikolaBorislavovHristov 2019-08-09 16:44:37 +03:00
parent 3d2b044589
commit 10f5d31e44
4 changed files with 96 additions and 249 deletions

View file

@ -1,18 +0,0 @@
const React = require('react');
const PropTypes = require('prop-types');
const Label = React.forwardRef(({ children, onClick }, ref) => {
return React.cloneElement(React.Children.only(children), { ref, onClick });
});
Label.displayName = 'Popup.Label';
Label.propTypes = {
onClick: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
module.exports = Label;

View file

@ -1,16 +0,0 @@
const PropTypes = require('prop-types');
const Menu = ({ className, tabIndex, children }) => children;
Menu.displayName = 'Popup.Menu';
Menu.propTypes = {
className: PropTypes.string,
tabIndex: PropTypes.number,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
};
module.exports = Menu;

View file

@ -1,234 +1,118 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { Modal } = require('stremio-router');
const Label = require('./Label');
const Menu = require('./Menu');
const styles = require('./styles');
class Popup extends React.Component {
constructor(props) {
super(props);
const Popup = ({ open = false, renderLabel, renderMenu, onCloseRequest }) => {
const labelContainerRef = React.useRef(null);
const menuContainerRef = React.useRef(null);
const menuContentContainerRef = React.useRef(null);
React.useEffect(() => {
const windowOnClick = (event) => {
if ((labelContainerRef.current !== null && labelContainerRef.current.contains(event.target)) ||
(menuContainerRef.current !== null && menuContainerRef.current.contains(event.target))) {
return;
}
this.labelRef = React.createRef();
this.menuContainerRef = React.createRef();
this.menuScrollRef = React.createRef();
this.menuChildrenRef = React.createRef();
this.popupMutationObserver = this.createPopupMutationObserver();
this.state = {
open: false
onCloseRequest(event);
};
}
componentDidMount() {
window.addEventListener('blur', this.close);
window.addEventListener('resize', this.close);
window.addEventListener('keyup', this.onKeyUp);
}
componentWillUnmount() {
window.removeEventListener('blur', this.close);
window.removeEventListener('resize', this.close);
window.removeEventListener('keyup', this.onKeyUp);
this.popupMutationObserver.disconnect();
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.open !== this.state.open ||
nextProps.children !== this.props.children;
}
componentDidUpdate(prevProps, prevState) {
if (this.state.open !== prevState.open) {
this.updateMenuStyles();
if (this.state.open) {
this.popupMutationObserver.observe(document.documentElement, {
childList: true,
attributes: true,
characterData: true,
subtree: true
});
if (typeof this.props.onOpen === 'function') {
this.props.onOpen();
}
} else {
this.popupMutationObserver.disconnect();
if (typeof this.props.onClose === 'function') {
this.props.onClose();
}
}
if (open) {
window.addEventListener('click', windowOnClick, true);
window.addEventListener('scroll', onCloseRequest, true);
window.addEventListener('resize', onCloseRequest, true);
}
}
createPopupMutationObserver = () => {
let prevLabelRect = {};
let prevMenuChildrenRect = {};
return new MutationObserver(() => {
if (this.state.open) {
const labelRect = this.labelRef.current.getBoundingClientRect();
const menuChildrenRect = this.menuChildrenRef.current.getBoundingClientRect();
if (labelRect.x !== prevLabelRect.x ||
labelRect.y !== prevLabelRect.y ||
labelRect.width !== prevLabelRect.width ||
labelRect.height !== prevLabelRect.height ||
menuChildrenRect.x !== prevMenuChildrenRect.x ||
menuChildrenRect.y !== prevMenuChildrenRect.y ||
menuChildrenRect.width !== prevMenuChildrenRect.width ||
menuChildrenRect.height !== prevMenuChildrenRect.height) {
this.updateMenuStyles();
}
prevLabelRect = labelRect;
prevMenuChildrenRect = menuChildrenRect;
} else {
prevLabelRect = {};
prevMenuChildrenRect = {};
}
});
}
updateMenuStyles = () => {
if (!this.state.open) {
return () => {
window.removeEventListener('click', windowOnClick, true);
window.removeEventListener('scroll', onCloseRequest, true);
window.removeEventListener('resize', onCloseRequest, true);
};
}, [open, onCloseRequest]);
React.useEffect(() => {
if (!open) {
return;
}
this.menuContainerRef.current.removeAttribute('style');
this.menuScrollRef.current.removeAttribute('style');
const menuDirections = {};
const documentRect = document.documentElement.getBoundingClientRect();
const labelRect = this.labelRef.current.getBoundingClientRect();
const menuChildredRect = this.menuChildrenRef.current.getBoundingClientRect();
const labelRect = labelContainerRef.current.getBoundingClientRect();
const menuContentRect = menuContentContainerRef.current.getBoundingClientRect();
const labelPosition = {
left: labelRect.x - documentRect.x,
top: labelRect.y - documentRect.y,
right: (documentRect.width + documentRect.x) - (labelRect.x + labelRect.width),
bottom: (documentRect.height + documentRect.y) - (labelRect.y + labelRect.height)
left: labelRect.left - documentRect.left,
top: labelRect.top - documentRect.top,
right: (documentRect.width + documentRect.left) - (labelRect.left + labelRect.width),
bottom: (documentRect.height + documentRect.top) - (labelRect.top + labelRect.height)
};
const bottomMenuStyles = {
top: `${labelPosition.top + labelRect.height}px`,
maxHeight: `${labelPosition.bottom}px`
};
const topMenuStyles = {
bottom: `${labelPosition.bottom + labelRect.height}px`,
maxHeight: `${labelPosition.top}px`
};
const rightMenuStyles = {
left: `${labelPosition.left}px`,
maxWidth: `${labelPosition.right + labelRect.width}px`
};
const leftMenuStyles = {
right: `${labelPosition.right}px`,
maxWidth: `${labelPosition.left + labelRect.width}px`
};
if (menuChildredRect.height <= labelPosition.bottom) {
this.menuContainerRef.current.style.top = `${labelPosition.top + labelRect.height}px`;
this.menuScrollRef.current.style.maxHeight = `${labelPosition.bottom}px`;
menuDirections.bottom = true;
} else if (menuChildredRect.height <= labelPosition.top) {
this.menuContainerRef.current.style.bottom = `${labelPosition.bottom + labelRect.height}px`;
this.menuScrollRef.current.style.maxHeight = `${labelPosition.top}px`;
menuDirections.top = true;
if (menuContentRect.height <= labelPosition.bottom) {
menuContainerRef.current.style.top = bottomMenuStyles.top;
menuContainerRef.current.style.maxHeight = bottomMenuStyles.maxHeight;
} else if (menuContentRect.height <= labelPosition.top) {
menuContainerRef.current.style.bottom = topMenuStyles.bottom;
menuContainerRef.current.style.maxHeight = topMenuStyles.maxHeight;
} else if (labelPosition.bottom >= labelPosition.top) {
this.menuContainerRef.current.style.top = `${labelPosition.top + labelRect.height}px`;
this.menuScrollRef.current.style.maxHeight = `${labelPosition.bottom}px`;
menuDirections.bottom = true;
menuContainerRef.current.style.top = bottomMenuStyles.top;
menuContainerRef.current.style.maxHeight = bottomMenuStyles.maxHeight;
} else {
this.menuContainerRef.current.style.bottom = `${labelPosition.bottom + labelRect.height}px`;
this.menuScrollRef.current.style.maxHeight = `${labelPosition.top}px`;
menuDirections.top = true;
menuContainerRef.current.style.bottom = topMenuStyles.bottom;
menuContainerRef.current.style.maxHeight = topMenuStyles.maxHeight;
}
if (menuChildredRect.width <= (labelPosition.right + labelRect.width)) {
this.menuContainerRef.current.style.left = `${labelPosition.left}px`;
this.menuScrollRef.current.style.maxWidth = `${labelPosition.right + labelRect.width}px`;
menuDirections.right = true;
} else if (menuChildredRect.width <= (labelPosition.left + labelRect.width)) {
this.menuContainerRef.current.style.right = `${labelPosition.right}px`;
this.menuScrollRef.current.style.maxWidth = `${labelPosition.left + labelRect.width}px`;
menuDirections.left = true;
if (menuContentRect.width <= (labelPosition.right + labelRect.width)) {
menuContainerRef.current.style.left = rightMenuStyles.left;
menuContainerRef.current.style.maxWidth = rightMenuStyles.maxWidth;
} else if (menuContentRect.width <= (labelPosition.left + labelRect.width)) {
menuContainerRef.current.style.right = leftMenuStyles.right;
menuContainerRef.current.style.maxWidth = leftMenuStyles.maxWidth;
} else if (labelPosition.right > labelPosition.left) {
this.menuContainerRef.current.style.left = `${labelPosition.left}px`;
this.menuScrollRef.current.style.maxWidth = `${labelPosition.right + labelRect.width}px`;
menuDirections.right = true;
menuContainerRef.current.style.left = rightMenuStyles.left;
menuContainerRef.current.style.maxWidth = rightMenuStyles.maxWidth;
} else {
this.menuContainerRef.current.style.right = `${labelPosition.right}px`;
this.menuScrollRef.current.style.maxWidth = `${labelPosition.left + labelRect.width}px`;
menuDirections.left = true;
menuContainerRef.current.style.right = leftMenuStyles.right;
menuContainerRef.current.style.maxWidth = leftMenuStyles.maxWidth;
}
this.menuContainerRef.current.style.visibility = 'visible';
}
onKeyUp = (event) => {
if (this.state.open && event.keyCode === 27) {
this.close();
}
}
open = () => {
this.setState({ open: true });
}
close = () => {
this.setState({ open: false });
}
menuContainerOnClick = (event) => {
event.nativeEvent.closePrevented = true;
}
modalBackgroundOnClick = (event) => {
if (!event.nativeEvent.closePrevented) {
this.close();
}
}
renderLabel(labelElement) {
return React.cloneElement(labelElement, { ref: this.labelRef, onClick: this.open });
}
renderMenu(menuElement) {
if (!this.state.open) {
return React.cloneElement(menuElement, {}, null);
}
return React.cloneElement(menuElement, {},
<Modal>
<div className={classnames(styles['popup-modal-layer'], menuElement.props.className)} onClick={this.modalBackgroundOnClick}>
<div ref={this.menuContainerRef} className={styles['menu-container']} onClick={this.menuContainerOnClick}>
<div ref={this.menuScrollRef} className={styles['menu-scroll-container']} tabIndex={menuElement.props.tabIndex}>
<div ref={this.menuChildrenRef} className={styles['menu-scroll-content']}>
{menuElement.props.children}
</div>
menuContainerRef.current.style.visibility = 'visible';
}, [open]);
return (
<React.Fragment>
{renderLabel({ ref: labelContainerRef })}
{
open ?
<Modal className={styles['popup-modal-container']}>
<div ref={menuContainerRef} className={styles['menu-container']}>
{renderMenu({
ref: menuContentContainerRef,
className: styles['menu-content-container']
})}
</div>
</div>
</div>
</Modal>
);
}
render() {
if (React.Children.count(this.props.children) !== 2) {
console.warn(new Error('Popup children should be one Popup.Label and one Popup.Menu'));
return null;
}
if (!React.isValidElement(this.props.children[0]) || this.props.children[0].type !== Label) {
console.warn(new Error('First Popup child should be of type Popup.Label'));
return null;
}
if (!React.isValidElement(this.props.children[1]) || this.props.children[1].type !== Menu) {
console.warn(new Error('Second Popup child should be of type Popup.Menu'));
return null;
}
return (
<React.Fragment>
{this.renderLabel(this.props.children[0])}
{this.renderMenu(this.props.children[1])}
</React.Fragment>
);
}
</Modal>
:
null
}
</React.Fragment>
);
}
Popup.Label = Label;
Popup.Menu = Menu;
Popup.propTypes = {
onOpen: PropTypes.func,
onClose: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
open: PropTypes.bool,
renderLabel: PropTypes.func.isRequired,
renderMenu: PropTypes.func.isRequired,
onCloseRequest: PropTypes.func.isRequired
};
module.exports = Popup;

View file

@ -1,19 +1,16 @@
.popup-modal-layer {
width: 100%;
height: 100%;
.popup-modal-container {
pointer-events: none;
.menu-container {
position: absolute;
pointer-events: auto;
visibility: hidden;
overflow: visible;
overflow: auto;
box-shadow: 0 1.35rem 2.7rem var(--color-backgrounddarker40),
0 1.1rem 0.85rem var(--color-backgrounddarker20);
.menu-scroll-container {
box-shadow: var(--box-shadow);
overflow: auto;
.menu-scroll-content {
overflow: visible;
}
.menu-content-container {
overflow: visible;
}
}
}