Align tag inputs with other input boxes (#2965)

* Wrap tag input component

* Postcss nested selectors

* Chips has same size as in ui

* Input matches with sizes/paddings of others

* Adjust colours, move hint text

* Added ChipInput from search in wrapper

* Using InputChip Wrapper in search (#2965)
This commit is contained in:
Jaco Greeff 2016-10-31 23:22:22 +01:00 committed by GitHub
parent 04432b2766
commit 381af547fa
10 changed files with 238 additions and 152 deletions

View File

@ -92,6 +92,7 @@
"nock": "^8.0.0", "nock": "^8.0.0",
"postcss-import": "^8.1.2", "postcss-import": "^8.1.2",
"postcss-loader": "^0.8.1", "postcss-loader": "^0.8.1",
"postcss-nested": "^1.0.0",
"postcss-simple-vars": "^3.0.0", "postcss-simple-vars": "^3.0.0",
"react-addons-test-utils": "^15.3.0", "react-addons-test-utils": "^15.3.0",
"react-copy-to-clipboard": "^4.2.3", "react-copy-to-clipboard": "^4.2.3",

View File

@ -17,10 +17,8 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import ContentSave from 'material-ui/svg-icons/content/save'; import ContentSave from 'material-ui/svg-icons/content/save';
// import ChipInput from 'material-ui-chip-input';
import ChipInput from 'material-ui-chip-input/src/ChipInput';
import { Button, Form, Input, Modal } from '../../ui'; import { Button, Form, Input, InputChip, Modal } from '../../ui';
import { validateName } from '../../util/validation'; import { validateName } from '../../util/validation';
export default class EditMeta extends Component { export default class EditMeta extends Component {
@ -104,53 +102,20 @@ export default class EditMeta extends Component {
const { tags } = meta || []; const { tags } = meta || [];
return ( return (
<ChipInput <InputChip
ref='tagsInput' tokens={ tags }
value={ tags } onTokensChange={ this.onTagsChange }
onRequestAdd={ this.onAddTag } label='(optional) tags'
onRequestDelete={ this.onDeleteTag } hint='press <Enter> to add a tag'
floatingLabelText='(optional) tags' clearOnBlur
hintText='press <Enter> to add a tag'
onUpdateInput={ this.onTagsInputChange }
floatingLabelFixed
fullWidth
/> />
); );
} }
onAddTag = (tag) => { onTagsChange = (newTags) => {
const { meta } = this.state;
const { tags } = meta || [];
this.onMetaChange('tags', [].concat(tags, tag));
}
onDeleteTag = (tag) => {
const { meta } = this.state;
const { tags } = meta || [];
const newTags = tags
.filter(t => t !== tag);
this.onMetaChange('tags', newTags); this.onMetaChange('tags', newTags);
} }
onTagsInputChange = (value) => {
const { meta } = this.state;
const { tags = [] } = meta;
const tokens = value.split(/[\s,;]+/);
const newTokens = tokens
.slice(0, -1)
.filter(t => t.length > 0);
const inputValue = tokens.slice(-1)[0].trim();
this.onMetaChange('tags', [].concat(tags, newTokens));
this.refs.tagsInput.setState({ inputValue });
}
onNameChange = (name) => { onNameChange = (name) => {
this.setState(validateName(name)); this.setState(validateName(name));
} }

View File

@ -41,11 +41,3 @@
width: 0; width: 0;
height: 0; height: 0;
} }
.chip > svg {
width: 1.2rem !important;
height: 1.2rem !important;
margin: initial !important;
margin-right: 4px !important;
padding: 4px 0 !important;
}

View File

@ -15,14 +15,9 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Chip } from 'material-ui';
import { blue300 } from 'material-ui/styles/colors';
// import ChipInput from 'material-ui-chip-input';
import ChipInput from 'material-ui-chip-input/src/ChipInput';
import ActionSearch from 'material-ui/svg-icons/action/search'; import ActionSearch from 'material-ui/svg-icons/action/search';
import { uniq } from 'lodash';
import { Button } from '../../'; import { Button, InputChip } from '../../';
import styles from './search.css'; import styles from './search.css';
@ -74,28 +69,14 @@ export default class ActionbarSearch extends Component {
className={ styles.searchcontainer } className={ styles.searchcontainer }
key='searchAccount'> key='searchAccount'>
<div className={ inputContainerClasses.join(' ') }> <div className={ inputContainerClasses.join(' ') }>
<ChipInput <InputChip
clearOnBlur={ false }
className={ styles.input } className={ styles.input }
chipRenderer={ this.chipRenderer } hint='Enter search input...'
hintText='Enter search input...' tokens={ tokens }
ref='searchInput'
value={ tokens }
onBlur={ this.handleSearchBlur } onBlur={ this.handleSearchBlur }
onRequestAdd={ this.handleTokenAdd } onInputChange={ this.handleInputChange }
onRequestDelete={ this.handleTokenDelete } onTokensChange={ this.handleTokensChange }
onUpdateInput={ this.handleInputChange }
hintStyle={ {
bottom: 16,
left: 2,
transition: 'none'
} }
inputStyle={ {
marginBottom: 18
} }
textFieldStyle={ {
height: 42
} }
/> />
</div> </div>
@ -108,72 +89,13 @@ export default class ActionbarSearch extends Component {
); );
} }
chipRenderer = (state, key) => { handleTokensChange = (tokens) => {
const { value, isFocused, isDisabled, handleClick, handleRequestDelete } = state; this.handleSearchChange(tokens);
return (
<Chip
key={ key }
className={ styles.chip }
style={ {
margin: '8px 8px 0 0',
float: 'left',
pointerEvents: isDisabled ? 'none' : undefined,
alignItems: 'center'
} }
labelStyle={ {
paddingRight: 6,
fontSize: '0.9rem',
lineHeight: 'initial'
} }
backgroundColor={ isFocused ? blue300 : 'rgba(0, 0, 0, 0.73)' }
onTouchTap={ handleClick }
onRequestDelete={ handleRequestDelete }
>
{ value }
</Chip>
);
} }
handleTokenAdd = (value) => { handleInputChange = (inputValue) => {
const { tokens } = this.props;
const { inputValue } = this.state;
const newSearchTokens = uniq([].concat(tokens, value));
this.setState({
inputValue: inputValue === value ? '' : inputValue
}, () => {
this.handleSearchChange(newSearchTokens);
});
}
handleTokenDelete = (value) => {
const { tokens } = this.props;
const newSearchTokens = []
.concat(tokens)
.filter(v => v !== value);
this.handleSearchChange(newSearchTokens);
this.refs.searchInput.focus();
}
handleInputChange = (value) => {
const splitTokens = value.split(/[\s,;]/);
const inputValue = (splitTokens.length <= 1)
? value
: splitTokens.slice(-1)[0].trim();
this.refs.searchInput.setState({ inputValue });
this.setState({ inputValue }, () => { this.setState({ inputValue }, () => {
if (splitTokens.length > 1) { this.handleSearchChange();
const tokensToAdd = splitTokens.slice(0, -1);
tokensToAdd.forEach(token => this.handleTokenAdd(token));
} else {
this.handleSearchChange();
}
}); });
} }
@ -182,12 +104,10 @@ export default class ActionbarSearch extends Component {
const { inputValue } = this.state; const { inputValue } = this.state;
const newSearchTokens = [] const newSearchTokens = []
.concat(searchTokens || tokens) .concat(searchTokens || tokens);
.filter(v => v.length > 0);
const newSearchValues = [] const newSearchValues = []
.concat(searchTokens || tokens, inputValue) .concat(searchTokens || tokens, inputValue);
.filter(v => v.length > 0);
onChange(newSearchTokens, newSearchValues); onChange(newSearchTokens, newSearchValues);
} }
@ -214,19 +134,15 @@ export default class ActionbarSearch extends Component {
} }
handleOpenSearch = (showSearch, force) => { handleOpenSearch = (showSearch, force) => {
if (this.state.stateChanging && !force) return false; if (this.state.stateChanging && !force) {
return false;
}
this.setState({ this.setState({
showSearch: showSearch, showSearch: showSearch,
stateChanging: true stateChanging: true
}); });
if (showSearch) {
this.refs.searchInput.focus();
} else {
this.refs.searchInput.getInputNode().blur();
}
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {
this.setState({ stateChanging: false }); this.setState({ stateChanging: false });
}, 450); }, 450);

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './inputChip';

