Replace react-tether with react-popper

This commit is contained in:
Mark McDowall 2019-04-14 14:46:40 -07:00
parent 9c26da70da
commit 478e13b0fd
33 changed files with 838 additions and 991 deletions

View File

@ -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 = [

View File

@ -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 {

View File

@ -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 (
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions}
renderTarget={
(ref) => {
this._buttonRef = ref;
<Manager>
<Reference>
{({ ref }) => (
<div
ref={ref}
id={this._buttonId}
>
<Link
ref={ref}
className={styles.button}
component="div"
onPress={this.onPress}
>
{
isLookingUpSeries && isQueued && !isPopulated ?
<LoadingIndicator
className={styles.loading}
size={20}
/> :
null
}
return (
<div ref={ref}>
<Link
className={styles.button}
component="div"
onPress={this.onPress}
>
{
isLookingUpSeries && isQueued && !isPopulated ?
<LoadingIndicator
className={styles.loading}
size={20}
/> :
null
}
{
isPopulated && selectedSeries && isExistingSeries ?
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/> :
null
}
{
isPopulated && selectedSeries && isExistingSeries ?
{
isPopulated && selectedSeries ?
<ImportSeriesTitle
title={selectedSeries.title}
year={selectedSeries.year}
network={selectedSeries.network}
isExistingSeries={isExistingSeries}
/> :
null
}
{
isPopulated && !selectedSeries ?
<div className={styles.noMatches}>
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/> :
null
}
{
isPopulated && selectedSeries ?
<ImportSeriesTitle
title={selectedSeries.title}
year={selectedSeries.year}
network={selectedSeries.network}
isExistingSeries={isExistingSeries}
/> :
null
}
{
isPopulated && !selectedSeries ?
<div className={styles.noMatches}>
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
/>
No match found!
</div> :
null
}
</div> :
null
}
{
!isFetching && !!error ?
<div>
<Icon
className={styles.warningIcon}
title={errorMessage}
name={icons.WARNING}
kind={kinds.WARNING}
/>
{
!isFetching && !!error ?
<div>
<Icon
className={styles.warningIcon}
title={errorMessage}
name={icons.WARNING}
kind={kinds.WARNING}
/>
Search failed, please try again later.
</div> :
null
}
<div className={styles.dropdownArrowContainer}>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</div>
)}
</Reference>
<Portal>
<Popper
placement="bottom"
modifiers={{
preventOverflow: {
boundariesElement: 'viewport'
}
}}
>
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
return (
<div
ref={ref}
id={this._contentId}
className={styles.contentContainer}
style={style}
>
{
this.state.isOpen ?
<div className={styles.content}>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon name={icons.SEARCH} />
</div>
<TextInput
className={styles.searchInput}
name={`${name}_textInput`}
value={this.state.term}
onChange={this.onSearchInputChange}
/>
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>
<Icon name={icons.REFRESH} />
</FormInputButton>
</div>
<div className={styles.results}>
{
items.map((item) => {
return (
<ImportSeriesSearchResultConnector
key={item.tvdbId}
tvdbId={item.tvdbId}
title={item.title}
year={item.year}
network={item.network}
onPress={this.onSeriesSelect}
/>
);
})
}
</div>
</div> :
null
}
<div className={styles.dropdownArrowContainer}>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</div>
);
}
}
renderElement={
(ref) => {
this._contentRef = ref;
if (!this.state.isOpen) {
return;
}
return (
<div
ref={ref}
className={styles.contentContainer}
>
<div className={styles.content}>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon name={icons.SEARCH} />
</div>
<TextInput
className={styles.searchInput}
name={`${name}_textInput`}
value={this.state.term}
onChange={this.onSearchInputChange}
/>
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>
<Icon name={icons.REFRESH} />
</FormInputButton>
</div>
<div className={styles.results}>
{
items.map((item) => {
return (
<ImportSeriesSearchResultConnector
key={item.tvdbId}
tvdbId={item.tvdbId}
title={item.title}
year={item.year}
network={item.network}
onPress={this.onSeriesSelect}
/>
);
})
}
</div>
</div>
</div>
);
}
}
/>
);
}}
</Popper>
</Portal>
</Manager>
);
}
}

