Popup/Multiselect api changed

This commit is contained in:
NikolaBorislavovHristov 2019-10-06 11:48:04 +03:00
parent acd75db430
commit f20d04d5ee
8 changed files with 197 additions and 241 deletions

View file

@ -1,3 +0,0 @@
const Dropdown = require('./Dropdown');
module.exports = Dropdown;

View file

@ -1,74 +0,0 @@
.dropdown-label-container {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 1rem;
background-color: var(--color-backgroundlighter);
&:hover, &:focus {
filter: brightness(1.2);
}
&:global(.active) {
background-color: var(--color-surfacelight);
.label {
color: var(--color-backgrounddarker);
}
.icon {
fill: var(--color-backgrounddarker);
}
}
.label {
flex: 1;
max-height: 2.4em;
color: var(--color-surfacelighter);
}
.icon {
flex: none;
width: 1rem;
height: 1rem;
margin-left: 1rem;
fill: var(--color-surfacelighter);
}
}
.dropdown-menu-container {
.dropdown-option-container {
display: flex;
flex-direction: row;
align-items: center;
padding: 1rem;
background-color: var(--color-backgroundlighter);
&:global(.selected) {
background-color: var(--color-surfacedarker);
.icon {
display: block;
}
}
&:hover, &:focus {
background-color: var(--color-surfacedark);
}
.label {
flex: 1;
max-height: 4.8em;
color: var(--color-surfacelighter);
}
.icon {
flex: none;
display: none;
width: 1rem;
height: 1rem;
margin-left: 1rem;
fill: var(--color-surfacelighter);
}
}
}

View file

@ -7,9 +7,24 @@ const Popup = require('stremio/common/Popup');
const useBinaryState = require('stremio/common/useBinaryState'); const useBinaryState = require('stremio/common/useBinaryState');
const styles = require('./styles'); const styles = require('./styles');
// TODO rename to multiselect const Multiselect = ({ className, direction, title, renderLabelContent, options, selected, onOpen, onClose, onSelect, ...props }) => {
const Dropdown = ({ className, menuClassName, menuMatchLabelWidth, renderLabel, name, selected, options, tabIndex, onOpen, onClose, onSelect }) => { options = Array.isArray(options) ?
options.filter(option => option && typeof option.value === 'string')
:
[];
selected = Array.isArray(selected) ?
selected.filter(value => typeof value === 'string')
:
[];
const [menuOpen, openMenu, closeMenu, toggleMenu] = useBinaryState(false); const [menuOpen, openMenu, closeMenu, toggleMenu] = useBinaryState(false);
const popupLabelOnClick = React.useCallback((event) => {
if (!event.nativeEvent.togglePopupPrevented) {
toggleMenu();
}
}, [toggleMenu]);
const popupMenuOnClick = React.useCallback((event) => {
event.nativeEvent.togglePopupPrevented = true;
}, []);
const optionOnClick = React.useCallback((event) => { const optionOnClick = React.useCallback((event) => {
if (typeof onSelect === 'function') { if (typeof onSelect === 'function') {
onSelect(event); onSelect(event);
@ -29,72 +44,66 @@ const Dropdown = ({ className, menuClassName, menuMatchLabelWidth, renderLabel,
onClose(); onClose();
} }
} }
}, [menuOpen, onOpen, onClose]); }, [menuOpen]);
return ( return (
<Popup <Popup
open={menuOpen} open={menuOpen}
menuMatchLabelWidth={typeof menuMatchLabelWidth === 'boolean' ? menuMatchLabelWidth : true} direction={direction}
onCloseRequest={closeMenu} onCloseRequest={closeMenu}
renderLabel={(ref) => ( renderLabel={({ ref, className: popupLabelClassName, children }) => (
<Button ref={ref} className={classnames(className, styles['dropdown-label-container'], { 'active': menuOpen })} title={name} tabIndex={tabIndex} onClick={toggleMenu}> <Button {...props} ref={ref} className={classnames(className, popupLabelClassName, styles['label-container'], { 'active': menuOpen })} title={title} onClick={popupLabelOnClick}>
{ {
typeof renderLabel === 'function' ? typeof renderLabelContent === 'function' ?
renderLabel() renderLabelContent()
: :
<React.Fragment> <React.Fragment>
<div className={styles['label']}> <div className={styles['label']}>
{ {
Array.isArray(selected) && selected.length > 0 ? selected.length > 0 ?
options.reduce((labels, { label, value }) => { options.reduce((labels, { label, value }) => {
if (selected.includes(value)) { if (selected.includes(value)) {
labels.push(label); labels.push(typeof label === 'string' ? label : value);
} }
return labels; return labels;
}, []).join(', ') }, []).join(', ')
: :
name title
} }
</div> </div>
<Icon className={styles['icon']} icon={'ic_arrow_down'} /> <Icon className={styles['icon']} icon={'ic_arrow_down'} />
</React.Fragment> </React.Fragment>
} }
{children}
</Button> </Button>
)} )}
renderMenu={() => ( renderMenu={() => (
<div className={classnames(menuClassName, styles['dropdown-menu-container'])}> <div className={styles['menu-container']} onClick={popupMenuOnClick}>
{ {options.map(({ label, value }) => (
Array.isArray(options) && options.length > 0 ? <Button key={value} className={classnames(styles['option-container'], { 'selected': selected.includes(value) })} title={typeof label === 'string' ? label : value} data-value={value} onClick={optionOnClick}>
options.map(({ label, value }) => ( <div className={styles['label']}>{typeof label === 'string' ? label : value}</div>
<Button key={value} className={classnames(styles['dropdown-option-container'], { 'selected': Array.isArray(selected) && selected.includes(value) })} title={label} data-name={name} data-value={value} onClick={optionOnClick}> <Icon className={styles['icon']} icon={'ic_check'} />
<div className={styles['label']}>{label}</div> </Button>
<Icon className={styles['icon']} icon={'ic_check'} /> ))}
</Button>
))
:
null
}
</div> </div>
)} )}
/> />
); );
}; };
Dropdown.propTypes = { Multiselect.propTypes = {
className: PropTypes.string, className: PropTypes.string,
menuClassName: PropTypes.string, direction: PropTypes.any,
menuMatchLabelWidth: PropTypes.bool, title: PropTypes.string,
renderLabel: PropTypes.func, renderLabelContent: PropTypes.func,
name: PropTypes.string,
selected: PropTypes.arrayOf(PropTypes.string),
options: PropTypes.arrayOf(PropTypes.shape({ options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
value: PropTypes.string.isRequired label: PropTypes.string
})), })),
tabIndex: PropTypes.number, selected: PropTypes.arrayOf(PropTypes.string),
onOpen: PropTypes.func, onOpen: PropTypes.func,
onClose: PropTypes.func, onClose: PropTypes.func,
onSelect: PropTypes.func onSelect: PropTypes.func
}; };
module.exports = Dropdown; module.exports = Multiselect;