View File

@ -0,0 +1,26 @@
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.chip {
& > svg {
width: 1.2rem !important;
height: 1.2rem !important;
margin: initial !important;
margin-right: 4px !important;
padding: 4px 0 !important;
}
}

View File

@ -0,0 +1,164 @@
// Copyright 2015, 2016 Ethcore (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { Chip } from 'material-ui';
import ChipInput from 'material-ui-chip-input';
import { blue300 } from 'material-ui/styles/colors';
import { uniq } from 'lodash';
import styles from './inputChip.css';
export default class InputChip extends Component {
static propTypes = {
tokens: PropTypes.array.isRequired,
className: PropTypes.string,
hint: PropTypes.string,
label: PropTypes.string,
onTokensChange: PropTypes.func,
onInputChange: PropTypes.func,
onBlur: PropTypes.func,
clearOnBlur: PropTypes.bool
}
static defaultProps = {
clearOnBlur: false
}
render () {
const { clearOnBlur, className, hint, label, tokens } = this.props;
const classes = `${className}`;
return (
<ChipInput
className={ classes }
ref='chipInput'
value={ tokens }
clearOnBlur={ clearOnBlur }
floatingLabelText={ label }
hintText={ hint }
chipRenderer={ this.chipRenderer }
onBlur={ this.handleBlur }
onRequestAdd={ this.handleTokenAdd }
onRequestDelete={ this.handleTokenDelete }
onUpdateInput={ this.handleInputChange }
floatingLabelFixed
fullWidth
hintStyle={ {
bottom: 16,
left: 1,
transition: 'none'
} }
inputStyle={ {
marginBottom: 18
} }
textFieldStyle={ {
height: 42
} } />
);
}
chipRenderer = (state, key) => {
const { value, isFocused, isDisabled, handleClick, handleRequestDelete } = state;
return (
<Chip
key={ key }
className={ styles.chip }
style={ {
margin: '8px 8px 0 0',
float: 'left',
pointerEvents: isDisabled ? 'none' : undefined,
alignItems: 'center'
} }
labelStyle={ {
paddingRight: 6,
fontSize: '0.9rem',
lineHeight: 'initial'
} }
backgroundColor={ isFocused ? blue300 : 'rgba(50, 50, 50, 0.73)' }
onTouchTap={ handleClick }
onRequestDelete={ handleRequestDelete }
>
{ value }
</Chip>
);
}
handleBlur = () => {
const { onBlur } = this.props;
if (typeof onBlur === 'function') {
onBlur();
}
}
handleTokenAdd = (value) => {
const { tokens, onInputChange } = this.props;
const newTokens = uniq([].concat(tokens, value));
this.handleTokensChange(newTokens);
if (value === this.refs.chipInput.state.inputValue && typeof onInputChange === 'function') {
onInputChange('');
}
}
handleTokenDelete = (value) => {
const { tokens } = this.props;
const newTokens = uniq([]
.concat(tokens)
.filter(v => v !== value));
this.handleTokensChange(newTokens);
this.refs.chipInput.focus();
}
handleInputChange = (value) => {
const { onInputChange } = this.props;
const splitTokens = value.split(/[\s,;]/);
const inputValue = (splitTokens.length <= 1)
? value
: splitTokens.slice(-1)[0].trim();
this.refs.chipInput.setState({ inputValue });
if (splitTokens.length > 1) {
const tokensToAdd = splitTokens.slice(0, -1);
tokensToAdd.forEach(token => this.handleTokenAdd(token));
}
if (typeof onInputChange === 'function') {
onInputChange(inputValue);
}
}
handleTokensChange = (tokens) => {
const { onTokensChange } = this.props;
onTokensChange(tokens.filter(token => token && token.length > 0));
}
}