View File

@ -18,7 +18,7 @@
}
.pathInput {
composes: pathInputWrapper from '~Components/Form/PathInput.css';
composes: inputWrapper from '~Components/Form/PathInput.css';
flex: 0 0 auto;
}

View File

@ -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 (
<div className={className}>
<Autosuggest
id={name}
inputProps={inputProps}
theme={theme}
suggestions={suggestions}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
/>
</div>
<AutoSuggestInput
{...otherProps}
name={name}
value={value}
suggestions={suggestions}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
onInputBlur={this.onInputBlur}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
/>
);
}
}
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: ''
};

View File

@ -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;
}

View File

@ -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 (
<Reference>
{({ ref }) => {
if (renderInputComponent) {
return renderInputComponent(inputProps, ref);
}
return (
<div ref={ref}>
<input
{...inputProps}
/>
</div>
);
}}
</Reference>
);
}
renderSuggestionsContainer = ({ containerProps, children }) => {
return ReactDOM.createPortal(
<Popper
placement='bottom-start'
modifiers={{
computeStyle: {
fn: this.onComputeStyle
},
flip: {
padding: this.props.minHeight
}
}}
>
{({ ref: popperRef, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
return (
<div
ref={popperRef}
style={style}
className={children ? styles.suggestionsContainerOpen : undefined}
>
<div
{...containerProps}
style={{
maxHeight: style.maxHeight
}}
>
{children}
</div>
</div>
);
}}
</Popper>,
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 (
<Manager>
<Autosuggest
{...otherProps}
ref={forwardedRef}
id={name}
inputProps={inputProps}
theme={theme}
suggestions={suggestions}
getSuggestionValue={getSuggestionValue}
renderInputComponent={this.renderInputComponent}
renderSuggestionsContainer={this.renderSuggestionsContainer}
renderSuggestion={renderSuggestion}
onSuggestionSelected={onSuggestionSelected}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
/>
</Manager>
);
}
}
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;

View File

@ -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';
}

View File

