mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-18 21:12:13 +00:00
Popup component reimplemented
This commit is contained in:
parent
3d2b044589
commit
10f5d31e44
4 changed files with 96 additions and 249 deletions
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue