Merge branch 'discover-filters' into inputs-modal

This commit is contained in:
svetlagasheva 2020-02-14 11:38:52 +02:00
commit fc94063d8f
8 changed files with 188 additions and 136 deletions

View file

@ -1,15 +1,15 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useModalsContainer } = require('stremio/router');
const Button = require('stremio/common/Button');
const Icon = require('stremio-icons/dom');
const { Modal } = require('stremio-router');
const styles = require('./styles');
const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequest, ...props }) => {
const onModalDialogContainerMouseDown = React.useCallback((event) => {
event.nativeEvent.closeModalDialogPrevented = true;
}, []);
const modalContainerRef = React.useRef(null);
const modalsContainer = useModalsContainer();
const closeButtonOnClick = React.useCallback((event) => {
if (typeof onCloseRequest === 'function') {
onCloseRequest({
@ -20,42 +20,50 @@ const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequ
});
}
}, [dataset, onCloseRequest]);
const onModalContainerMouseDown = React.useCallback((event) => {
if (!event.nativeEvent.closeModalDialogPrevented && typeof onCloseRequest === 'function') {
onCloseRequest({
type: 'close',
dataset: dataset,
reactEvent: event,
nativeEvent: event.nativeEvent
});
}
}, [dataset, onCloseRequest]);
const onModalDialogContainerMouseDown = React.useCallback((event) => {
event.nativeEvent.closeModalDialogPrevented = true;
}, []);
React.useEffect(() => {
const onCloseEvent = (event) => {
if (!event.closeModalDialogPrevented && typeof onCloseRequest === 'function') {
const closeEvent = {
const onWindowResize = (event) => {
if (typeof onCloseRequest === 'function') {
onCloseRequest({
type: 'close',
dataset: dataset,
nativeEvent: event
};
switch (event.type) {
case 'resize':
onCloseRequest(closeEvent);
break;
case 'keydown':
if (event.key === 'Escape') {
onCloseRequest(closeEvent);
}
break;
case 'mousedown':
if (event.target !== document.documentElement) {
onCloseRequest(closeEvent);
}
break;
});
}
};
const onWindowKeyDown = (event) => {
// its `-2` because focus lock render locking divs around its content
if (modalsContainer.childNodes[modalsContainer.childElementCount - 2] === modalContainerRef.current) {
if (typeof onCloseRequest === 'function') {
onCloseRequest({
type: 'close',
dataset: dataset,
nativeEvent: event
});
}
}
};
window.addEventListener('resize', onCloseEvent);
window.addEventListener('keydown', onCloseEvent);
window.addEventListener('mousedown', onCloseEvent);
window.addEventListener('resize', onWindowResize);
window.addEventListener('keydown', onWindowKeyDown);
return () => {
window.removeEventListener('resize', onCloseEvent);
window.removeEventListener('keydown', onCloseEvent);
window.removeEventListener('mousedown', onCloseEvent);
window.removeEventListener('resize', onWindowResize);
window.removeEventListener('keydown', onWindowKeyDown);
};
}, [dataset, onCloseRequest]);
return (
<Modal {...props} className={classnames(className, styles['modal-container'])}>
<Modal ref={modalContainerRef} {...props} className={classnames(className, styles['modal-container'])} onMouseDown={onModalContainerMouseDown}>
<div className={styles['modal-dialog-container']} onMouseDown={onModalDialogContainerMouseDown}>
<div className={styles['header-container']}>
{

View file

@ -8,7 +8,7 @@ const ModalDialog = require('stremio/common/ModalDialog');
const useBinaryState = require('stremio/common/useBinaryState');
const styles = require('./styles');
const Multiselect = ({ className, direction, title, disabled, dataset, modalOptions, renderLabelContent, renderLabelText, onOpen, onClose, onSelect, ...props }) => {
const Multiselect = ({ className, mode, direction, title, disabled, dataset, renderLabelContent, renderLabelText, onOpen, onClose, onSelect, ...props }) => {
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
const options = React.useMemo(() => {
return Array.isArray(props.options) ?
@ -26,19 +26,19 @@ const Multiselect = ({ className, direction, title, disabled, dataset, modalOpti
:
[];
}, [props.selected]);
const popupLabelOnClick = React.useCallback((event) => {
const labelOnClick = React.useCallback((event) => {
if (typeof props.onClick === 'function') {
props.onClick(event);
}
if (!event.nativeEvent.togglePopupPrevented) {
if (!event.nativeEvent.toggleMenuPrevented) {
toggleMenu();
}
}, [props.onClick, toggleMenu]);
const popupMenuOnClick = React.useCallback((event) => {
event.nativeEvent.togglePopupPrevented = true;
const menuOnClick = React.useCallback((event) => {
event.nativeEvent.toggleMenuPrevented = true;
}, []);
const popupMenuOnKeyDown = React.useCallback((event) => {
const menuOnKeyDown = React.useCallback((event) => {
event.nativeEvent.buttonClickPrevented = true;
}, []);
const optionOnClick = React.useCallback((event) => {
@ -78,8 +78,8 @@ const Multiselect = ({ className, direction, title, disabled, dataset, modalOpti
mountedRef.current = true;
}, [menuOpen]);
const renderLabel = React.useCallback(({ children, className, ...props }) => (
<Button {...props} className={classnames(className, styles['label-container'], { 'active': menuOpen })} title={title} disabled={disabled} onClick={popupLabelOnClick}>
const renderLabel = React.useMemo(() => ({ children, className, ...props }) => (
<Button {...props} className={classnames(className, styles['label-container'], { 'active': menuOpen })} title={title} disabled={disabled} onClick={labelOnClick}>
{
typeof renderLabelContent === 'function' ?
renderLabelContent()
@ -107,67 +107,55 @@ const Multiselect = ({ className, direction, title, disabled, dataset, modalOpti
}
{children}
</Button>
));
const renderOptions = React.useCallback((options) => {
return (
options.length > 0 ?
options.map(({ label, value }) => (
<Button key={value} className={classnames(styles['option-container'], { 'selected': selected.includes(value) })} title={typeof label === 'string' ? label : value} data-value={value} onClick={optionOnClick}>
<div className={styles['label']}>{typeof label === 'string' ? label : value}</div>
<Icon className={styles['icon']} icon={'ic_check'} />
</Button>
))
:
<div className={styles['no-options-container']}>
<div className={styles['label']}>No options available</div>
</div>
);
}, [options]);
return (
<React.Fragment>
), [menuOpen, title, disabled, options, selected, labelOnClick, renderLabelContent, renderLabelText]);
const renderMenu = React.useMemo(() => () => (
<div className={styles['menu-container']} onKeyDown={menuOnKeyDown} onClick={menuOnClick}>
{
!modalOptions ?
<Popup
open={menuOpen}
direction={direction}
onCloseRequest={closeMenu}
renderLabel={(labelProps) => {
return renderLabel({
...labelProps,
...props,
className: classnames(className, labelProps.className)
});
}}
renderMenu={() => (
<div className={styles['menu-container']} onKeyDown={popupMenuOnKeyDown} onClick={popupMenuOnClick}>
{renderOptions(options)}
</div>
)}
/>
options.length > 0 ?
options.map(({ label, value }) => (
<Button key={value} className={classnames(styles['option-container'], { 'selected': selected.includes(value) })} title={typeof label === 'string' ? label : value} data-value={value} onClick={optionOnClick}>
<div className={styles['label']}>{typeof label === 'string' ? label : value}</div>
<Icon className={styles['icon']} icon={'ic_check'} />
</Button>
))
:
<React.Fragment>
{renderLabel({
...props,
className
})}
{
menuOpen ?
<ModalDialog title={title} onCloseRequest={closeMenu}>
<div className={classnames(styles['menu-container'], { 'modal': modalOptions })}>
{renderOptions(options)}
</div>
</ModalDialog>
:
null
}
</React.Fragment>
<div className={styles['no-options-container']}>
<div className={styles['label']}>No options available</div>
</div>
}
</React.Fragment>
);
</div>
), [options, selected, menuOnKeyDown, menuOnClick, optionOnClick]);
const renderPopupLabel = React.useMemo(() => (labelProps) => {
return renderLabel({
...labelProps,
...props,
className: classnames(className, labelProps.className)
});
}, [props, className, renderLabel]);
return mode === 'modal' ?
renderLabel({
...props,
className,
children: menuOpen ?
<ModalDialog className={styles['modal-container']} title={title} onCloseRequest={closeMenu} onKeyDown={menuOnKeyDown} onClick={menuOnClick}>
{renderMenu()}
</ModalDialog>
:
null
})
:
<Popup
open={menuOpen}
direction={direction}
onCloseRequest={closeMenu}
renderLabel={renderPopupLabel}
renderMenu={renderMenu}
/>;
};
Multiselect.propTypes = {
className: PropTypes.string,
mode: PropTypes.oneOf(['popup', 'modal']),
direction: PropTypes.any,
title: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({

View file

@ -40,33 +40,58 @@
}
}
.menu-container {
&:global(.modal) {
.modal-container, .popup-menu-container {
.menu-container {
.option-container {
width: 15rem;
display: flex;
flex-direction: row;
align-items: center;
padding: 1rem;
background-color: var(--color-backgroundlighter);
&:not(:last-child) {
margin-bottom: 1rem;
&: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);
}
}
.no-options-container {
width: 15rem;
}
}
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 1rem;
background-color: var(--color-backgroundlighter);
.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;
.label {
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
font-size: 1.2rem;
text-align: center;
color: var(--color-surfacelighter);
}
}

View file

@ -5,15 +5,17 @@ const classnames = require('classnames');
const FocusLock = require('react-focus-lock').default;
const { useModalsContainer } = require('../ModalsContainerContext');
const Modal = ({ className, autoFocus, disabled, children, ...props }) => {
const Modal = React.forwardRef(({ className, autoFocus, disabled, children, ...props }, ref) => {
const modalsContainer = useModalsContainer();
return ReactDOM.createPortal(
<FocusLock className={classnames(className, 'modal-container')} autoFocus={!!autoFocus} disabled={!!disabled} lockProps={props}>
<FocusLock ref={ref} className={classnames(className, 'modal-container')} autoFocus={!!autoFocus} disabled={!!disabled} lockProps={props}>
{children}
</FocusLock>,
modalsContainer
);
};
});
Modal.displayName = 'Modal';
Modal.propTypes = {
className: PropTypes.string,

View file

@ -1,9 +1,11 @@
const { useRouteFocused } = require('./RouteFocusedContext');
const { useModalsContainer } = require('./ModalsContainerContext');
const Modal = require('./Modal');
const Router = require('./Router');
module.exports = {
useRouteFocused,
useModalsContainer,
Modal,
Router
};

View file

@ -52,12 +52,16 @@ const Discover = ({ urlParams, queryParams }) => {
<MainNavBar className={styles['nav-bar']} route={'discover'} />
<div className={styles['discover-content']}>
<div className={styles['selectable-inputs-container']}>
{selectInputs.map((selectInput, index) => (
{selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => (
<Multiselect
{...selectInput}
key={index}
modalSelects={false}
className={styles['select-input-container']}
title={title}
options={options}
selected={selected}
renderLabelText={renderLabelText}
onSelect={onSelect}
/>
))}
<Button className={styles['filter-container']} title={'More filters'} onClick={openInputsModal}>
@ -144,13 +148,23 @@ const Discover = ({ urlParams, queryParams }) => {
</div>
{
inputsModalOpen ?
<ModalDialog title={'Select inputs'} className={styles['selectable-inputs-modal-container']} onCloseRequest={closeInputsModal}>
{selectInputs.map((selectInput, index) => (
<Multiselect
{...selectInput}
key={index}
modalSelects={true}
/>
<ModalDialog title={'Catalog filters'} className={styles['selectable-inputs-modal-container']} onCloseRequest={closeInputsModal}>
{selectInputs.map(({ title, isRequired, options, selected, renderLabelText, onSelect }, index) => (
<div key={index} className={styles['selectable-inputs-container']}>
<div className={styles['select-input-label-container']} title={title}>
{title}
{isRequired ? '*' : null}
</div>
<Multiselect
className={styles['select-input-container']}
mode={'modal'}
title={title}
options={options}
selected={selected}
renderLabelText={renderLabelText}
onSelect={onSelect}
/>
</div>
))}
</ModalDialog>
:

View file

@ -5,6 +5,7 @@
label-container-label: label;
label-container-icon: icon;
multiselect-menu-container: menu-container;
multiselect-modal-container: modal-container;
}
:import('~stremio/common/PaginationInput/styles.less') {
@ -176,25 +177,36 @@
}
.selectable-inputs-modal-container {
.multiselect-label-container {
width: 15rem;
height: 3rem;
.selectable-inputs-container {
display: flex;
flex-direction: row;
align-items: center;
&:not(:last-child) {
margin-bottom: 1rem;
}
&:hover {
background-color: var(--color-surfacelight);
.label-container-label {
color: var(--color-backgrounddarker);
}
.label-container-icon {
fill: var(--color-backgrounddarker);
}
.select-input-label-container {
flex: none;
width: 10rem;
max-height: 2.4em;
padding-right: 0.5rem;
color: var(--color-backgrounddarker);
}
.select-input-container {
flex: none;
width: 15rem;
height: 3rem;
}
}
}
.multiselect-modal-container {
.multiselect-menu-container {
width: 15rem;
max-height: calc(3.2rem * 7);
overflow-y: auto;
}
}

View file

@ -110,6 +110,7 @@ const mapSelectableInputs = (discover) => {
};
const extraSelects = discover.selectable.extra.map((extra) => {
const title = `Select ${extra.name}`;
const isRequired = extra.isRequired;
const options = (extra.isRequired ? [] : [NONE_EXTRA_VALUE])
.concat(extra.options)
.map((option) => ({
@ -141,7 +142,7 @@ const mapSelectableInputs = (discover) => {
}
});
};
return { title, options, selected, renderLabelText, onSelect };
return { title, isRequired, options, selected, renderLabelText, onSelect };
});
const paginationInput = discover.selectable.has_prev_page || discover.selectable.has_next_page ?
{