diff --git a/frontend/gulp/webpack.js b/frontend/gulp/webpack.js index 67c795042..cf63f140f 100644 --- a/frontend/gulp/webpack.js +++ b/frontend/gulp/webpack.js @@ -20,7 +20,8 @@ const cssVarsFiles = [ '../src/Styles/Variables/colors', '../src/Styles/Variables/dimensions', '../src/Styles/Variables/fonts', - '../src/Styles/Variables/animations' + '../src/Styles/Variables/animations', + '../src/Styles/Variables/zIndexes' ].map(require.resolve); const plugins = [ diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css index 16a7c9951..6bdfd093e 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.css @@ -1,11 +1,6 @@ -.tether { - z-index: 2000; -} - .button { composes: link from '~Components/Link/Link.css'; - position: relative; display: flex; align-items: center; padding: 6px 16px; @@ -36,9 +31,10 @@ } .contentContainer { + z-index: $popperZIndex; margin-top: 4px; - padding: 0 8px; - width: 400px; + /* 400px container witdh with 8px padding on each side */ + width: 384px; } .content { diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js index 8d734dc32..68c448d1c 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js +++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSelectArtist.js @@ -1,9 +1,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; -import TetherComponent from 'react-tether'; +import { Manager, Popper, Reference } from 'react-popper'; +import getUniqueElememtId from 'Utilities/getUniqueElementId'; import { icons, kinds } from 'Helpers/Props'; import Icon from 'Components/Icon'; +import Portal from 'Components/Portal'; import FormInputButton from 'Components/Form/FormInputButton'; import Link from 'Components/Link/Link'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -12,19 +13,6 @@ import ImportArtistSearchResultConnector from './ImportArtistSearchResultConnect import ImportArtistName from './ImportArtistName'; import styles from './ImportArtistSelectArtist.css'; -const tetherOptions = { - skipMoveElement: true, - constraints: [ - { - to: 'window', - attachment: 'together', - pin: true - } - ], - attachment: 'top center', - targetAttachment: 'bottom center' -}; - class ImportArtistSelectArtist extends Component { // @@ -34,6 +22,9 @@ class ImportArtistSelectArtist extends Component { super(props, context); this._artistLookupTimeout = null; + this._scheduleUpdate = null; + this._buttonId = getUniqueElememtId(); + this._contentId = getUniqueElememtId(); this.state = { term: props.id, @@ -41,16 +32,14 @@ class ImportArtistSelectArtist extends Component { }; } - // - // Control - - _setButtonRef = (ref) => { - this._buttonRef = ref; + componentDidUpdate() { + if (this._scheduleUpdate) { + this._scheduleUpdate(); + } } - _setContentRef = (ref) => { - this._contentRef = ref; - } + // + // Control _addListener() { window.addEventListener('click', this.onWindowClick); @@ -64,14 +53,18 @@ class ImportArtistSelectArtist extends Component { // Listeners onWindowClick = (event) => { - const button = ReactDOM.findDOMNode(this._buttonRef); - const content = ReactDOM.findDOMNode(this._contentRef); + const button = document.getElementById(this._buttonId); + const content = document.getElementById(this._contentId); - if (!button) { + if (!button || !content) { return; } - if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) { + if ( + !button.contains(event.target) && + !content.contains(event.target) && + this.state.isOpen + ) { this.setState({ isOpen: false }); this._removeListener(); } @@ -129,130 +122,159 @@ class ImportArtistSelectArtist extends Component { error.responseJSON.message; return ( - - - { - isLookingUpArtist && isQueued && !isPopulated && - - } - - { - isPopulated && selectedArtist && isExistingArtist && - - } - - { - isPopulated && selectedArtist && - - } - - { - isPopulated && !selectedArtist && -
- - - No match found! -
- } - - { - !isFetching && !!error && -
- - - Search failed, please try again later. -
- } - -
- -
- - - { - this.state.isOpen && + + + {({ ref }) => (
-
-
-
- -
- - + { + isLookingUpArtist && isQueued && !isPopulated ? + : + null + } + + { + isPopulated && selectedArtist && isExistingArtist ? + : + null + } + + { + isPopulated && selectedArtist ? + : + null + } + + { + isPopulated && !selectedArtist ? +
+ + + No match found! +
: + null + } + + { + !isFetching && !!error ? +
+ + + Search failed, please try again later. +
: + null + } + +
+ - - - -
- -
+ +
+ )} + + + + + {({ ref, style, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; + + return ( +
{ - items.map((item) => { - return ( - - ); - }) + this.state.isOpen ? +
+
+
+ +
+ + + + + + +
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+
: + null } +
-
-
- } - + ); + }} + + + ); } } diff --git a/frontend/src/Album/TrackQuality.js b/frontend/src/Album/TrackQuality.js index 08f9f8498..866e7d11f 100644 --- a/frontend/src/Album/TrackQuality.js +++ b/frontend/src/Album/TrackQuality.js @@ -5,6 +5,10 @@ import { kinds } from 'Helpers/Props'; import Label from 'Components/Label'; function getTooltip(title, quality, size) { + if (!title) { + return; + } + const revision = quality.revision; if (revision.real && revision.real > 0) { diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.css b/frontend/src/Components/FileBrowser/FileBrowserModalContent.css index a4c0fb1e7..7ddb9e806 100644 --- a/frontend/src/Components/FileBrowser/FileBrowserModalContent.css +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.css @@ -18,7 +18,7 @@ } .pathInput { - composes: pathInputWrapper from '~Components/Form/PathInput.css'; + composes: inputWrapper from '~Components/Form/PathInput.css'; flex: 0 0 auto; } diff --git a/frontend/src/Components/Form/AutoCompleteInput.js b/frontend/src/Components/Form/AutoCompleteInput.js index 740726b36..e19700d08 100644 --- a/frontend/src/Components/Form/AutoCompleteInput.js +++ b/frontend/src/Components/Form/AutoCompleteInput.js @@ -1,9 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Autosuggest from 'react-autosuggest'; -import classNames from 'classnames'; import jdu from 'jdu'; -import styles from './AutoCompleteInput.css'; +import AutoSuggestInput from './AutoSuggestInput'; class AutoCompleteInput extends Component { @@ -39,31 +37,6 @@ class AutoCompleteInput extends Component { }); } - onInputKeyDown = (event) => { - const { - name, - value, - onChange - } = this.props; - - const { suggestions } = this.state; - - if ( - event.key === 'Tab' && - suggestions.length && - suggestions[0] !== this.props.value - ) { - event.preventDefault(); - - if (value) { - onChange({ - name, - value: suggestions[0] - }); - } - } - } - onInputBlur = () => { this.setState({ suggestions: [] }); } @@ -88,74 +61,37 @@ class AutoCompleteInput extends Component { render() { const { - className, - inputClassName, name, value, - placeholder, - hasError, - hasWarning + ...otherProps } = this.props; const { suggestions } = this.state; - const inputProps = { - className: classNames( - inputClassName, - hasError && styles.hasError, - hasWarning && styles.hasWarning, - ), - name, - value, - placeholder, - autoComplete: 'off', - spellCheck: false, - onChange: this.onInputChange, - onKeyDown: this.onInputKeyDown, - onBlur: this.onInputBlur - }; - - const theme = { - container: styles.inputContainer, - containerOpen: styles.inputContainerOpen, - suggestionsContainer: styles.container, - suggestionsList: styles.list, - suggestion: styles.listItem, - suggestionHighlighted: styles.highlighted - }; - return ( -
- -
+ ); } } AutoCompleteInput.propTypes = { - className: PropTypes.string.isRequired, - inputClassName: PropTypes.string.isRequired, name: PropTypes.string.isRequired, value: PropTypes.string, values: PropTypes.arrayOf(PropTypes.string).isRequired, - placeholder: PropTypes.string, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, onChange: PropTypes.func.isRequired }; AutoCompleteInput.defaultProps = { - className: styles.inputWrapper, - inputClassName: styles.input, value: '' }; diff --git a/frontend/src/Components/Form/AutoCompleteInput.css b/frontend/src/Components/Form/AutoSuggestInput.css similarity index 76% rename from frontend/src/Components/Form/AutoCompleteInput.css rename to frontend/src/Components/Form/AutoSuggestInput.css index 8a19eba06..0dddd47c2 100644 --- a/frontend/src/Components/Form/AutoCompleteInput.css +++ b/frontend/src/Components/Form/AutoSuggestInput.css @@ -10,25 +10,20 @@ composes: hasWarning from '~Components/Form/Input.css'; } -.inputWrapper { - display: flex; -} - .inputContainer { - position: relative; flex-grow: 1; } -.container { +.suggestionsContainer { @add-mixin scrollbar; @add-mixin scrollbarTrack; @add-mixin scrollbarThumb; } -.inputContainerOpen { - .container { - position: absolute; - z-index: 1; +.suggestionsContainerOpen { + z-index: $popperZIndex; + + .suggestionsContainer { overflow-y: auto; max-height: 200px; width: 100%; @@ -39,20 +34,16 @@ } } -.list { +.suggestionsList { margin: 5px 0; padding-left: 0; list-style-type: none; } -.listItem { +.suggestion { padding: 0 16px; } -.match { - font-weight: bold; -} - -.highlighted { +.suggestionHighlighted { background-color: $menuItemHoverBackgroundColor; } diff --git a/frontend/src/Components/Form/AutoSuggestInput.js b/frontend/src/Components/Form/AutoSuggestInput.js new file mode 100644 index 000000000..0fc8172ea --- /dev/null +++ b/frontend/src/Components/Form/AutoSuggestInput.js @@ -0,0 +1,257 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Autosuggest from 'react-autosuggest'; +import { Manager, Popper, Reference } from 'react-popper'; +import classNames from 'classnames'; +import Portal from 'Components/Portal'; +import styles from './AutoSuggestInput.css'; + +class AutoSuggestInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._scheduleUpdate = null; + } + + componentDidUpdate(prevProps) { + if ( + this._scheduleUpdate && + prevProps.suggestions !== this.props.suggestions + ) { + this._scheduleUpdate(); + } + } + + // + // Control + + renderInputComponent = (inputProps) => { + const { renderInputComponent } = this.props; + + return ( + + {({ ref }) => { + if (renderInputComponent) { + return renderInputComponent(inputProps, ref); + } + + return ( +
+ +
+ ); + }} +
+ ); + } + + renderSuggestionsContainer = ({ containerProps, children }) => { + return ( + + + {({ ref: popperRef, style, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; + + return ( +
+
+ {children} +
+
+ ); + }} +
+
+ ); + } + + // + // Listeners + + onComputeMaxHeight = (data) => { + const { + top, + bottom, + width + } = data.offsets.reference; + + const windowHeight = window.innerHeight; + + if ((/^botton/).test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom; + } else { + data.styles.maxHeight = top; + } + + data.styles.width = width; + + return data; + } + + onInputChange = (event, { newValue }) => { + this.props.onChange({ + name: this.props.name, + value: newValue + }); + } + + onInputKeyDown = (event) => { + const { + name, + value, + suggestions, + onChange + } = this.props; + + if ( + event.key === 'Tab' && + suggestions.length && + suggestions[0] !== this.props.value + ) { + event.preventDefault(); + + if (value) { + onChange({ + name, + value: suggestions[0] + }); + } + } + } + + // + // Render + + render() { + const { + forwardedRef, + className, + inputContainerClassName, + name, + value, + placeholder, + suggestions, + hasError, + hasWarning, + getSuggestionValue, + renderSuggestion, + onInputChange, + onInputKeyDown, + onInputFocus, + onInputBlur, + onSuggestionsFetchRequested, + onSuggestionsClearRequested, + onSuggestionSelected, + ...otherProps + } = this.props; + + const inputProps = { + className: classNames( + className, + hasError && styles.hasError, + hasWarning && styles.hasWarning, + ), + name, + value, + placeholder, + autoComplete: 'off', + spellCheck: false, + onChange: onInputChange || this.onInputChange, + onKeyDown: onInputKeyDown || this.onInputKeyDown, + onFocus: onInputFocus, + onBlur: onInputBlur + }; + + const theme = { + container: inputContainerClassName, + containerOpen: styles.suggestionsContainerOpen, + suggestionsContainer: styles.suggestionsContainer, + suggestionsList: styles.suggestionsList, + suggestion: styles.suggestion, + suggestionHighlighted: styles.suggestionHighlighted + }; + + return ( + + + + ); + } +} + +AutoSuggestInput.propTypes = { + forwardedRef: PropTypes.func, + className: PropTypes.string.isRequired, + inputContainerClassName: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + placeholder: PropTypes.string, + suggestions: PropTypes.array.isRequired, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + enforceMaxHeight: PropTypes.bool.isRequired, + minHeight: PropTypes.number.isRequired, + maxHeight: PropTypes.number.isRequired, + getSuggestionValue: PropTypes.func.isRequired, + renderInputComponent: PropTypes.func, + renderSuggestion: PropTypes.func.isRequired, + onInputChange: PropTypes.func, + onInputKeyDown: PropTypes.func, + onInputFocus: PropTypes.func, + onInputBlur: PropTypes.func.isRequired, + onSuggestionsFetchRequested: PropTypes.func.isRequired, + onSuggestionsClearRequested: PropTypes.func.isRequired, + onSuggestionSelected: PropTypes.func, + onChange: PropTypes.func.isRequired +}; + +AutoSuggestInput.defaultProps = { + className: styles.input, + inputContainerClassName: styles.inputContainer, + enforceMaxHeight: true, + minHeight: 50, + maxHeight: 200 +}; + +export default AutoSuggestInput; diff --git a/frontend/src/Components/Form/DeviceInput.css b/frontend/src/Components/Form/DeviceInput.css index 212901853..7abe83db5 100644 --- a/frontend/src/Components/Form/DeviceInput.css +++ b/frontend/src/Components/Form/DeviceInput.css @@ -2,7 +2,7 @@ display: flex; } -.inputContainer { - composes: inputContainer from '~./TagInput.css'; +.input { + composes: input from '~./TagInput.css'; composes: hasButton from '~Components/Form/Input.css'; } diff --git a/frontend/src/Components/Form/DeviceInput.js b/frontend/src/Components/Form/DeviceInput.js index 79d6fd3fa..f77c7cf29 100644 --- a/frontend/src/Components/Form/DeviceInput.js +++ b/frontend/src/Components/Form/DeviceInput.js @@ -47,6 +47,7 @@ class DeviceInput extends Component { render() { const { className, + name, items, selectedDevices, hasError, @@ -58,7 +59,8 @@ class DeviceInput extends Component { return (
{ - this._buttonRef = ref; - } - - _setOptionsRef = (ref) => { - this._optionsRef = ref; - } - _addListener() { window.addEventListener('click', this.onWindowClick); } @@ -125,9 +113,26 @@ class EnhancedSelectInput extends Component { // // Listeners + onComputeMaxHeight = (data) => { + const { + top, + bottom + } = data.offsets.reference; + + const windowHeight = window.innerHeight; + + if ((/^botton/).test(data.placement)) { + data.styles.maxHeight = windowHeight - bottom; + } else { + data.styles.maxHeight = top; + } + + return data; + } + onWindowClick = (event) => { - const button = ReactDOM.findDOMNode(this._buttonRef); - const options = ReactDOM.findDOMNode(this._optionsRef); + const button = document.getElementById(this._buttonId); + const options = document.getElementById(this._optionsId); if (!button || this.state.isMobile) { return; @@ -271,80 +276,110 @@ class EnhancedSelectInput extends Component { return (
- - - - - {selectedOption ? selectedOption.value : null} - - + + + {({ ref }) => (
- + + + + {selectedOption ? selectedOption.value : null} + + +
+ +
+ +
- -
- - { - isOpen && !isMobile && -
-
- { - values.map((v, index) => { - return ( - + + + {({ ref, style, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; + + return ( +
+ { + isOpen && !isMobile ? + - {v.value} - - ); - }) - } -
-
- } - + { + values.map((v, index) => { + return ( + + {v.value} + + ); + }) + } + : + null + } +
+ ); + } + } + + + { isMobile && diff --git a/frontend/src/Components/Form/PathInput.css b/frontend/src/Components/Form/PathInput.css index 94d1b1c62..3b32b16f0 100644 --- a/frontend/src/Components/Form/PathInput.css +++ b/frontend/src/Components/Form/PathInput.css @@ -1,66 +1,16 @@ -.path { - composes: input from '~Components/Form/Input.css'; -} - -.hasError { - composes: hasError from '~Components/Form/Input.css'; -} - -.hasWarning { - composes: hasWarning from '~Components/Form/Input.css'; -} - .hasFileBrowser { + composes: input from '~./AutoSuggestInput.css'; composes: hasButton from '~Components/Form/Input.css'; } -.pathInputWrapper { +.inputWrapper { display: flex; } -.pathInputContainer { - position: relative; - flex-grow: 1; -} - -.pathContainer { - @add-mixin scrollbar; - @add-mixin scrollbarTrack; - @add-mixin scrollbarThumb; -} - -.pathInputContainerOpen { - .pathContainer { - position: absolute; - z-index: 1; - overflow-y: auto; - max-height: 200px; - width: 100%; - border: 1px solid $inputBorderColor; - border-radius: 4px; - background-color: $white; - box-shadow: inset 0 1px 1px $inputBoxShadowColor; - } -} - -.pathList { - margin: 5px 0; - padding-left: 0; - list-style-type: none; -} - -.pathListItem { - padding: 0 16px; -} - .pathMatch { font-weight: bold; } -.pathHighlighted { - background-color: $menuItemHoverBackgroundColor; -} - .fileBrowserButton { composes: button from '~./FormInputButton.css'; diff --git a/frontend/src/Components/Form/PathInput.js b/frontend/src/Components/Form/PathInput.js index 5451844cf..2bc47e586 100644 --- a/frontend/src/Components/Form/PathInput.js +++ b/frontend/src/Components/Form/PathInput.js @@ -1,10 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Autosuggest from 'react-autosuggest'; -import classNames from 'classnames'; import { icons } from 'Helpers/Props'; import Icon from 'Components/Icon'; import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; +import AutoSuggestInput from './AutoSuggestInput'; import FormInputButton from './FormInputButton'; import styles from './PathInput.css'; @@ -16,6 +15,8 @@ class PathInput extends Component { constructor(props, context) { super(props, context); + this._node = document.getElementById('portal-root'); + this.state = { isFileBrowserModalOpen: false }; @@ -106,56 +107,30 @@ class PathInput extends Component { render() { const { className, - inputClassName, name, value, - placeholder, paths, includeFiles, - hasError, - hasWarning, hasFileBrowser, - onChange + onChange, + ...otherProps } = this.props; - - const inputProps = { - className: classNames( - inputClassName, - hasError && styles.hasError, - hasWarning && styles.hasWarning, - hasFileBrowser && styles.hasFileBrowser - ), - name, - value, - placeholder, - autoComplete: 'off', - spellCheck: false, - onChange: this.onInputChange, - onKeyDown: this.onInputKeyDown, - onBlur: this.onInputBlur - }; - - const theme = { - container: styles.pathInputContainer, - containerOpen: styles.pathInputContainerOpen, - suggestionsContainer: styles.pathContainer, - suggestionsList: styles.pathList, - suggestion: styles.pathListItem, - suggestionHighlighted: styles.pathHighlighted - }; - return (
- { @@ -185,14 +160,10 @@ class PathInput extends Component { PathInput.propTypes = { className: PropTypes.string.isRequired, - inputClassName: PropTypes.string.isRequired, name: PropTypes.string.isRequired, value: PropTypes.string, - placeholder: PropTypes.string, paths: PropTypes.array.isRequired, includeFiles: PropTypes.bool.isRequired, - hasError: PropTypes.bool, - hasWarning: PropTypes.bool, hasFileBrowser: PropTypes.bool, onChange: PropTypes.func.isRequired, onFetchPaths: PropTypes.func.isRequired, @@ -200,8 +171,7 @@ PathInput.propTypes = { }; PathInput.defaultProps = { - className: styles.pathInputWrapper, - inputClassName: styles.path, + className: styles.inputWrapper, value: '', hasFileBrowser: true }; diff --git a/frontend/src/Components/Form/TagInput.css b/frontend/src/Components/Form/TagInput.css index 5cf0bca8a..599cb9975 100644 --- a/frontend/src/Components/Form/TagInput.css +++ b/frontend/src/Components/Form/TagInput.css @@ -1,5 +1,6 @@ -.inputContainer { - composes: input from '~Components/Form/Input.css'; + +.input { + composes: input from '~./AutoSuggestInput.css'; position: relative; padding: 0; @@ -13,20 +14,7 @@ } } -.hasError { - composes: hasError from '~Components/Form/Input.css'; -} - -.hasWarning { - composes: hasWarning from '~Components/Form/Input.css'; -} - -.tags { - flex: 0 0 auto; - max-width: 100%; -} - -.input { +.internalInput { flex: 1 1 0%; margin-left: 3px; min-width: 20%; @@ -35,44 +23,3 @@ height: 21px; border: none; } - -.suggestionsContainer { - @add-mixin scrollbar; - @add-mixin scrollbarTrack; - @add-mixin scrollbarThumb; -} - -.containerOpen { - .suggestionsContainer { - position: absolute; - right: -1px; - left: -1px; - z-index: 1; - overflow-y: auto; - margin-top: 1px; - max-height: 110px; - border: 1px solid $inputBorderColor; - border-radius: 4px; - background-color: $white; - box-shadow: inset 0 1px 1px $inputBoxShadowColor; - } -} - -.suggestionsList { - margin: 5px 0; - padding-left: 0; - list-style-type: none; -} - -.suggestion { - padding: 0 16px; - cursor: default; - - &:hover { - background-color: $menuItemHoverBackgroundColor; - } -} - -.suggestionHighlighted { - background-color: $menuItemHoverBackgroundColor; -} diff --git a/frontend/src/Components/Form/TagInput.js b/frontend/src/Components/Form/TagInput.js index fa7ec9dc6..dec4ee2c9 100644 --- a/frontend/src/Components/Form/TagInput.js +++ b/frontend/src/Components/Form/TagInput.js @@ -1,17 +1,17 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import Autosuggest from 'react-autosuggest'; import classNames from 'classnames'; import { kinds } from 'Helpers/Props'; import tagShape from 'Helpers/Props/Shapes/tagShape'; +import AutoSuggestInput from './AutoSuggestInput'; import TagInputInput from './TagInputInput'; import TagInputTag from './TagInputTag'; import styles from './TagInput.css'; function getTag(value, selectedIndex, suggestions, allowNew) { if (selectedIndex == null && value) { - const existingTag = _.find(suggestions, { name: value }); + const existingTag = suggestions.find((suggestion) => suggestion.name === value); if (existingTag) { return existingTag; @@ -184,7 +184,7 @@ class TagInput extends Component { // // Render - renderInputComponent = (inputProps) => { + renderInputComponent = (inputProps, forwardedRef) => { const { tags, kind, @@ -194,6 +194,7 @@ class TagInput extends Component { return ( ); } @@ -269,7 +250,7 @@ class TagInput extends Component { TagInput.propTypes = { className: PropTypes.string.isRequired, - inputClassName: PropTypes.string.isRequired, + inputContainerClassName: PropTypes.string.isRequired, tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, allowNew: PropTypes.bool.isRequired, @@ -285,8 +266,8 @@ TagInput.propTypes = { }; TagInput.defaultProps = { - className: styles.inputContainer, - inputClassName: styles.input, + className: styles.internalInput, + inputContainerClassName: styles.input, allowNew: true, kind: kinds.INFO, placeholder: '', diff --git a/frontend/src/Components/Form/TagInputInput.css b/frontend/src/Components/Form/TagInputInput.css index 182320b1a..059946f34 100644 --- a/frontend/src/Components/Form/TagInputInput.css +++ b/frontend/src/Components/Form/TagInputInput.css @@ -1,4 +1,9 @@ .inputContainer { + position: absolute; + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; display: flex; flex-wrap: wrap; padding: 6px 16px; diff --git a/frontend/src/Components/Form/TagInputInput.js b/frontend/src/Components/Form/TagInputInput.js index 6d5dff2f8..5bf73921b 100644 --- a/frontend/src/Components/Form/TagInputInput.js +++ b/frontend/src/Components/Form/TagInputInput.js @@ -23,6 +23,7 @@ class TagInputInput extends Component { render() { const { + forwardedRef, className, tags, inputProps, @@ -33,6 +34,7 @@ class TagInputInput extends Component { return (
{ - const menu = ReactDOM.findDOMNode(this.refs.menu); - const menuContent = ReactDOM.findDOMNode(this.refs.menuContent); + const menuButton = document.getElementById(this._menuButtonId); - if (!menu) { + if (!menuButton) { return; } - if ((!menu.contains(event.target) || menuContent.contains(event.target)) && this.state.isMenuOpen) { + if (!menuButton.contains(event.target) && this.state.isMenuOpen) { this.setState({ isMenuOpen: false }); this._removeListener(); } @@ -116,8 +127,10 @@ class Menu extends Component { this.setMaxHeight(); } - onWindowScroll = () => { - this.setMaxHeight(); + onWindowScroll = (event) => { + if (this.state.isMenuOpen) { + this.setMaxHeight(); + } } onMenuButtonPress = () => { @@ -158,35 +171,40 @@ class Menu extends Component { } ); - const content = React.cloneElement( - childrenArray[1], - { - ref: 'menuContent', - alignMenu, - maxHeight, - isOpen: isMenuOpen - } - ); - return ( - -
- {button} -
- - { - isMenuOpen && - content - } -
+ + + {({ ref }) => ( +
+ {button} +
+ )} +
+ + + + {({ ref, style, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; + + return React.cloneElement( + childrenArray[1], + { + forwardedRef: ref, + style: { + ...style, + maxHeight + }, + isOpen: isMenuOpen + } + ); + }} + + +
); } } diff --git a/frontend/src/Components/Menu/MenuContent.css b/frontend/src/Components/Menu/MenuContent.css index 0acc07390..b9327fdd7 100644 --- a/frontend/src/Components/Menu/MenuContent.css +++ b/frontend/src/Components/Menu/MenuContent.css @@ -1,4 +1,5 @@ .menuContent { + z-index: $popperZIndex; display: flex; flex-direction: column; background-color: $toolbarMenuItemBackgroundColor; diff --git a/frontend/src/Components/Menu/MenuContent.js b/frontend/src/Components/Menu/MenuContent.js index 1acacf80f..fbeb9ddce 100644 --- a/frontend/src/Components/Menu/MenuContent.js +++ b/frontend/src/Components/Menu/MenuContent.js @@ -10,30 +10,37 @@ class MenuContent extends Component { render() { const { + forwardedRef, className, children, - maxHeight + style, + isOpen } = this.props; return (
- - {children} - + { + isOpen ? + + {children} + : + null + }
); } } MenuContent.propTypes = { + forwardedRef: PropTypes.func, className: PropTypes.string, children: PropTypes.node.isRequired, - maxHeight: PropTypes.number + style: PropTypes.object, + isOpen: PropTypes.bool }; MenuContent.defaultProps = { diff --git a/frontend/src/Components/Menu/MenuItemSeparator.css b/frontend/src/Components/Menu/MenuItemSeparator.css index a867e3153..e48e7f16f 100644 --- a/frontend/src/Components/Menu/MenuItemSeparator.css +++ b/frontend/src/Components/Menu/MenuItemSeparator.css @@ -1,5 +1,6 @@ .separator { overflow: hidden; + min-height: 1px; height: 1px; background-color: $themeDarkColor; } diff --git a/frontend/src/Components/Modal/Modal.css b/frontend/src/Components/Modal/Modal.css index a9b2a27ae..b9d702f86 100644 --- a/frontend/src/Components/Modal/Modal.css +++ b/frontend/src/Components/Modal/Modal.css @@ -1,7 +1,7 @@ .modalContainer { position: absolute; top: 0; - z-index: 1000; + z-index: $modalZIndex; width: 100%; height: 100%; } diff --git a/frontend/src/Components/Modal/Modal.js b/frontend/src/Components/Modal/Modal.js index a9de82c6d..8dfe43433 100644 --- a/frontend/src/Components/Modal/Modal.js +++ b/frontend/src/Components/Modal/Modal.js @@ -28,7 +28,7 @@ class Modal extends Component { constructor(props, context) { super(props, context); - this._node = document.getElementById('modal-root'); + this._node = document.getElementById('portal-root'); this._backgroundRef = null; this._modalId = getUniqueElememtId(); } diff --git a/frontend/src/Components/Portal.js b/frontend/src/Components/Portal.js new file mode 100644 index 000000000..2e5237093 --- /dev/null +++ b/frontend/src/Components/Portal.js @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; + +function Portal(props) { + const { children, target } = props; + return ReactDOM.createPortal(children, target); +} + +Portal.propTypes = { + children: PropTypes.node.isRequired, + target: PropTypes.object.isRequired +}; + +Portal.defaultProps = { + target: document.getElementById('portal-root') +}; + +export default Portal; diff --git a/frontend/src/Components/Tooltip/Popover.css b/frontend/src/Components/Tooltip/Popover.css index f7b87f0b9..7b0592844 100644 --- a/frontend/src/Components/Tooltip/Popover.css +++ b/frontend/src/Components/Tooltip/Popover.css @@ -1,97 +1,3 @@ -.tether { - z-index: 2000; -} - -.popoverContainer { - margin: 10px 15px; -} - -.popover { - position: relative; - background-color: $white; - box-shadow: 0 5px 10px $popoverShadowColor; -} - -.arrow, -.arrow::after { - position: absolute; - display: block; - width: 0; - height: 0; - border-width: 11px; - border-style: solid; - border-color: transparent; -} - -.arrow::after { - border-width: 10px; - content: ''; -} - -.top { - bottom: -11px; - left: 50%; - margin-left: -11px; - border-top-color: $popoverArrowBorderColor; - border-bottom-width: 0; - - &::after { - bottom: 1px; - margin-left: -10px; - border-top-color: $white; - border-bottom-width: 0; - content: ' '; - } -} - -.right { - top: 50%; - left: -11px; - margin-top: -11px; - border-right-color: $popoverArrowBorderColor; - border-left-width: 0; - - &::after { - bottom: -10px; - left: 1px; - border-right-color: $white; - border-left-width: 0; - content: ' '; - } -} - -.bottom { - top: -11px; - left: 50%; - margin-left: -11px; - border-top-width: 0; - border-bottom-color: $popoverArrowBorderColor; - - &::after { - top: 1px; - margin-left: -10px; - border-top-width: 0; - border-bottom-color: $white; - content: ' '; - } -} - -.left { - top: 50%; - right: -11px; - margin-top: -11px; - border-right-width: 0; - border-left-color: $popoverArrowBorderColor; - - &::after { - right: 1px; - bottom: -10px; - border-right-width: 0; - border-left-color: $white; - content: ' '; - } -} - .title { padding: 10px 20px; border-bottom: 1px solid $popoverTitleBorderColor; @@ -103,3 +9,7 @@ overflow: auto; padding: 10px; } + +.tooltipBody { + padding: 0; +} diff --git a/frontend/src/Components/Tooltip/Popover.js b/frontend/src/Components/Tooltip/Popover.js index c958fce1b..9ce73cf08 100644 --- a/frontend/src/Components/Tooltip/Popover.js +++ b/frontend/src/Components/Tooltip/Popover.js @@ -1,160 +1,37 @@ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import TetherComponent from 'react-tether'; -import classNames from 'classnames'; -import isMobileUtil from 'Utilities/isMobile'; -import { tooltipPositions } from 'Helpers/Props'; +import React from 'react'; +import Tooltip from './Tooltip'; import styles from './Popover.css'; -const baseTetherOptions = { - skipMoveElement: true, - constraints: [ - { - to: 'window', - attachment: 'together', - pin: true - } - ] -}; - -const tetherOptions = { - [tooltipPositions.TOP]: { - ...baseTetherOptions, - attachment: 'bottom center', - targetAttachment: 'top center' - }, - - [tooltipPositions.RIGHT]: { - ...baseTetherOptions, - attachment: 'middle left', - targetAttachment: 'middle right' - }, - - [tooltipPositions.BOTTOM]: { - ...baseTetherOptions, - attachment: 'top center', - targetAttachment: 'bottom center' - }, - - [tooltipPositions.LEFT]: { - ...baseTetherOptions, - attachment: 'middle right', - targetAttachment: 'middle left' - } -}; - -class Popover extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isOpen: false - }; - - this._closeTimeout = null; - } - - componentWillUnmount() { - if (this._closeTimeout) { - this._closeTimeout = clearTimeout(this._closeTimeout); - } - } - - // - // Listeners - - onClick = () => { - if (isMobileUtil()) { - this.setState({ isOpen: !this.state.isOpen }); - } - } - - onMouseEnter = () => { - if (this._closeTimeout) { - this._closeTimeout = clearTimeout(this._closeTimeout); - } - - this.setState({ isOpen: true }); - } - - onMouseLeave = () => { - this._closeTimeout = setTimeout(() => { - this.setState({ isOpen: false }); - }, 100); - } - - // - // Render - - render() { - const { - className, - anchor, - title, - body, - position - } = this.props; - - return ( - - - {anchor} - - - { - this.state.isOpen && -
-
-
- -
- {title} -
- -
- {body} -
-
-
- } - - ); - } +function Popover(props) { + const { + title, + body, + ...otherProps + } = props; + + return ( + +
+ {title} +
+ +
+ {body} +
+
+ } + /> + ); } Popover.propTypes = { - className: PropTypes.string, - anchor: PropTypes.node.isRequired, title: PropTypes.string.isRequired, - body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - position: PropTypes.oneOf(tooltipPositions.all) -}; - -Popover.defaultProps = { - position: tooltipPositions.TOP + body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired }; export default Popover; diff --git a/frontend/src/Components/Tooltip/Tooltip.css b/frontend/src/Components/Tooltip/Tooltip.css index d1d798e0f..1db58372b 100644 --- a/frontend/src/Components/Tooltip/Tooltip.css +++ b/frontend/src/Components/Tooltip/Tooltip.css @@ -1,8 +1,5 @@ -.tether { - z-index: 2000; -} - .tooltipContainer { + z-index: $popperZIndex; margin: 10px 15px; } diff --git a/frontend/src/Components/Tooltip/Tooltip.js b/frontend/src/Components/Tooltip/Tooltip.js index 43caf87e8..ea940c9d7 100644 --- a/frontend/src/Components/Tooltip/Tooltip.js +++ b/frontend/src/Components/Tooltip/Tooltip.js @@ -1,48 +1,12 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import TetherComponent from 'react-tether'; +import { Manager, Popper, Reference } from 'react-popper'; import classNames from 'classnames'; import isMobileUtil from 'Utilities/isMobile'; import { kinds, tooltipPositions } from 'Helpers/Props'; +import Portal from 'Components/Portal'; import styles from './Tooltip.css'; -const baseTetherOptions = { - skipMoveElement: true, - constraints: [ - { - to: 'window', - attachment: 'together', - pin: true - } - ] -}; - -const tetherOptions = { - [tooltipPositions.TOP]: { - ...baseTetherOptions, - attachment: 'bottom center', - targetAttachment: 'top center' - }, - - [tooltipPositions.RIGHT]: { - ...baseTetherOptions, - attachment: 'middle left', - targetAttachment: 'middle right' - }, - - [tooltipPositions.BOTTOM]: { - ...baseTetherOptions, - attachment: 'top center', - targetAttachment: 'bottom center' - }, - - [tooltipPositions.LEFT]: { - ...baseTetherOptions, - attachment: 'middle right', - targetAttachment: 'middle left' - } -}; - class Tooltip extends Component { // @@ -51,11 +15,18 @@ class Tooltip extends Component { constructor(props, context) { super(props, context); + this._scheduleUpdate = null; + this._closeTimeout = null; + this.state = { isOpen: false }; + } - this._closeTimeout = null; + componentDidUpdate() { + if (this._scheduleUpdate && this.state.isOpen) { + this._scheduleUpdate(); + } } componentWillUnmount() { @@ -67,6 +38,10 @@ class Tooltip extends Component { // // Listeners + onMeasure = ({ width }) => { + this.setState({ width }); + } + onClick = () => { if (isMobileUtil()) { this.setState({ isOpen: !this.state.isOpen }); @@ -93,6 +68,7 @@ class Tooltip extends Component { render() { const { className, + bodyClassName, anchor, tooltip, kind, @@ -100,55 +76,81 @@ class Tooltip extends Component { } = this.props; return ( - - - {anchor} - - - { - this.state.isOpen && -
+ + {({ ref }) => ( + -
+ {anchor} + + )} + + + + + {({ ref, style, placement, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; + + return (
- -
- {tooltip} + ref={ref} + className={styles.tooltipContainer} + style={style} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + > + { + this.state.isOpen ? +
+
+ +
+ {tooltip} +
+
: + null + }
-
-
- } - + ); + }} +
+
+ ); } } Tooltip.propTypes = { className: PropTypes.string, + bodyClassName: PropTypes.string.isRequired, anchor: PropTypes.node.isRequired, tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]), @@ -156,6 +158,7 @@ Tooltip.propTypes = { }; Tooltip.defaultProps = { + bodyClassName: styles.body, kind: kinds.DEFAULT, position: tooltipPositions.TOP }; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index 9a30186be..6d628fa1d 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -238,6 +238,7 @@ class InteractiveImportRow extends Component { { @@ -247,6 +248,7 @@ class InteractiveImportRow extends Component { { @@ -256,6 +258,7 @@ class InteractiveImportRow extends Component { { @@ -268,6 +271,7 @@ class InteractiveImportRow extends Component { { @@ -286,6 +290,7 @@ class InteractiveImportRow extends Component { { diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.js b/frontend/src/Settings/Quality/Definition/QualityDefinition.js index e83236069..9c5258019 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinition.js +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.js @@ -3,10 +3,12 @@ import React, { Component } from 'react'; import ReactSlider from 'react-slider'; import formatBytes from 'Utilities/Number/formatBytes'; import roundNumber from 'Utilities/Number/roundNumber'; -import { kinds } from 'Helpers/Props'; +import { kinds, tooltipPositions } from 'Helpers/Props'; import Label from 'Components/Label'; import NumberInput from 'Components/Form/NumberInput'; import TextInput from 'Components/Form/TextInput'; +import Popover from 'Components/Tooltip/Popover'; +import QualityDefinitionLimits from './QualityDefinitionLimits'; import styles from './QualityDefinition.css'; const MIN = 0; @@ -141,13 +143,8 @@ class QualityDefinition extends Component { const minBytes = minSize * 128; const maxBytes = maxSize && maxSize * 128; - // Calculates the bytes used by a twenty minute EP - const minTwenty = formatBytes(minBytes * 20 * 60, 2); - const maxTwenty = maxBytes ? formatBytes(maxBytes * 20 * 60, 2) : 'Unlimited'; - - // Calculates the bytes used by a forty-five minute LP - const minFortyFive = formatBytes(minBytes * 45 * 60, 2); - const maxFortyFive = maxBytes ? formatBytes(maxBytes * 45 * 60, 2) : 'Unlimited'; + const minRate = `${formatBytes(minBytes, true)}/s`; + const maxRate = maxBytes ? `${formatBytes(maxBytes, true)}/s` : 'Unlimited'; return (
@@ -181,13 +178,35 @@ class QualityDefinition extends Component {
- - + {minRate} + } + title="Minimum Limits" + body={ + + } + position={tooltipPositions.BOTTOM} + />
- - + {maxRate} + } + title="Maximum Limits" + body={ + + } + position={tooltipPositions.BOTTOM} + />
diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.js b/frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.js new file mode 100644 index 000000000..618fa1bd8 --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; + +function QualityDefinitionLimits(props) { + const { + bytes, + message + } = props; + + if (!bytes) { + return
{message}
; + } + + const twenty = formatBytes(bytes * 20 * 60); + const fourtyFive = formatBytes(bytes * 45 * 60); + const sixty = formatBytes(bytes * 60 * 60); + + return ( +
+
20 Minutes: {twenty}
+
45 Minutes: {fourtyFive}
+
60 Minutes: {sixty}
+
+ ); +} + +QualityDefinitionLimits.propTypes = { + bytes: PropTypes.number, + message: PropTypes.string.isRequired +}; + +export default QualityDefinitionLimits; diff --git a/frontend/src/Styles/Variables/colors.js b/frontend/src/Styles/Variables/colors.js index 8626225cf..427cba0c3 100644 --- a/frontend/src/Styles/Variables/colors.js +++ b/frontend/src/Styles/Variables/colors.js @@ -164,7 +164,7 @@ module.exports = { popoverTitleBackgroundColor: '#f7f7f7', popoverTitleBorderColor: '#ebebeb', popoverShadowColor: 'rgba(0, 0, 0, 0.2)', - popoverArrowBorderColor: 'rgba(0, 0, 0, 0.25)', + popoverArrowBorderColor: '#fff', popoverTitleBackgroundInverseColor: '#3a3f51', popoverTitleBorderInverseColor: '#353535', diff --git a/frontend/src/Styles/Variables/zIndexes.js b/frontend/src/Styles/Variables/zIndexes.js new file mode 100644 index 000000000..986ceb548 --- /dev/null +++ b/frontend/src/Styles/Variables/zIndexes.js @@ -0,0 +1,4 @@ +module.exports = { + modalZIndex: 1000, + popperZIndex: 2000 +}; diff --git a/frontend/src/Utilities/Number/formatBytes.js b/frontend/src/Utilities/Number/formatBytes.js index 1ff1b5a97..7bae5367b 100644 --- a/frontend/src/Utilities/Number/formatBytes.js +++ b/frontend/src/Utilities/Number/formatBytes.js @@ -1,6 +1,6 @@ import filesize from 'filesize'; -function formatBytes(input) { +function formatBytes(input, showBits = false) { const size = Number(input); if (isNaN(size)) { @@ -9,7 +9,8 @@ function formatBytes(input) { return filesize(size, { base: 2, - round: 1 + round: 1, + bits: showBits }); } diff --git a/frontend/src/index.html b/frontend/src/index.html index f8b0e5e6a..b1b48b66a 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -48,7 +48,7 @@ - +
diff --git a/package.json b/package.json index dab3e4c2d..21517be0a 100644 --- a/package.json +++ b/package.json @@ -99,10 +99,10 @@ "react-google-recaptcha": "1.0.5", "react-lazyload": "2.5.0", "react-measure": "1.4.7", + "react-popper": "1.3.3", "react-redux": "6.0.1", "react-router-dom": "4.3.1", "react-slider": "0.11.2", - "react-tether": "1.0.4", "react-text-truncate": "0.14.0", "react-virtualized": "9.21.0", "redux": "4.0.1", diff --git a/yarn.lock b/yarn.lock index 47f3519a0..80a24dcc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2371,6 +2371,14 @@ create-react-class@15.6.3: loose-envify "^1.3.1" object-assign "^4.1.1" +create-react-context@<=0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca" + integrity sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A== + dependencies: + fbjs "^0.8.0" + gud "^1.0.0" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -3427,7 +3435,7 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.9: +fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= @@ -3917,6 +3925,11 @@ graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, g resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== +gud@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" + integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== + gulp-cached@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/gulp-cached/-/gulp-cached-1.1.1.tgz#fe7cd4f87f37601e6073cfedee5c2bdaf8b6acce" @@ -6273,6 +6286,11 @@ plugin-error@^0.1.2: arr-union "^2.0.1" extend-shallow "^1.1.2" +popper.js@^1.14.4: + version "1.15.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" + integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -7095,6 +7113,18 @@ react-measure@1.4.7: prop-types "^15.5.4" resize-observer-polyfill "^1.4.1" +react-popper@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6" + integrity sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w== + dependencies: + "@babel/runtime" "^7.1.2" + create-react-context "<=0.2.2" + popper.js "^1.14.4" + prop-types "^15.6.1" + typed-styles "^0.0.7" + warning "^4.0.2" + react-redux@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" @@ -7145,14 +7175,6 @@ react-slider@0.11.2: resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-0.11.2.tgz#ae014e1454c3cdd5f28b5c2495b2a08abd9971e6" integrity sha512-y49ZwJJ7OcPdihgt71xYI8GRdAzpFuSLQR8b+cKotutxqf8MAEPEtqvWKlg+3ZQRe5PMN6oWbIb7wEYDF8XhNQ== -react-tether@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/react-tether/-/react-tether-1.0.4.tgz#a1f0973391ba4c3c2b5c38d5be16c42f0fd96759" - integrity sha512-WQ3Ulj9k6to8We/rUqkX4fB5L4jYGnUEXtAxyth9kcKqf0miVOR6MYS3hJodQbpNIBB5DvA+/ZH8nlUtMupSVA== - dependencies: - prop-types "^15.6.2" - tether "^1.4.5" - react-text-truncate@0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/react-text-truncate/-/react-text-truncate-0.14.0.tgz#f33319804459f429b55bf13784de4f7125c9bba3" @@ -8384,11 +8406,6 @@ terser@^3.16.1: source-map "~0.6.1" source-map-support "~0.5.9" -tether@^1.4.5: - version "1.4.5" - resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.5.tgz#8efd7b35572767ba502259ba9b1cc167fcf6f2c1" - integrity sha512-fysT1Gug2wbRi7a6waeu39yVDwiNtvwj5m9eRD+qZDSHKNghLo6KqP/U3yM2ap6TNUL2skjXGJaJJTJqoC31vw== - text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -8590,6 +8607,11 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +typed-styles@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" + integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q== + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -9007,7 +9029,7 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" -warning@^4.0.1: +warning@^4.0.1, warning@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==