New: Replace react-tether with react-popper (#789)

This commit is contained in:
Qstick 2019-05-10 21:56:04 -04:00 committed by GitHub
parent 6ea7f97b16
commit cf40df7606
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 981 additions and 970 deletions

View File

@ -20,7 +20,8 @@ const cssVarsFiles = [
'../src/Styles/Variables/colors',
'../src/Styles/Variables/dimensions',
'../src/Styles/Variables/fonts',
'../src/Styles/Variables/animations'
'../src/Styles/Variables/animations',
'../src/Styles/Variables/zIndexes'
].map(require.resolve);
const plugins = [

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 ImportArtistSearchResultConnector from './ImportArtistSearchResultConnect
import ImportArtistName from './ImportArtistName';
import styles from './ImportArtistSelectArtist.css';
const tetherOptions = {
skipMoveElement: true,
constraints: [
{
to: 'window',
attachment: 'together',
pin: true
}
],
attachment: 'top center',
targetAttachment: 'bottom center'
};
class ImportArtistSelectArtist extends Component {
//
@ -34,6 +22,9 @@ class ImportArtistSelectArtist extends Component {
super(props, context);
this._artistLookupTimeout = null;
this._scheduleUpdate = null;
this._buttonId = getUniqueElememtId();
this._contentId = getUniqueElememtId();
this.state = {
term: props.id,
@ -41,17 +32,15 @@ class ImportArtistSelectArtist extends Component {
};
}
componentDidUpdate() {
if (this._scheduleUpdate) {
this._scheduleUpdate();
}
}
//
// Control
_setButtonRef = (ref) => {
this._buttonRef = ref;
}
_setContentRef = (ref) => {
this._contentRef = ref;
}
_addListener() {
window.addEventListener('click', this.onWindowClick);
}
@ -64,14 +53,18 @@ class ImportArtistSelectArtist extends Component {
// Listeners
onWindowClick = (event) => {
const button = ReactDOM.findDOMNode(this._buttonRef);
const content = ReactDOM.findDOMNode(this._contentRef);
const button = document.getElementById(this._buttonId);
const content = document.getElementById(this._contentId);
if (!button) {
if (!button || !content) {
return;
}
if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) {
if (
!button.contains(event.target) &&
!content.contains(event.target) &&
this.state.isOpen
) {
this.setState({ isOpen: false });
this._removeListener();
}
@ -129,130 +122,159 @@ class ImportArtistSelectArtist extends Component {
error.responseJSON.message;
return (
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions}
>
<Link
ref={this._setButtonRef}
className={styles.button}
component="div"
onPress={this.onPress}
>
{
isLookingUpArtist && isQueued && !isPopulated &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
isPopulated && selectedArtist && isExistingArtist &&
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
}
{
isPopulated && selectedArtist &&
<ImportArtistName
artistName={selectedArtist.artistName}
disambiguation={selectedArtist.disambiguation}
// year={selectedArtist.year}
isExistingArtist={isExistingArtist}
/>
}
{
isPopulated && !selectedArtist &&
<div className={styles.noMatches}>
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
No match found!
</div>
}
{
!isFetching && !!error &&
<div>
<Icon
className={styles.warningIcon}
title={errorMessage}
name={icons.WARNING}
kind={kinds.WARNING}
/>
Search failed, please try again later.
</div>
}
<div className={styles.dropdownArrowContainer}>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
{
this.state.isOpen &&
<Manager>
<Reference>
{({ ref }) => (
<div
ref={this._setContentRef}
className={styles.contentContainer}
ref={ref}
id={this._buttonId}
>
<div className={styles.content}>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon name={icons.SEARCH} />
</div>
<Link
ref={ref}
className={styles.button}
component="div"
onPress={this.onPress}
>
{
isLookingUpArtist && isQueued && !isPopulated ?
<LoadingIndicator
className={styles.loading}
size={20}
/> :
null
}
<TextInput
className={styles.searchInput}
name={`${name}_textInput`}
value={this.state.term}
onChange={this.onSearchInputChange}
{
isPopulated && selectedArtist && isExistingArtist ?
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/> :
null
}
{
isPopulated && selectedArtist ?
<ImportArtistName
artistName={selectedArtist.artistName}
disambiguation={selectedArtist.disambiguation}
// year={selectedArtist.year}
isExistingArtist={isExistingArtist}
/> :
null
}
{
isPopulated && !selectedArtist ?
<div className={styles.noMatches}>
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
No match found!
</div> :
null
}
{
!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}
/>
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
title="Refresh"
>
<Icon name={icons.REFRESH} />
</FormInputButton>
</div>
<div className={styles.results}>
{
items.map((item) => {
return (
<ImportArtistSearchResultConnector
key={item.foreignArtistId}
foreignArtistId={item.foreignArtistId}
artistName={item.artistName}
disambiguation={item.disambiguation}
// year={item.year}
onPress={this.onArtistSelect}
/>
);
})
}
</div>
</div>
</Link>
</div>
}
</TetherComponent>
)}
</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 (
<ImportArtistSearchResultConnector
key={item.foreignArtistId}
foreignArtistId={item.foreignArtistId}
artistName={item.artistName}
disambiguation={item.disambiguation}
// year={item.year}
onPress={this.onArtistSelect}
/>
);
})
}
</div>
</div> :
null
}
</div>
);
}}
</Popper>
</Portal>
</Manager>
);
}
}

