diff --git a/frontend/gulp/webpack.js b/frontend/gulp/webpack.js index 82a6dcd62..779373307 100644 --- a/frontend/gulp/webpack.js +++ b/frontend/gulp/webpack.js @@ -19,7 +19,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/AddSeries/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.css b/frontend/src/AddSeries/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.css index 16a7c9951..6bdfd093e 100644 --- a/frontend/src/AddSeries/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.css +++ b/frontend/src/AddSeries/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.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/AddSeries/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.js b/frontend/src/AddSeries/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.js index d67a6bf2e..c029c8fe2 100644 --- a/frontend/src/AddSeries/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.js +++ b/frontend/src/AddSeries/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.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 ImportSeriesSearchResultConnector from './ImportSeriesSearchResultConnect import ImportSeriesTitle from './ImportSeriesTitle'; import styles from './ImportSeriesSelectSeries.css'; -const tetherOptions = { - skipMoveElement: true, - constraints: [ - { - to: 'window', - attachment: 'together', - pin: true - } - ], - attachment: 'top center', - targetAttachment: 'bottom center' -}; - class ImportSeriesSelectSeries extends Component { // @@ -34,8 +22,9 @@ class ImportSeriesSelectSeries extends Component { super(props, context); this._seriesLookupTimeout = null; - this._buttonRef = {}; - this._contentRef = {}; + this._scheduleUpdate = null; + this._buttonId = getUniqueElememtId(); + this._contentId = getUniqueElememtId(); this.state = { term: props.id, @@ -43,6 +32,12 @@ class ImportSeriesSelectSeries extends Component { }; } + componentDidUpdate() { + if (this._scheduleUpdate) { + this._scheduleUpdate(); + } + } + // // Control @@ -58,8 +53,8 @@ class ImportSeriesSelectSeries extends Component { // Listeners onWindowClick = (event) => { - const button = ReactDOM.findDOMNode(this._buttonRef.current); - const content = ReactDOM.findDOMNode(this._contentRef.current); + const button = document.getElementById(this._buttonId); + const content = document.getElementById(this._contentId); if (!button || !content) { return; @@ -127,150 +122,158 @@ class ImportSeriesSelectSeries extends Component { error.responseJSON.message; return ( - { - this._buttonRef = ref; + + + {({ ref }) => ( +
+ + { + isLookingUpSeries && isQueued && !isPopulated ? + : + null + } - return ( -
- - { - isLookingUpSeries && isQueued && !isPopulated ? - : - null - } + { + isPopulated && selectedSeries && isExistingSeries ? + : + null + } - { - isPopulated && selectedSeries && isExistingSeries ? + { + isPopulated && selectedSeries ? + : + null + } + + { + isPopulated && !selectedSeries ? +
: - null - } - - { - isPopulated && selectedSeries ? - : - null - } - - { - isPopulated && !selectedSeries ? -
- + /> No match found! -
: - null - } +
: + null + } - { - !isFetching && !!error ? -
- + { + !isFetching && !!error ? +
+ Search failed, please try again later. +
: + null + } + +
+ +
+ +
+ )} + + + + + {({ ref, style, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; + + return ( +
+ { + this.state.isOpen ? +
+
+
+ +
+ + + + + + +
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } +
: null } - -
- -
- -
- ); - } - } - renderElement={ - (ref) => { - this._contentRef = ref; - - if (!this.state.isOpen) { - return; - } - - return ( -
-
-
-
- -
- - - - - - -
- -
- { - items.map((item) => { - return ( - - ); - }) - } -
-
- ); - } - } - /> + ); + }} +
+
+ ); } } 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..29b525206 --- /dev/null +++ b/frontend/src/Components/Form/AutoSuggestInput.js @@ -0,0 +1,273 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Autosuggest from 'react-autosuggest'; +import { Manager, Popper, Reference } from 'react-popper'; +import classNames from 'classnames'; +import styles from './AutoSuggestInput.css'; + +class AutoSuggestInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._scheduleUpdate = null; + this._node = document.getElementById('portal-root'); + } + + 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 ReactDOM.createPortal( + + {({ ref: popperRef, style, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; + + return ( +
+
+ {children} +
+
+ ); + }} +
, + this._node + ); + } + + // + // Listeners + + onComputeStyle = (data) => { + const { + enforceMaxHeight, + maxHeight + } = this.props; + + const { + top, + bottom, + left, + width + } = data.offsets.reference; + + const popperHeight = data.offsets.popper.height; + const windowHeight = window.innerHeight; + + data.styles.top = 0; + data.styles.left = 0; + data.styles.width = width; + data.styles.willChange = 'transform'; + + if (data.placement === 'bottom-start') { + data.styles.transform = `translate3d(${left}px, ${bottom}px, 0)`; + + if (enforceMaxHeight) { + data.styles.maxHeight = Math.min(maxHeight, windowHeight - bottom); + } + } else if (data.placement === 'top-start') { + data.styles.transform = `translate3d(${left}px, ${top-popperHeight}px, 0)`; + + if (enforceMaxHeight) { + data.styles.maxHeight = Math.min(maxHeight, top); + } + } + + 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 (
{ - const button = ReactDOM.findDOMNode(this._buttonRef.current); - const options = ReactDOM.findDOMNode(this._optionsRef.current); + const button = document.getElementById(this._buttonId); + const options = document.getElementById(this._optionsId); if (!button || this.state.isMobile) { return; @@ -266,96 +259,96 @@ class EnhancedSelectInput extends Component { return (
- { - this._buttonRef = ref; - - return ( + + + {({ ref }) => ( +
-
- + - - {selectedOption ? selectedOption.value : null} - + {selectedOption ? selectedOption.value : null} + -
- -
- -
+
+ +
+
- ); - } - } - renderElement={ - (ref) => { - this._optionsRef = ref; +
+ )} +
+ + + {({ ref, style, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; - if (!isOpen || isMobile) { - return; - } - - return ( -
-
+ return ( +
{ - values.map((v, index) => { - return ( - - {v.value} - - ); - }) + isOpen && !isMobile ? +
+ { + 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..87b2849f1 100644 --- a/frontend/src/Components/Form/TagInput.css +++ b/frontend/src/Components/Form/TagInput.css @@ -1,5 +1,5 @@ -.inputContainer { - composes: input from '~Components/Form/Input.css'; +.input { + composes: input from '~./AutoSuggestInput.css'; position: relative; padding: 0; @@ -13,20 +13,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 +22,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._menuRef.current); - const menuContent = ReactDOM.findDOMNode(this._menuContentRef.current); + const menuButton = document.getElementById(this._menuButtonId); - if (!menu || !menuContent) { + 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(); } @@ -124,17 +128,9 @@ class Menu extends Component { } onWindowScroll = (event) => { - if (!this._menuContentRef.current) { - return; + if (this.state.isMenuOpen) { + this.setMaxHeight(); } - - const menuContent = ReactDOM.findDOMNode(this._menuContentRef.current); - - if (menuContent && menuContent.contains(event.target)) { - return; - } - - this.setMaxHeight(); } onMenuButtonPress = () => { @@ -176,45 +172,39 @@ class Menu extends Component { ); return ( - { - this._menuRef = ref; + + + {({ ref }) => ( +
+ {button} +
+ )} +
- return ( -
- {button} -
- ); - } - } - renderElement={ - (ref) => { - this._menuContentRef = ref; + + + {({ ref, style, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; - if (!isMenuOpen) { - return null; - } - - return React.cloneElement( - childrenArray[1], - { - ref, - alignMenu, - maxHeight, - isOpen: isMenuOpen - } - ); - } - } - /> + 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/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 567815654..9ce73cf08 100644 --- a/frontend/src/Components/Tooltip/Popover.js +++ b/frontend/src/Components/Tooltip/Popover.js @@ -1,171 +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 - } - ] -}; +function Popover(props) { + const { + title, + body, + ...otherProps + } = props; -const tetherOptions = { - [tooltipPositions.TOP]: { - ...baseTetherOptions, - attachment: 'bottom center', - targetAttachment: 'top center' - }, + return ( + +
+ {title} +
- [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} - - ) - } - renderElement={ - (ref) => { - if (!this.state.isOpen) { - return null; - } - - return ( -
-
-
- -
- {title} -
- -
- {body} -
-
-
- ); - } - } - /> - ); - } +
+ {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 40293081c..af872ba4d 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,13 +76,9 @@ class Tooltip extends Component { } = this.props; return ( - ( + + + {({ ref }) => ( {anchor} - ) - } - renderElement={ - (ref) => { - if (!this.state.isOpen) { - return; - } + )} + - return ( -
+ + + {({ ref, style, scheduleUpdate }) => { + this._scheduleUpdate = scheduleUpdate; + + return (
-
+ { + this.state.isOpen ? +
+
-
- {tooltip} -
+
+ {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]), @@ -167,6 +155,7 @@ Tooltip.propTypes = { }; Tooltip.defaultProps = { + bodyClassName: styles.body, kind: kinds.DEFAULT, position: tooltipPositions.TOP }; diff --git a/frontend/src/Styles/Variables/colors.js b/frontend/src/Styles/Variables/colors.js index 051a8b624..75e140db7 100644 --- a/frontend/src/Styles/Variables/colors.js +++ b/frontend/src/Styles/Variables/colors.js @@ -163,7 +163,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: '#4f566f', 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/index.html b/frontend/src/index.html index 8e13b9452..799deaf44 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -48,7 +48,7 @@ - +
diff --git a/package.json b/package.json index fb4a422cc..64ae2281a 100644 --- a/package.json +++ b/package.json @@ -96,11 +96,11 @@ "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-tabs": "3.0.0", - "react-tether": "2.0.0", "react-text-truncate": "0.14.0", "react-virtualized": "9.21.0", "redux": "4.0.1", diff --git a/yarn.lock b/yarn.lock index 2dff34604..6aef422df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2369,6 +2369,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" @@ -3372,7 +3380,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= @@ -3866,6 +3874,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" @@ -6319,6 +6332,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" @@ -6874,6 +6892,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" @@ -6932,14 +6962,6 @@ react-tabs@3.0.0: classnames "^2.2.0" prop-types "^15.5.0" -react-tether@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/react-tether/-/react-tether-2.0.0.tgz#84928b9636f1fe0a6874d1e450e7822e87f8cb07" - integrity sha512-iJnqTQV42Pf7w4xrg3g1gxSxbBCXleDt8AzlSoAqRINqB+mhcJUeegpB8SFMJ/nKT7lSfMkx3GvUfYY+9sPBGw== - 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" @@ -8235,11 +8257,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" @@ -8457,6 +8474,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" @@ -8862,7 +8884,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==