View file

@ -0,0 +1,3 @@
const Multiselect = require('./Multiselect');
module.exports = Multiselect;

View file

@ -0,0 +1,78 @@
:import('~stremio/common/Popup/styles.less') {
popup-menu-container: menu-container;
}
.label-container {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 1rem;
background-color: var(--color-backgroundlighter);
&:global(.active) {
background-color: var(--color-surfacelight);
.label {
color: var(--color-backgrounddarker);
}
.icon {
fill: var(--color-backgrounddarker);
}
}
.label {
flex: 1;
max-height: 2.4em;
margin-right: 1rem;
color: var(--color-surfacelighter);
}
.icon {
flex: none;
width: 1rem;
height: 1rem;
fill: var(--color-surfacelighter);
}
.popup-menu-container {
width: 100%;
.menu-container {
.option-container {
display: flex;
flex-direction: row;
align-items: center;
padding: 1rem;
background-color: var(--color-backgroundlighter);
&:global(.selected) {
background-color: var(--color-surfacedarker);
.icon {
display: block;
}
}
&:hover, &:focus {
background-color: var(--color-surfacedark);
}
.label {
flex: 1;
max-height: 4.8em;
margin-right: 1rem;
color: var(--color-surfacelighter);
}
.icon {
flex: none;
display: none;
width: 1rem;
height: 1rem;
fill: var(--color-surfacelighter);
}
}
}
}
}

View file