View File

@ -5,6 +5,10 @@ import { kinds } from 'Helpers/Props';
import Label from 'Components/Label';
function getTooltip(title, quality, size) {
if (!title) {
return;
}
const revision = quality.revision;
if (revision.real && revision.real > 0) {

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,257 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import { Manager, Popper, Reference } from 'react-popper';
import classNames from 'classnames';
import Portal from 'Components/Portal';
import styles from './AutoSuggestInput.css';
class AutoSuggestInput extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._scheduleUpdate = null;
}
componentDidUpdate(prevProps) {
if (
this._scheduleUpdate &&
prevProps.suggestions !== this.props.suggestions
) {
this._scheduleUpdate();
}
}
//
// Control
renderInputComponent = (inputProps) => {
const { renderInputComponent } = this.props;
return (
<Reference>
{({ ref }) => {
if (renderInputComponent) {
return renderInputComponent(inputProps, ref);
}
return (
<div ref={ref}>
<input
{...inputProps}
/>
</div>
);
}}
</Reference>
);
}
renderSuggestionsContainer = ({ containerProps, children }) => {
return (
<Portal>
<Popper
placement='bottom-start'
modifiers={{
computeMaxHeight: {
order: 851,
enabled: true,
fn: this.onComputeMaxHeight
},
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>
</Portal>
);
}
//
// Listeners
onComputeMaxHeight = (data) => {
const {
top,
bottom,
width
} = data.offsets.reference;
const windowHeight = window.innerHeight;
if ((/^botton/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
}
data.styles.width = width;
return data;
}
onInputChange = (event, { newValue }) => {
this.props.onChange({
name: this.props.name,
value: newValue
});
}
onInputKeyDown = (event) => {
const {
name,
value,
suggestions,
onChange
} = this.props;
if (
event.key === 'Tab' &&
suggestions.length &&
suggestions[0] !== this.props.value
) {
event.preventDefault();
if (value) {
onChange({
name,
value: suggestions[0]
});
}
}
}
//
// Render
render() {
const {
forwardedRef,
className,
inputContainerClassName,
name,
value,
placeholder,
suggestions,
hasError,
hasWarning,
getSuggestionValue,
renderSuggestion,
onInputChange,
onInputKeyDown,
onInputFocus,
onInputBlur,
onSuggestionsFetchRequested,
onSuggestionsClearRequested,
onSuggestionSelected,
...otherProps
} = this.props;
const inputProps = {
className: classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
),
name,
value,
placeholder,
autoComplete: 'off',
spellCheck: false,
onChange: onInputChange || this.onInputChange,
onKeyDown: onInputKeyDown || this.onInputKeyDown,
onFocus: onInputFocus,
onBlur: onInputBlur
};
const theme = {
container: inputContainerClassName,
containerOpen: styles.suggestionsContainerOpen,
suggestionsContainer: styles.suggestionsContainer,
suggestionsList: styles.suggestionsList,
suggestion: styles.suggestion,
suggestionHighlighted: styles.suggestionHighlighted
};
return (
<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,10 +40,13 @@
}
.optionsContainer {
z-index: $popperZIndex;
width: auto;
}
.options {
composes: scroller from '~Components/Scroller/Scroller.css';
border: 1px solid $inputBorderColor;
border-radius: 4px;
background-color: $white;

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,6 +75,10 @@ class EnhancedSelectInput extends Component {
constructor(props, context) {
super(props, context);
this._scheduleUpdate = null;
this._buttonId = getUniqueElememtId();
this._optionsId = getUniqueElememtId();
this.state = {
isOpen: false,
selectedIndex: getSelectedIndex(props),
@ -96,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)
@ -106,14 +102,6 @@ class EnhancedSelectInput extends Component {
//
// Control
_setButtonRef = (ref) => {
this._buttonRef = ref;
}
_setOptionsRef = (ref) => {
this._optionsRef = ref;
}
_addListener() {
window.addEventListener('click', this.onWindowClick);
}
@ -125,9 +113,26 @@ class EnhancedSelectInput extends Component {
//
// Listeners
onComputeMaxHeight = (data) => {
const {
top,
bottom
} = data.offsets.reference;
const windowHeight = window.innerHeight;
if ((/^botton/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
}
return data;
}
onWindowClick = (event) => {
const button = ReactDOM.findDOMNode(this._buttonRef);
const options = ReactDOM.findDOMNode(this._optionsRef);
const button = document.getElementById(this._buttonId);
const options = document.getElementById(this._optionsId);
if (!button || this.state.isMobile) {
return;
@ -271,80 +276,110 @@ class EnhancedSelectInput extends Component {
return (
<div>
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions}
>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<Link
ref={this._setButtonRef}
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}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
<Manager>
<Reference>
{({ ref }) => (
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
ref={ref}
id={this._buttonId}
>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<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}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</Measure>
</div>
)}
</Reference>
<Portal>
<Popper
placement="bottom-start"
modifiers={{
computeMaxHeight: {
order: 851,
enabled: true,
fn: this.onComputeMaxHeight
}
>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</Measure>
}}
>
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
{
isOpen && !isMobile &&
<div
ref={this._setOptionsRef}
className={styles.optionsContainer}
style={{
minWidth: width
}}
>
<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}
return (
<div
ref={ref}
id={this._optionsId}
className={styles.optionsContainer}
style={{
...style,
minWidth: width
}}
>
{
isOpen && !isMobile ?
<Scroller
className={styles.options}
style={{
maxHeight: style.maxHeight
}}
>
{v.value}
</OptionComponent>
);
})
}
</div>
</div>
}
</TetherComponent>
{
values.map((v, index) => {
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
{...v}
isMobile={false}
onSelect={this.onSelect}
>
{v.value}
</OptionComponent>
);
})
}
</Scroller> :
null
}
</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,6 @@
.inputContainer {
composes: input from '~Components/Form/Input.css';
.input {
composes: input from '~./AutoSuggestInput.css';
position: relative;
padding: 0;
@ -13,20 +14,7 @@
}
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from '~Components/Form/Input.css';
}
.tags {
flex: 0 0 auto;
max-width: 100%;
}
.input {
.internalInput {
flex: 1 1 0%;
margin-left: 3px;
min-width: 20%;
@ -35,44 +23,3 @@
height: 21px;
border: none;
}
.suggestionsContainer {
@add-mixin scrollbar;
@add-mixin scrollbarTrack;
@add-mixin scrollbarThumb;
}
.containerOpen {
.suggestionsContainer {
position: absolute;
right: -1px;
left: -1px;
z-index: 1;
overflow-y: auto;
margin-top: 1px;
max-height: 110px;
border: 1px solid $inputBorderColor;
border-radius: 4px;
background-color: $white;
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
}
}
.suggestionsList {
margin: 5px 0;
padding-left: 0;
list-style-type: none;
}
.suggestion {
padding: 0 16px;
cursor: default;
&:hover {
background-color: $menuItemHoverBackgroundColor;
}
}
.suggestionHighlighted {
background-color: $menuItemHoverBackgroundColor;
}

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,6 +37,9 @@ class Menu extends Component {
constructor(props, context) {
super(props, context);
this._scheduleUpdate = null;
this._menuButtonId = getUniqueElememtId();
this.state = {
isMenuOpen: false,
maxHeight: 0
@ -48,6 +50,12 @@ class Menu extends Component {
this.setMaxHeight();
}
componentDidUpdate() {
if (this._scheduleUpdate) {
this._scheduleUpdate();
}
}
componentWillUnmount() {
this._removeListener();
}
@ -60,22 +68,26 @@ class Menu extends Component {
return;
}
const menu = ReactDOM.findDOMNode(this.refs.menu);
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;
}
setMaxHeight() {
this.setState({
maxHeight: this.getMaxHeight()
});
const maxHeight = this.getMaxHeight();
if (maxHeight !== this.state.maxHeight) {
this.setState({
maxHeight
});
}
}
_addListener() {
@ -99,14 +111,13 @@ class Menu extends Component {
// Listeners
onWindowClick = (event) => {
const menu = ReactDOM.findDOMNode(this.refs.menu);
const menuContent = ReactDOM.findDOMNode(this.refs.menuContent);
const menuButton = document.getElementById(this._menuButtonId);
if (!menu) {
if (!menuButton) {
return;
}
if ((!menu.contains(event.target) || menuContent.contains(event.target)) && this.state.isMenuOpen) {
if (!menuButton.contains(event.target) && this.state.isMenuOpen) {
this.setState({ isMenuOpen: false });
this._removeListener();
}
@ -116,8 +127,10 @@ class Menu extends Component {
this.setMaxHeight();
}
onWindowScroll = () => {
this.setMaxHeight();
onWindowScroll = (event) => {
if (this.state.isMenuOpen) {
this.setMaxHeight();
}
}
onMenuButtonPress = () => {
@ -158,35 +171,40 @@ class Menu extends Component {
}
);
const content = React.cloneElement(
childrenArray[1],
{
ref: 'menuContent',
alignMenu,
maxHeight,
isOpen: isMenuOpen
}
);
return (
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions[alignMenu]}
>
<div
ref="menu"
className={className}
>
{button}
</div>
<Manager>
<Reference>
{({ ref }) => (
<div
ref={ref}
id={this._menuButtonId}
className={className}
>
{button}
</div>
)}
</Reference>
{
isMenuOpen &&
content
}
</TetherComponent>
<Portal>
<Popper {...popperOptions[alignMenu]}>
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
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,5 +1,6 @@
.separator {
overflow: hidden;
min-height: 1px;
height: 1px;
background-color: $themeDarkColor;
}

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,160 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TetherComponent from 'react-tether';
import classNames from 'classnames';
import isMobileUtil from 'Utilities/isMobile';
import { tooltipPositions } from 'Helpers/Props';
import React from 'react';
import Tooltip from './Tooltip';
import styles from './Popover.css';
const baseTetherOptions = {
skipMoveElement: true,
constraints: [
{
to: 'window',
attachment: 'together',
pin: true
}
]
};
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]}
>
<span
className={className}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
{anchor}
</span>
{
this.state.isOpen &&
<div
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>
}
</TetherComponent>
);
}
<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,55 +76,81 @@ class Tooltip extends Component {
} = this.props;
return (
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions[position]}
>
<span
className={className}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
{anchor}
</span>
{
this.state.isOpen &&
<div
className={styles.tooltipContainer}
<Manager>
<Reference>
{({ ref }) => (
<span
ref={ref}
className={className}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<div
className={classNames(
styles.tooltip,
styles[kind]
)}
>
<div
className={classNames(
styles.arrow,
styles[kind],
styles[position]
)}
/>
{anchor}
</span>
)}
</Reference>
<div className={styles.body}>
{tooltip}
<Portal>
<Popper
placement={position}
// Disable events to improve performance when many tooltips
// are shown (Quality Definitions for example).
eventsEnabled={false}
modifiers={{
preventOverflow: {
// Fixes positioning for tooltips in the queue
// and likely others.
escapeWithReference: true
}
}}
>
{({ ref, style, placement, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
return (
<div
ref={ref}
className={styles.tooltipContainer}
style={style}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
{
this.state.isOpen ?
<div
className={classNames(
styles.tooltip,
styles[kind]
)}
>
<div
className={classNames(
styles.arrow,
styles[kind],
styles[placement.split('-')[0]]
)}
/>
<div className={bodyClassName}>
{tooltip}
</div>
</div> :
null
}
</div>
</div>
</div>
}
</TetherComponent>
);
}}
</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]),
@ -156,6 +158,7 @@ Tooltip.propTypes = {
};
Tooltip.defaultProps = {
bodyClassName: styles.body,
kind: kinds.DEFAULT,
position: tooltipPositions.TOP
};

View File

@ -238,6 +238,7 @@ class InteractiveImportRow extends Component {
<TableRowCellButton
isDisabled={!allowArtistChange}
title={allowArtistChange ? 'Click to change artist' : undefined}
onPress={this.onSelectArtistPress}
>
{
@ -247,6 +248,7 @@ class InteractiveImportRow extends Component {
<TableRowCellButton
isDisabled={!artist}
title={artist ? 'Click to change album' : undefined}
onPress={this.onSelectAlbumPress}
>
{
@ -256,6 +258,7 @@ class InteractiveImportRow extends Component {
<TableRowCellButton
isDisabled={!artist || !album}
title={artist && album ? 'Click to change track' : undefined}
onPress={this.onSelectTrackPress}
>
{
@ -268,6 +271,7 @@ class InteractiveImportRow extends Component {
<TableRowCellButton
className={styles.quality}
title="Click to change quality"
onPress={this.onSelectQualityPress}
>
{
@ -286,6 +290,7 @@ class InteractiveImportRow extends Component {
<TableRowCellButton
className={styles.language}
title="Click to change language"
onPress={this.onSelectLanguagePress}
>
{

View File

@ -3,10 +3,12 @@ import React, { Component } from 'react';
import ReactSlider from 'react-slider';
import formatBytes from 'Utilities/Number/formatBytes';
import roundNumber from 'Utilities/Number/roundNumber';
import { kinds } from 'Helpers/Props';
import { kinds, tooltipPositions } from 'Helpers/Props';
import Label from 'Components/Label';
import NumberInput from 'Components/Form/NumberInput';
import TextInput from 'Components/Form/TextInput';
import Popover from 'Components/Tooltip/Popover';
import QualityDefinitionLimits from './QualityDefinitionLimits';
import styles from './QualityDefinition.css';
const MIN = 0;
@ -141,13 +143,8 @@ class QualityDefinition extends Component {
const minBytes = minSize * 128;
const maxBytes = maxSize && maxSize * 128;
// Calculates the bytes used by a twenty minute EP
const minTwenty = formatBytes(minBytes * 20 * 60, 2);
const maxTwenty = maxBytes ? formatBytes(maxBytes * 20 * 60, 2) : 'Unlimited';
// Calculates the bytes used by a forty-five minute LP
const minFortyFive = formatBytes(minBytes * 45 * 60, 2);
const maxFortyFive = maxBytes ? formatBytes(maxBytes * 45 * 60, 2) : 'Unlimited';
const minRate = `${formatBytes(minBytes, true)}/s`;
const maxRate = maxBytes ? `${formatBytes(maxBytes, true)}/s` : 'Unlimited';
return (
<div className={styles.qualityDefinition}>
@ -181,13 +178,35 @@ class QualityDefinition extends Component {
<div className={styles.sizes}>
<div>
<Label title={'Minimum size for a 20 minute EP'} kind={kinds.WARNING}>{minTwenty}</Label>
<Label title={'Minimum size for a 45 minute LP'} kind={kinds.INFO}>{minFortyFive}</Label>
<Popover
anchor={
<Label kind={kinds.INFO}>{minRate}</Label>
}
title="Minimum Limits"
body={
<QualityDefinitionLimits
bytes={minBytes}
message="No minimum for any runtime"
/>
}
position={tooltipPositions.BOTTOM}
/>
</div>
<div>
<Label title={'Maximum size for a 20 minute EP'} kind={kinds.WARNING}>{maxTwenty}</Label>
<Label title={'Maximum size for a 45 minute LP'} kind={kinds.INFO}>{maxFortyFive}</Label>
<Popover
anchor={
<Label kind={kinds.WARNING}>{maxRate}</Label>
}
title="Maximum Limits"
body={
<QualityDefinitionLimits
bytes={maxBytes}
message="No limit for any runtime"
/>
}
position={tooltipPositions.BOTTOM}
/>
</div>
</div>
</div>

View File

@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import React from 'react';
import formatBytes from 'Utilities/Number/formatBytes';
function QualityDefinitionLimits(props) {
const {
bytes,
message
} = props;
if (!bytes) {
return <div>{message}</div>;
}
const twenty = formatBytes(bytes * 20 * 60);
const fourtyFive = formatBytes(bytes * 45 * 60);
const sixty = formatBytes(bytes * 60 * 60);
return (
<div>
<div>20 Minutes: {twenty}</div>
<div>45 Minutes: {fourtyFive}</div>
<div>60 Minutes: {sixty}</div>
</div>
);
}
QualityDefinitionLimits.propTypes = {
bytes: PropTypes.number,
message: PropTypes.string.isRequired
};
export default QualityDefinitionLimits;

View File

@ -164,7 +164,7 @@ module.exports = {
popoverTitleBackgroundColor: '#f7f7f7',
popoverTitleBorderColor: '#ebebeb',
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
popoverArrowBorderColor: 'rgba(0, 0, 0, 0.25)',
popoverArrowBorderColor: '#fff',
popoverTitleBackgroundInverseColor: '#3a3f51',
popoverTitleBorderInverseColor: '#353535',

View File

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

View File

@ -1,6 +1,6 @@
import filesize from 'filesize';
function formatBytes(input) {
function formatBytes(input, showBits = false) {
const size = Number(input);
if (isNaN(size)) {
@ -9,7 +9,8 @@ function formatBytes(input) {
return filesize(size, {
base: 2,
round: 1
round: 1,
bits: showBits
});
}

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

@ -99,10 +99,10 @@
"react-google-recaptcha": "1.0.5",
"react-lazyload": "2.5.0",
"react-measure": "1.4.7",
"react-popper": "1.3.3",
"react-redux": "6.0.1",
"react-router-dom": "4.3.1",
"react-slider": "0.11.2",
"react-tether": "1.0.4",
"react-text-truncate": "0.14.0",
"react-virtualized": "9.21.0",
"redux": "4.0.1",

View File

@ -2371,6 +2371,14 @@ create-react-class@15.6.3:
loose-envify "^1.3.1"
object-assign "^4.1.1"
create-react-context@<=0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca"
integrity sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==
dependencies:
fbjs "^0.8.0"
gud "^1.0.0"
cross-spawn@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
@ -3427,7 +3435,7 @@ fb-watchman@^2.0.0:
dependencies:
bser "^2.0.0"
fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.9:
fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.9:
version "0.8.17"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
@ -3917,6 +3925,11 @@ graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, g
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
gulp-cached@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/gulp-cached/-/gulp-cached-1.1.1.tgz#fe7cd4f87f37601e6073cfedee5c2bdaf8b6acce"
@ -6273,6 +6286,11 @@ plugin-error@^0.1.2:
arr-union "^2.0.1"
extend-shallow "^1.1.2"
popper.js@^1.14.4:
version "1.15.0"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -7095,6 +7113,18 @@ react-measure@1.4.7:
prop-types "^15.5.4"
resize-observer-polyfill "^1.4.1"
react-popper@1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6"
integrity sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==
dependencies:
"@babel/runtime" "^7.1.2"
create-react-context "<=0.2.2"
popper.js "^1.14.4"
prop-types "^15.6.1"
typed-styles "^0.0.7"
warning "^4.0.2"
react-redux@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"
@ -7145,14 +7175,6 @@ react-slider@0.11.2:
resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-0.11.2.tgz#ae014e1454c3cdd5f28b5c2495b2a08abd9971e6"
integrity sha512-y49ZwJJ7OcPdihgt71xYI8GRdAzpFuSLQR8b+cKotutxqf8MAEPEtqvWKlg+3ZQRe5PMN6oWbIb7wEYDF8XhNQ==
react-tether@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/react-tether/-/react-tether-1.0.4.tgz#a1f0973391ba4c3c2b5c38d5be16c42f0fd96759"
integrity sha512-WQ3Ulj9k6to8We/rUqkX4fB5L4jYGnUEXtAxyth9kcKqf0miVOR6MYS3hJodQbpNIBB5DvA+/ZH8nlUtMupSVA==
dependencies:
prop-types "^15.6.2"
tether "^1.4.5"
react-text-truncate@0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/react-text-truncate/-/react-text-truncate-0.14.0.tgz#f33319804459f429b55bf13784de4f7125c9bba3"
@ -8384,11 +8406,6 @@ terser@^3.16.1:
source-map "~0.6.1"
source-map-support "~0.5.9"
tether@^1.4.5:
version "1.4.5"
resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.5.tgz#8efd7b35572767ba502259ba9b1cc167fcf6f2c1"
integrity sha512-fysT1Gug2wbRi7a6waeu39yVDwiNtvwj5m9eRD+qZDSHKNghLo6KqP/U3yM2ap6TNUL2skjXGJaJJTJqoC31vw==
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -8590,6 +8607,11 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
typed-styles@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@ -9007,7 +9029,7 @@ warning@^3.0.0:
dependencies:
loose-envify "^1.0.0"
warning@^4.0.1:
warning@^4.0.1, warning@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==