From f20d04d5ee7fef7f8a686840c697e4f925b48464 Mon Sep 17 00:00:00 2001 From: NikolaBorislavovHristov Date: Sun, 6 Oct 2019 11:48:04 +0300 Subject: [PATCH] Popup/Multiselect api changed --- src/common/Dropdown/index.js | 3 - src/common/Dropdown/styles.less | 74 ------- .../Multiselect.js} | 75 ++++---- src/common/Multiselect/index.js | 3 + src/common/Multiselect/styles.less | 78 ++++++++ src/common/Popup/Popup.js | 181 ++++++------------ src/common/Popup/styles.less | 20 +- src/common/index.js | 4 +- 8 files changed, 197 insertions(+), 241 deletions(-) delete mode 100644 src/common/Dropdown/index.js delete mode 100644 src/common/Dropdown/styles.less rename src/common/{Dropdown/Dropdown.js => Multiselect/Multiselect.js} (51%) create mode 100644 src/common/Multiselect/index.js create mode 100644 src/common/Multiselect/styles.less diff --git a/src/common/Dropdown/index.js b/src/common/Dropdown/index.js deleted file mode 100644 index 3ac11be07..000000000 --- a/src/common/Dropdown/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const Dropdown = require('./Dropdown'); - -module.exports = Dropdown; diff --git a/src/common/Dropdown/styles.less b/src/common/Dropdown/styles.less deleted file mode 100644 index af7f98ae0..000000000 --- a/src/common/Dropdown/styles.less +++ /dev/null @@ -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); - } - } -} \ No newline at end of file diff --git a/src/common/Dropdown/Dropdown.js b/src/common/Multiselect/Multiselect.js similarity index 51% rename from src/common/Dropdown/Dropdown.js rename to src/common/Multiselect/Multiselect.js index d9698785d..a02e5476b 100644 --- a/src/common/Dropdown/Dropdown.js +++ b/src/common/Multiselect/Multiselect.js @@ -7,9 +7,24 @@ const Popup = require('stremio/common/Popup'); const useBinaryState = require('stremio/common/useBinaryState'); const styles = require('./styles'); -// TODO rename to multiselect -const Dropdown = ({ className, menuClassName, menuMatchLabelWidth, renderLabel, name, selected, options, tabIndex, onOpen, onClose, onSelect }) => { +const Multiselect = ({ className, direction, title, renderLabelContent, options, selected, onOpen, onClose, onSelect, ...props }) => { + 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 popupLabelOnClick = React.useCallback((event) => { + if (!event.nativeEvent.togglePopupPrevented) { + toggleMenu(); + } + }, [toggleMenu]); + const popupMenuOnClick = React.useCallback((event) => { + event.nativeEvent.togglePopupPrevented = true; + }, []); const optionOnClick = React.useCallback((event) => { if (typeof onSelect === 'function') { onSelect(event); @@ -29,72 +44,66 @@ const Dropdown = ({ className, menuClassName, menuMatchLabelWidth, renderLabel, onClose(); } } - }, [menuOpen, onOpen, onClose]); + }, [menuOpen]); return ( ( - )} renderMenu={() => ( -
- { - Array.isArray(options) && options.length > 0 ? - options.map(({ label, value }) => ( - - )) - : - null - } +
+ {options.map(({ label, value }) => ( + + ))}
)} /> ); }; -Dropdown.propTypes = { +Multiselect.propTypes = { className: PropTypes.string, - menuClassName: PropTypes.string, - menuMatchLabelWidth: PropTypes.bool, - renderLabel: PropTypes.func, - name: PropTypes.string, - selected: PropTypes.arrayOf(PropTypes.string), + direction: PropTypes.any, + title: PropTypes.string, + renderLabelContent: PropTypes.func, 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, onClose: PropTypes.func, onSelect: PropTypes.func }; -module.exports = Dropdown; +module.exports = Multiselect; diff --git a/src/common/Multiselect/index.js b/src/common/Multiselect/index.js new file mode 100644 index 000000000..0df1069f6 --- /dev/null +++ b/src/common/Multiselect/index.js @@ -0,0 +1,3 @@ +const Multiselect = require('./Multiselect'); + +module.exports = Multiselect; diff --git a/src/common/Multiselect/styles.less b/src/common/Multiselect/styles.less new file mode 100644 index 000000000..cc946e659 --- /dev/null +++ b/src/common/Multiselect/styles.less @@ -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); + } + } + } + } +} \ No newline at end of file diff --git a/src/common/Popup/Popup.js b/src/common/Popup/Popup.js index 0b041e541..32de2e057 100644 --- a/src/common/Popup/Popup.js +++ b/src/common/Popup/Popup.js @@ -1,147 +1,78 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const { Modal } = require('stremio-router'); +const FocusLock = require('react-focus-lock').default; const styles = require('./styles'); -// TODO rename to Popover -const Popup = ({ open, menuModalClassName, menuRelativePosition, menuMatchLabelWidth, renderLabel, renderMenu, onCloseRequest }) => { +const Popup = ({ open, direction, renderLabel, renderMenu, onCloseRequest }) => { const labelRef = React.useRef(null); - const menuRef = React.useRef(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 [autoDirection, setAutoDirection] = React.useState(null); const menuOnMouseDown = React.useCallback((event) => { event.nativeEvent.closePopupPrevented = true; }, []); - const menuOnScroll = React.useCallback((event) => { - event.nativeEvent.closePopupPrevented = true; - }, []); 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 (menuRelativePosition !== false) { - const documentRect = document.documentElement.getBoundingClientRect(); - const labelRect = labelRef.current.getBoundingClientRect(); - const menuRect = menuRef.current.getBoundingClientRect(); - const labelPosition = { - 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 matchLabelWidthMenuStyles = { - width: `${labelRect.width}px`, - maxWidth: `${labelRect.width}px` - }; - 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 (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' }; + window.addEventListener('resize', checkCloseEvent); + window.addEventListener('keydown', checkCloseEvent); + window.addEventListener('mousedown', checkCloseEvent); + } + return () => { + window.removeEventListener('resize', checkCloseEvent); + window.removeEventListener('keydown', checkCloseEvent); + window.removeEventListener('mousedown', checkCloseEvent); + }; + }, [open, onCloseRequest]); + React.useLayoutEffect(() => { + if (open) { + const documentRect = document.documentElement.getBoundingClientRect(); + const labelRect = labelRef.current.getBoundingClientRect(); + const labelOffsetTop = labelRect.top - documentRect.top; + const labelOffsetBottom = (documentRect.height + documentRect.top) - (labelRect.top + labelRect.height); + const autoDirection = labelOffsetBottom >= labelOffsetTop ? 'bottom' : 'top'; + setAutoDirection(autoDirection); + } else { + setAutoDirection(null); } - - setMenuStyles(menuStyles); }, [open]); - return ( - - {renderLabel(labelRef)} - { - open ? - -
- {renderMenu()} -
-
- : - null - } -
- ); + return renderLabel({ + ref: labelRef, + className: styles['label-container'], + children: open ? + + {renderMenu()} + + : + null + }); } Popup.propTypes = { open: PropTypes.bool, - menuModalClassName: PropTypes.string, - menuRelativePosition: PropTypes.bool, - menuMatchLabelWidth: PropTypes.bool, + direction: PropTypes.oneOf(['top', 'bottom']), renderLabel: PropTypes.func.isRequired, renderMenu: PropTypes.func.isRequired, - onCloseRequest: PropTypes.func.isRequired + onCloseRequest: PropTypes.func }; module.exports = Popup; diff --git a/src/common/Popup/styles.less b/src/common/Popup/styles.less index 478c81e5c..dfb675caa 100644 --- a/src/common/Popup/styles.less +++ b/src/common/Popup/styles.less @@ -1,12 +1,24 @@ -.menu-modal-container { - pointer-events: none; +.label-container { + position: relative; + overflow: visible; .menu-container { position: absolute; - pointer-events: auto; + right: 0; + z-index: 1; + overflow: visible; visibility: hidden; - overflow: auto; box-shadow: 0 1.35rem 2.7rem var(--color-backgrounddarker40), 0 1.1rem 0.85rem var(--color-backgrounddarker20); + + &.menu-direction-bottom { + top: 100%; + visibility: visible; + } + + &.menu-direction-top { + bottom: 100%; + visibility: visible; + } } } \ No newline at end of file diff --git a/src/common/index.js b/src/common/index.js index 6fd87f1e9..66b43c638 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -1,7 +1,7 @@ const Button = require('./Button'); const Checkbox = require('./Checkbox'); const ColorInput = require('./ColorInput'); -const Dropdown = require('./Dropdown'); +const Multiselect = require('./Multiselect'); const Image = require('./Image'); const MainNavBar = require('./MainNavBar'); const MetaItem = require('./MetaItem'); @@ -29,7 +29,7 @@ module.exports = { Button, Checkbox, ColorInput, - Dropdown, + Multiselect, Image, MainNavBar, MetaItem,