@ -1,147 +1,78 @@
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const classnames = require('classnames'); const classnames = require('classnames');
const { Modal } = require('stremio-router'); const FocusLock = require('react-focus-lock').default;
const styles = require('./styles'); const styles = require('./styles');
// TODO rename to Popover const Popup = ({ open, direction, renderLabel, renderMenu, onCloseRequest }) => {
const Popup = ({ open, menuModalClassName, menuRelativePosition, menuMatchLabelWidth, renderLabel, renderMenu, onCloseRequest }) => {
const labelRef = React.useRef(null); const labelRef = React.useRef(null);
const menuRef = React.useRef(null); const [autoDirection, setAutoDirection] = React.useState(null);
const [menuStyles, setMenuStyles] = React.useState({});
React.useEffect(() => {
const checkCloseEvent = (event) => {
switch (event.type) {
case 'resize':
onCloseRequest(event);
break;
case 'keydown':
if (event.key === 'Escape') {
onCloseRequest(event);
}
break;
case 'mousedown':
if (event.target !== document &&
event.target !== document.documentElement &&
!event.closePopupPrevented) {
onCloseRequest(event);
}
break;
case 'react-scroll':
if (!event.nativeEvent.closePopupPrevented) {
onCloseRequest(event.nativeEvent);
}
break;
}
};
if (open) {
window.addEventListener('react-scroll', checkCloseEvent);
window.addEventListener('mousedown', checkCloseEvent);
window.addEventListener('keydown', checkCloseEvent);
window.addEventListener('resize', checkCloseEvent);
}
return () => {
window.removeEventListener('react-scroll', checkCloseEvent);
window.removeEventListener('mousedown', checkCloseEvent);
window.removeEventListener('keydown', checkCloseEvent);
window.removeEventListener('resize', checkCloseEvent);
};
}, [open, onCloseRequest]);
const menuOnMouseDown = React.useCallback((event) => { const menuOnMouseDown = React.useCallback((event) => {
event.nativeEvent.closePopupPrevented = true; event.nativeEvent.closePopupPrevented = true;
}, []); }, []);
const menuOnScroll = React.useCallback((event) => {
event.nativeEvent.closePopupPrevented = true;
}, []);
React.useEffect(() => { React.useEffect(() => {
let menuStyles = {}; const checkCloseEvent = (event) => {
if (typeof onCloseRequest === 'function') {
switch (event.type) {
case 'resize':
onCloseRequest(event);
break;
case 'keydown':
if (event.key === 'Escape') {
onCloseRequest(event);
}
break;
case 'mousedown':
if (event.target !== document.documentElement &&
!labelRef.current.contains(event.target) &&
!event.closePopupPrevented) {
onCloseRequest(event);
}
break;
}
}
};
if (open) { if (open) {
if (menuRelativePosition !== false) { window.addEventListener('resize', checkCloseEvent);
const documentRect = document.documentElement.getBoundingClientRect(); window.addEventListener('keydown', checkCloseEvent);
const labelRect = labelRef.current.getBoundingClientRect(); window.addEventListener('mousedown', checkCloseEvent);
const menuRect = menuRef.current.getBoundingClientRect(); }
const labelPosition = { return () => {
left: labelRect.left - documentRect.left, window.removeEventListener('resize', checkCloseEvent);
top: labelRect.top - documentRect.top, window.removeEventListener('keydown', checkCloseEvent);
right: (documentRect.width + documentRect.left) - (labelRect.left + labelRect.width), window.removeEventListener('mousedown', checkCloseEvent);
bottom: (documentRect.height + documentRect.top) - (labelRect.top + labelRect.height) };
}; }, [open, onCloseRequest]);
const matchLabelWidthMenuStyles = { React.useLayoutEffect(() => {
width: `${labelRect.width}px`, if (open) {
maxWidth: `${labelRect.width}px` const documentRect = document.documentElement.getBoundingClientRect();
}; const labelRect = labelRef.current.getBoundingClientRect();
const bottomMenuStyles = { const labelOffsetTop = labelRect.top - documentRect.top;
top: `${labelPosition.top + labelRect.height}px`, const labelOffsetBottom = (documentRect.height + documentRect.top) - (labelRect.top + labelRect.height);
maxHeight: `${labelPosition.bottom}px` const autoDirection = labelOffsetBottom >= labelOffsetTop ? 'bottom' : 'top';
}; setAutoDirection(autoDirection);
const topMenuStyles = { } else {
bottom: `${labelPosition.bottom + labelRect.height}px`, setAutoDirection(null);
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 (menuRect.height <= labelPosition.bottom) {
menuStyles = { ...menuStyles, ...bottomMenuStyles };
} else if (menuRect.height <= labelPosition.top) {
menuStyles = { ...menuStyles, ...topMenuStyles };
} else if (labelPosition.bottom >= labelPosition.top) {
menuStyles = { ...menuStyles, ...bottomMenuStyles };
} else {
menuStyles = { ...menuStyles, ...topMenuStyles };
}
if (menuRect.width <= (labelPosition.right + labelRect.width)) {
menuStyles = { ...menuStyles, ...rightMenuStyles };
} else if (menuRect.width <= (labelPosition.left + labelRect.width)) {
menuStyles = { ...menuStyles, ...leftMenuStyles };
} else if (labelPosition.right > labelPosition.left) {
menuStyles = { ...menuStyles, ...rightMenuStyles };
} else {
menuStyles = { ...menuStyles, ...leftMenuStyles };
}
if (menuMatchLabelWidth) {
menuStyles = { ...menuStyles, ...matchLabelWidthMenuStyles };
}
}
menuStyles = { ...menuStyles, visibility: 'visible' };
} }
setMenuStyles(menuStyles);
}, [open]); }, [open]);
return ( return renderLabel({
<React.Fragment> ref: labelRef,
{renderLabel(labelRef)} className: styles['label-container'],
{ children: open ?
open ? <FocusLock className={classnames(styles['menu-container'], styles[`menu-direction-${typeof direction === 'string' ? direction : autoDirection}`])} autoFocus={false} lockProps={{ onMouseDown: menuOnMouseDown }}>
<Modal className={classnames(styles['menu-modal-container'], menuModalClassName)}> {renderMenu()}
<div ref={menuRef} style={menuStyles} className={styles['menu-container']} onMouseDown={menuOnMouseDown} onScroll={menuOnScroll}> </FocusLock>
{renderMenu()} :
</div> null
</Modal> });
:
null
}
</React.Fragment>
);
} }
Popup.propTypes = { Popup.propTypes = {
open: PropTypes.bool, open: PropTypes.bool,
menuModalClassName: PropTypes.string, direction: PropTypes.oneOf(['top', 'bottom']),
menuRelativePosition: PropTypes.bool,
menuMatchLabelWidth: PropTypes.bool,
renderLabel: PropTypes.func.isRequired, renderLabel: PropTypes.func.isRequired,
renderMenu: PropTypes.func.isRequired, renderMenu: PropTypes.func.isRequired,
onCloseRequest: PropTypes.func.isRequired onCloseRequest: PropTypes.func
}; };
module.exports = Popup; module.exports = Popup;