View File

@ -19,6 +19,7 @@ import FormWrap from './FormWrap';
import Input from './Input'; import Input from './Input';
import InputAddress from './InputAddress'; import InputAddress from './InputAddress';
import InputAddressSelect from './InputAddressSelect'; import InputAddressSelect from './InputAddressSelect';
import InputChip from './InputChip';
import InputInline from './InputInline'; import InputInline from './InputInline';
import Select from './Select'; import Select from './Select';
@ -29,6 +30,7 @@ export {
Input, Input,
InputAddress, InputAddress,
InputAddressSelect, InputAddressSelect,
InputChip,
InputInline, InputInline,
Select Select
}; };

View File

@ -25,7 +25,7 @@ import ConfirmDialog from './ConfirmDialog';
import Container, { Title as ContainerTitle } from './Container'; import Container, { Title as ContainerTitle } from './Container';
import ContextProvider from './ContextProvider'; import ContextProvider from './ContextProvider';
import Errors from './Errors'; import Errors from './Errors';
import Form, { AddressSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputInline, Select } from './Form'; import Form, { AddressSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select } from './Form';
import IdentityIcon from './IdentityIcon'; import IdentityIcon from './IdentityIcon';
import IdentityName from './IdentityName'; import IdentityName from './IdentityName';
import MethodDecoding from './MethodDecoding'; import MethodDecoding from './MethodDecoding';
@ -57,6 +57,7 @@ export {
Input, Input,
InputAddress, InputAddress,
InputAddressSelect, InputAddressSelect,
InputChip,
InputInline, InputInline,
Select, Select,
IdentityIcon, IdentityIcon,

View File

@ -17,6 +17,7 @@
const HappyPack = require('happypack'); const HappyPack = require('happypack');
const path = require('path'); const path = require('path');
const postcssImport = require('postcss-import'); const postcssImport = require('postcss-import');
const postcssNested = require('postcss-nested');
const postcssVars = require('postcss-simple-vars'); const postcssVars = require('postcss-simple-vars');
const rucksack = require('rucksack-css'); const rucksack = require('rucksack-css');
const webpack = require('webpack'); const webpack = require('webpack');
@ -113,6 +114,7 @@ module.exports = {
postcssImport({ postcssImport({
addDependencyTo: webpack addDependencyTo: webpack
}), }),
postcssNested({}),
postcssVars({ postcssVars({
unknown: function (node, name, result) { unknown: function (node, name, result) {
node.warn(result, `Unknown variable ${name}`); node.warn(result, `Unknown variable ${name}`);