diff --git a/src/common/ModalDialog/ModalDialog.js b/src/common/ModalDialog/ModalDialog.js index d6f995392..affe7dd35 100644 --- a/src/common/ModalDialog/ModalDialog.js +++ b/src/common/ModalDialog/ModalDialog.js @@ -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 ( - +
{ diff --git a/src/common/Multiselect/Multiselect.js b/src/common/Multiselect/Multiselect.js index 4ba163702..f197973d1 100644 --- a/src/common/Multiselect/Multiselect.js +++ b/src/common/Multiselect/Multiselect.js @@ -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 }) => ( - - )); - const renderOptions = React.useCallback((options) => { - return ( - options.length > 0 ? - options.map(({ label, value }) => ( - - )) - : -
-
No options available
-
- ); - }, [options]); - return ( - + ), [menuOpen, title, disabled, options, selected, labelOnClick, renderLabelContent, renderLabelText]); + const renderMenu = React.useMemo(() => () => ( +
{ - !modalOptions ? - { - return renderLabel({ - ...labelProps, - ...props, - className: classnames(className, labelProps.className) - }); - }} - renderMenu={() => ( -
- {renderOptions(options)} -
- )} - /> + options.length > 0 ? + options.map(({ label, value }) => ( + + )) : - - {renderLabel({ - ...props, - className - })} - { - menuOpen ? - -
- {renderOptions(options)} -
-
- : - null - } -
+
+
No options available
+
} - - ); +
+ ), [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 ? + + {renderMenu()} + + : + null + }) + : + ; }; Multiselect.propTypes = { className: PropTypes.string, + mode: PropTypes.oneOf(['popup', 'modal']), direction: PropTypes.any, title: PropTypes.string, options: PropTypes.arrayOf(PropTypes.shape({ diff --git a/src/common/Multiselect/styles.less b/src/common/Multiselect/styles.less index 3312b9fed..b06289a56 100644 --- a/src/common/Multiselect/styles.less +++ b/src/common/Multiselect/styles.less @@ -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); } } diff --git a/src/router/Modal/Modal.js b/src/router/Modal/Modal.js index 043a6599b..5309272fe 100644 --- a/src/router/Modal/Modal.js +++ b/src/router/Modal/Modal.js @@ -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( - + {children} , modalsContainer ); -}; +}); + +Modal.displayName = 'Modal'; Modal.propTypes = { className: PropTypes.string, diff --git a/src/router/index.js b/src/router/index.js index 56eb8b422..20b4f3a28 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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 }; diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index db7908310..59f397e99 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -52,12 +52,16 @@ const Discover = ({ urlParams, queryParams }) => {
- {selectInputs.map((selectInput, index) => ( + {selectInputs.map(({ title, options, selected, renderLabelText, onSelect }, index) => ( ))}
{ inputsModalOpen ? - - {selectInputs.map((selectInput, index) => ( - + + {selectInputs.map(({ title, isRequired, options, selected, renderLabelText, onSelect }, index) => ( +
+
+ {title} + {isRequired ? '*' : null} +
+ +
))}
: diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index 47d1710a9..2e84447e8 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -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; } } diff --git a/src/routes/Discover/useSelectableInputs.js b/src/routes/Discover/useSelectableInputs.js index 576109fde..bcb6e6aac 100644 --- a/src/routes/Discover/useSelectableInputs.js +++ b/src/routes/Discover/useSelectableInputs.js @@ -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 ? {