View file

@ -1,12 +1,24 @@
.menu-modal-container { .label-container {
pointer-events: none; position: relative;
overflow: visible;
.menu-container { .menu-container {
position: absolute; position: absolute;
pointer-events: auto; right: 0;
z-index: 1;
overflow: visible;
visibility: hidden; visibility: hidden;
overflow: auto;
box-shadow: 0 1.35rem 2.7rem var(--color-backgrounddarker40), box-shadow: 0 1.35rem 2.7rem var(--color-backgrounddarker40),
0 1.1rem 0.85rem var(--color-backgrounddarker20); 0 1.1rem 0.85rem var(--color-backgrounddarker20);
&.menu-direction-bottom {
top: 100%;
visibility: visible;
}
&.menu-direction-top {
bottom: 100%;
visibility: visible;
}
} }
} }

View file

@ -1,7 +1,7 @@
const Button = require('./Button'); const Button = require('./Button');
const Checkbox = require('./Checkbox'); const Checkbox = require('./Checkbox');
const ColorInput = require('./ColorInput'); const ColorInput = require('./ColorInput');
const Dropdown = require('./Dropdown'); const Multiselect = require('./Multiselect');
const Image = require('./Image'); const Image = require('./Image');
const MainNavBar = require('./MainNavBar'); const MainNavBar = require('./MainNavBar');
const MetaItem = require('./MetaItem'); const MetaItem = require('./MetaItem');
@ -29,7 +29,7 @@ module.exports = {
Button, Button,
Checkbox, Checkbox,
ColorInput, ColorInput,
Dropdown, Multiselect,
Image, Image,
MainNavBar, MainNavBar,
MetaItem, MetaItem,