@ -47,6 +47,7 @@ class DeviceInput extends Component {
render() {
const {
className,
name,
items,
selectedDevices,
hasError,
@ -58,7 +59,8 @@ class DeviceInput extends Component {
return (
<div className={className}>
<TagInput
className={styles.inputContainer}
inputContainerClassName={styles.input}
name={name}
tags={selectedDevices}
tagList={items}
allowNew={true}

View File

@ -1,7 +1,3 @@
.tether {
z-index: 2000;
}
.enhancedSelect {
composes: input from '~Components/Form/Input.css';
composes: link from '~Components/Link/Link.css';
@ -44,6 +40,7 @@
}
.optionsContainer {
z-index: $popperZIndex;
width: auto;
}

View File

@ -1,13 +1,14 @@
import _ from 'lodash';
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 classNames from 'classnames';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import isMobileUtil from 'Utilities/isMobile';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import { icons, scrollDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Portal from 'Components/Portal';
import Link from 'Components/Link/Link';
import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal';
@ -17,19 +18,6 @@ import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
import styles from './EnhancedSelectInput.css';
const tetherOptions = {
skipMoveElement: true,
constraints: [
{
to: 'window',
attachment: 'together',
pin: true
}
],
attachment: 'top left',
targetAttachment: 'bottom left'
};
function isArrowKey(keyCode) {
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
}
@ -87,8 +75,9 @@ class EnhancedSelectInput extends Component {
constructor(props, context) {
super(props, context);
this._buttonRef = {};
this._optionsRef = {};
this._scheduleUpdate = null;
this._buttonId = getUniqueElememtId();
this._optionsId = getUniqueElememtId();
this.state = {
isOpen: false,
@ -99,6 +88,10 @@ class EnhancedSelectInput extends Component {
}
componentDidUpdate(prevProps) {
if (this._scheduleUpdate) {
this._scheduleUpdate();
}
if (prevProps.value !== this.props.value) {
this.setState({
selectedIndex: getSelectedIndex(this.props)
@ -121,8 +114,8 @@ class EnhancedSelectInput extends Component {
// Listeners
onWindowClick = (event) => {
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 (
<div>
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions}
renderTarget={
(ref) => {
this._buttonRef = ref;
return (
<Manager>
<Reference>
{({ ref }) => (
<div
ref={ref}
id={this._buttonId}
>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<div ref={ref}>
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</div>
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</Measure>
);
}
}
renderElement={
(ref) => {
this._optionsRef = ref;
</div>
)}
</Reference>
<Portal>
<Popper placement="bottom-start">
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
if (!isOpen || isMobile) {
return;
}
return (
<div
ref={ref}
className={styles.optionsContainer}
style={{
minWidth: width
}}
>
<div className={styles.options}>
return (
<div
ref={ref}
id={this._optionsId}
className={styles.optionsContainer}
style={{
...style,
minWidth: width
}}
>
{
values.map((v, index) => {
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
{...v}
isMobile={false}
onSelect={this.onSelect}
>
{v.value}
</OptionComponent>
);
})
isOpen && !isMobile ?
<div className={styles.options}>
{
values.map((v, index) => {
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
{...v}
isMobile={false}
onSelect={this.onSelect}
>
{v.value}
</OptionComponent>
);
})
}
</div> :
null
}
</div>
</div>
);
}
}
/>
);
}
}
</Popper>
</Portal>
</Manager>
{
isMobile &&

View File

@ -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';

View File

@ -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 (
<div className={className}>
<Autosuggest
id={name}
inputProps={inputProps}
theme={theme}
<AutoSuggestInput
{...otherProps}
className={hasFileBrowser ? styles.hasFileBrowser : undefined}
name={name}
value={value}
suggestions={paths}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
onInputKeyDown={this.onInputKeyDown}
onInputBlur={this.onInputBlur}
onSuggestionSelected={this.onSuggestionSelected}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onChange={onChange}
/>
{
@ -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
};

View File

@ -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;
}

View File

@ -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 (
<TagInputInput
forwardedRef={forwardedRef}
tags={tags}
kind={kind}
inputProps={inputProps}
@ -208,10 +209,8 @@ class TagInput extends Component {
render() {
const {
className,
inputClassName,
placeholder,
hasError,
hasWarning
inputContainerClassName,
...otherProps
} = this.props;
const {
@ -220,48 +219,30 @@ class TagInput extends Component {
isFocused
} = this.state;
const inputProps = {
className: inputClassName,
name,
value,
placeholder,
autoComplete: 'off',
spellCheck: false,
onChange: this.onInputChange,
onKeyDown: this.onInputKeyDown,
onFocus: this.onInputFocus,
onBlur: this.onInputBlur
};
const theme = {
container: classNames(
className,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
),
containerOpen: styles.containerOpen,
suggestionsContainer: styles.suggestionsContainer,
suggestionsList: styles.suggestionsList,
suggestion: styles.suggestion,
suggestionHighlighted: styles.suggestionHighlighted
};
return (
<Autosuggest
ref={this._setAutosuggestRef}
id={name}
inputProps={inputProps}
theme={theme}
<AutoSuggestInput
{...otherProps}
forwardedRef={this._setAutosuggestRef}
className={styles.internalInput}
inputContainerClassName={classNames(
inputContainerClassName,
isFocused && styles.isFocused,
)}
value={value}
suggestions={suggestions}
getSuggestionValue={this.getSuggestionValue}
shouldRenderSuggestions={this.shouldRenderSuggestions}
focusInputOnSuggestionClick={false}
renderSuggestion={this.renderSuggestion}
renderInputComponent={this.renderInputComponent}
onInputChange={this.onInputChange}
onInputKeyDown={this.onInputKeyDown}
onInputFocus={this.onInputFocus}
onInputBlur={this.onInputBlur}
onSuggestionSelected={this.onSuggestionSelected}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onChange={this.onInputChange}
/>
);
}
@ -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: '',

View File

@ -1,4 +1,9 @@
.inputContainer {
position: absolute;
top: -1px;
right: -1px;
bottom: -1px;
left: -1px;
display: flex;
flex-wrap: wrap;
padding: 6px 16px;

View File

@ -23,6 +23,7 @@ class TagInputInput extends Component {
render() {
const {
forwardedRef,
className,
tags,
inputProps,
@ -33,6 +34,7 @@ class TagInputInput extends Component {
return (
<div
ref={forwardedRef}
className={className}
component="div"
onMouseDown={this.onMouseDown}
@ -59,6 +61,7 @@ class TagInputInput extends Component {
}
TagInputInput.propTypes = {
forwardedRef: PropTypes.func,
className: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
inputProps: PropTypes.object.isRequired,

View File

@ -1,7 +1,3 @@
.tether {
z-index: 2000;
}
.menu {
position: relative;
}

View File

@ -1,32 +1,31 @@
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 { align } from 'Helpers/Props';
import Portal from 'Components/Portal';
import styles from './Menu.css';
const baseTetherOptions = {
skipMoveElement: true,
constraints: [
{
to: 'window',
attachment: 'together',
pin: true
const sharedPopperOptions = {
modifiers: {
preventOverflow: {
padding: 0
},
flip: {
padding: 0
}
]
}
};
const tetherOptions = {
const popperOptions = {
[align.RIGHT]: {
...baseTetherOptions,
attachment: 'top right',
targetAttachment: 'bottom right'
...sharedPopperOptions,
placement: 'bottom-end'
},
[align.LEFT]: {
...baseTetherOptions,
attachment: 'top left',
targetAttachment: 'bottom left'
...sharedPopperOptions,
placement: 'bottom-start'
}
};
@ -38,8 +37,8 @@ class Menu extends Component {
constructor(props, context) {
super(props, context);
this._menuRef = {};
this._menuContentRef = {};
this._scheduleUpdate = null;
this._menuButtonId = getUniqueElememtId();
this.state = {
isMenuOpen: false,
@ -51,6 +50,12 @@ class Menu extends Component {
this.setMaxHeight();
}
componentDidUpdate() {
if (this._scheduleUpdate) {
this._scheduleUpdate();
}
}
componentWillUnmount() {
this._removeListener();
}
@ -63,13 +68,13 @@ class Menu extends Component {
return;
}
const menu = ReactDOM.findDOMNode(this._menuRef.current);
const menuButton = document.getElementById(this._menuButtonId);
if (!menu) {
if (!menuButton) {
return;
}
const { bottom } = menu.getBoundingClientRect();
const { bottom } = menuButton.getBoundingClientRect();
const maxHeight = window.innerHeight - bottom;
return maxHeight;
@ -106,14 +111,13 @@ class Menu extends Component {
// Listeners
onWindowClick = (event) => {
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 (
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions[alignMenu]}
renderTarget={
(ref) => {
this._menuRef = ref;
<Manager>
<Reference>
{({ ref }) => (
<div
ref={ref}
id={this._menuButtonId}
className={className}
>
{button}
</div>
)}
</Reference>
return (
<div
ref={ref}
className={className}
>
{button}
</div>
);
}
}
renderElement={
(ref) => {
this._menuContentRef = ref;
<Portal>
<Popper {...popperOptions[alignMenu]}>
{({ 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
}
);
}}
</Popper>
</Portal>
</Manager>
);
}
}

View File

@ -1,4 +1,5 @@
.menuContent {
z-index: $popperZIndex;
display: flex;
flex-direction: column;
background-color: $toolbarMenuItemBackgroundColor;

View File

@ -10,30 +10,37 @@ class MenuContent extends Component {
render() {
const {
forwardedRef,
className,
children,
maxHeight
style,
isOpen
} = this.props;
return (
<div
ref={forwardedRef}
className={className}
style={{
maxHeight: maxHeight ? `${maxHeight}px` : undefined
}}
style={style}
>
<Scroller className={styles.scroller}>
{children}
</Scroller>
{
isOpen ?
<Scroller className={styles.scroller}>
{children}
</Scroller> :
null
}
</div>
);
}
}
MenuContent.propTypes = {
forwardedRef: PropTypes.func,
className: PropTypes.string,
children: PropTypes.node.isRequired,
maxHeight: PropTypes.number
style: PropTypes.object,
isOpen: PropTypes.bool
};
MenuContent.defaultProps = {

View File

@ -1,7 +1,7 @@
.modalContainer {
position: absolute;
top: 0;
z-index: 1000;
z-index: $modalZIndex;
width: 100%;
height: 100%;
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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 (
<Tooltip
{...otherProps}
bodyClassName={styles.tooltipBody}
tooltip={
<div>
<div className={styles.title}>
{title}
</div>
[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 (
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions[position]}
renderTarget={
(ref) => (
<span
ref={ref}
className={className}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
{anchor}
</span>
)
}
renderElement={
(ref) => {
if (!this.state.isOpen) {
return null;
}
return (
<div
ref={ref}
className={styles.popoverContainer}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<div className={styles.popover}>
<div
className={classNames(
styles.arrow,
styles[position]
)}
/>
<div className={styles.title}>
{title}
</div>
<div className={styles.body}>
{body}
</div>
</div>
</div>
);
}
}
/>
);
}
<div className={styles.body}>
{body}
</div>
</div>
}
/>
);
}
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;

View File

@ -1,8 +1,5 @@
.tether {
z-index: 2000;
}
.tooltipContainer {
z-index: $popperZIndex;
margin: 10px 15px;
}

View File

@ -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 (
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions[position]}
renderTarget={
(ref) => (
<Manager>
<Reference>
{({ ref }) => (
<span
ref={ref}
className={className}
@ -116,50 +88,66 @@ class Tooltip extends Component {
>
{anchor}
</span>
)
}
renderElement={
(ref) => {
if (!this.state.isOpen) {
return;
}
)}
</Reference>
return (
<div
ref={ref}
className={styles.tooltipContainer}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<Portal>
<Popper
placement={position}
modifiers={{
preventOverflow: {
// Fixes positioning for tooltips in the queue
// and likely others.
escapeWithReference: true
}
}}
>
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
return (
<div
className={classNames(
styles.tooltip,
styles[kind]
)}
ref={ref}
className={styles.tooltipContainer}
style={style}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<div
className={classNames(
styles.arrow,
styles[kind],
styles[position]
)}
/>
{
this.state.isOpen ?
<div
className={classNames(
styles.tooltip,
styles[kind]
)}
>
<div
className={classNames(
styles.arrow,
styles[kind],
styles[position]
)}
/>
<div className={styles.body}>
{tooltip}
</div>
<div className={bodyClassName}>
{tooltip}
</div>
</div> :
null
}
</div>
</div>
);
}
}
/>
);
}}
</Popper>
</Portal>
</Manager>
);
}
}
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
};

View File

@ -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',

View File

@ -0,0 +1,4 @@
module.exports = {
modalZIndex: 1000,
popperZIndex: 2000
};

View File

@ -48,7 +48,7 @@
</head>
<body>
<div id="modal-root"></div>
<div id="portal-root"></div>
<div id="root" class="root"></div>
</body>

View File

@ -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",

View File

@ -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==