New Address Selector Component (#3829)
* WIP new address selector * WIP Working New Account Selector * WIP Fully working Addres Selector (missing keyboards navigation) * WIP Address Selector * Fully functionnal new Address Selector! * Implement disabled prop * Don't auto-open on focus + Text Selection fix * Add copy address capabilities * Better Address Selector Focus * Search from tags * [Address Selector] Better Focus // Parity Background * Linting * [Adress Selector] Better focused input style * Better focus support for inputs * Fix style issue * Add tags to accounts * linting * Add label to address selector * Removed old address selector + improved styling * Fixing address selection issues * fix test * Better logs... * PR Grumbles Part 1 * PR Grumbles Part 2 * PR Grumbles Part 3.1 * PR Grumbles Part 3.2 * PR Grumbles Part 3.3 * New Portal Component * Simpler Transition for Portal * PR Grumbles Part 4 * Align font-family with rest of UI * Fix null value input * Fix Webpack build...
This commit is contained in:
committed by
Gav Wood
parent
4e51f93176
commit
1ffc6ac58c
@@ -14,53 +14,110 @@
|
||||
/* You should have received a copy of the GNU General Public License
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
.account {
|
||||
padding: 0.25em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
text-transform: uppercase;
|
||||
padding: 0 0 0 1em;
|
||||
}
|
||||
.input {
|
||||
box-sizing: border-box;
|
||||
appearance: textfield;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
.balance {
|
||||
color: #aaa;
|
||||
padding-left: 1em;
|
||||
}
|
||||
transition-property: font-size, padding;
|
||||
transition-duration: 0.5s;
|
||||
transition-timing-function: cubic-bezier(0.7,0,0.3,1);
|
||||
|
||||
.image {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
color: white;
|
||||
font-family: inherit;
|
||||
font-size: 2em;
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 35px;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&.noLabel {
|
||||
top: 11px;
|
||||
&::placeholder {
|
||||
color: #a2a2a2;
|
||||
}
|
||||
}
|
||||
|
||||
.paddedInput input {
|
||||
padding-left: 46px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
.inputAddress {
|
||||
position: relative;
|
||||
|
||||
&:hover, *:hover {
|
||||
cursor: text !important;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
min-height: 0 !important;
|
||||
line-height: inherit !important;
|
||||
.main {
|
||||
position: relative;
|
||||
left: 0;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 1rem 2.5rem 0.25em;
|
||||
color: rgba(255, 255, 255, 0.498039);
|
||||
}
|
||||
|
||||
.underline {
|
||||
position: relative;
|
||||
margin: 0 9rem 0 2.5rem;
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.input {
|
||||
font-size: 1.5em;
|
||||
padding: 0 9rem 0.5em 2.5rem;
|
||||
display: block;
|
||||
|
||||
padding-right: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.categories {
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
margin: 2rem 2rem 0;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 0.5em;
|
||||
max-width: 35em;
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
font-size: 1.5em;
|
||||
font-color: white;
|
||||
}
|
||||
|
||||
.cards {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,277 +14,599 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { isEqual, pick } from 'lodash';
|
||||
import { MenuItem } from 'material-ui';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import keycode, { codes } from 'keycode';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { fromWei } from '~/api/util/wei';
|
||||
import { nodeOrStringProptype } from '~/util/proptypes';
|
||||
import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline';
|
||||
|
||||
import AutoComplete from '../AutoComplete';
|
||||
import IdentityIcon from '../../IdentityIcon';
|
||||
import IdentityName from '../../IdentityName';
|
||||
import AccountCard from '~/ui/AccountCard';
|
||||
import InputAddress from '~/ui/Form/InputAddress';
|
||||
import Portal from '~/ui/Portal';
|
||||
import { validateAddress } from '~/util/validation';
|
||||
|
||||
import styles from './addressSelect.css';
|
||||
|
||||
export default class AddressSelect extends Component {
|
||||
const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' };
|
||||
|
||||
// Current Form ID
|
||||
let currentId = 1;
|
||||
|
||||
class AddressSelect extends Component {
|
||||
static contextTypes = {
|
||||
api: PropTypes.object.isRequired
|
||||
}
|
||||
muiTheme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
// Required props
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
// Redux props
|
||||
accountsInfo: PropTypes.object,
|
||||
accounts: PropTypes.object,
|
||||
allowInput: PropTypes.bool,
|
||||
balances: PropTypes.object,
|
||||
contacts: PropTypes.object,
|
||||
contracts: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
error: nodeOrStringProptype(),
|
||||
hint: nodeOrStringProptype(),
|
||||
label: nodeOrStringProptype(),
|
||||
tokens: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
wallets: PropTypes.object
|
||||
}
|
||||
wallets: PropTypes.object,
|
||||
|
||||
// Optional props
|
||||
allowInput: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
hint: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: ''
|
||||
};
|
||||
|
||||
state = {
|
||||
autocompleteEntries: [],
|
||||
entries: {},
|
||||
addresses: [],
|
||||
value: ''
|
||||
}
|
||||
|
||||
// Cache autocomplete items
|
||||
items = {}
|
||||
|
||||
entriesFromProps (props = this.props) {
|
||||
const { accounts = {}, contacts = {}, contracts = {}, wallets = {} } = props;
|
||||
|
||||
const autocompleteEntries = [].concat(
|
||||
Object.values(wallets),
|
||||
'divider',
|
||||
Object.values(accounts),
|
||||
'divider',
|
||||
Object.values(contacts),
|
||||
'divider',
|
||||
Object.values(contracts)
|
||||
);
|
||||
|
||||
const entries = {
|
||||
...wallets,
|
||||
...accounts,
|
||||
...contacts,
|
||||
...contracts
|
||||
};
|
||||
|
||||
return { autocompleteEntries, entries };
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
const keys = [ 'error', 'value' ];
|
||||
|
||||
const prevValues = pick(this.props, keys);
|
||||
const nextValues = pick(nextProps, keys);
|
||||
|
||||
return !isEqual(prevValues, nextValues);
|
||||
}
|
||||
expanded: false,
|
||||
focused: false,
|
||||
focusedCat: null,
|
||||
focusedItem: null,
|
||||
inputFocused: false,
|
||||
inputValue: '',
|
||||
values: []
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { value } = this.props;
|
||||
const { entries, autocompleteEntries } = this.entriesFromProps();
|
||||
const addresses = Object.keys(entries).sort();
|
||||
|
||||
this.setState({ autocompleteEntries, entries, addresses, value });
|
||||
this.setValues();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps.value !== this.props.value) {
|
||||
this.setState({ value: newProps.value });
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.values && this.values.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setValues(nextProps);
|
||||
}
|
||||
|
||||
setValues (props = this.props) {
|
||||
const { accounts = {}, contracts = {}, contacts = {}, wallets = {} } = props;
|
||||
|
||||
const accountsN = Object.keys(accounts).length;
|
||||
const contractsN = Object.keys(contracts).length;
|
||||
const contactsN = Object.keys(contacts).length;
|
||||
const walletsN = Object.keys(wallets).length;
|
||||
|
||||
if (accountsN + contractsN + contactsN + walletsN === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.values = [
|
||||
{
|
||||
label: 'accounts',
|
||||
values: [].concat(
|
||||
Object.values(wallets),
|
||||
Object.values(accounts)
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'contacts',
|
||||
values: Object.values(contacts)
|
||||
},
|
||||
{
|
||||
label: 'contracts',
|
||||
values: Object.values(contracts)
|
||||
}
|
||||
].filter((cat) => cat.values.length > 0);
|
||||
|
||||
this.handleChange();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { allowInput, disabled, error, hint, label } = this.props;
|
||||
const { autocompleteEntries, value } = this.state;
|
||||
const input = this.renderInput();
|
||||
const content = this.renderContent();
|
||||
|
||||
const searchText = this.getSearchText();
|
||||
const icon = this.renderIdentityIcon(value);
|
||||
const classes = [ styles.main ];
|
||||
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<AutoComplete
|
||||
className={ !icon ? '' : styles.paddedInput }
|
||||
disabled={ disabled }
|
||||
entries={ autocompleteEntries }
|
||||
entry={ this.getEntry() || {} }
|
||||
error={ error }
|
||||
filter={ this.handleFilter }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='ui.addressSelect.search.hint'
|
||||
defaultMessage='search for {hint}'
|
||||
values={ {
|
||||
hint: hint ||
|
||||
<FormattedMessage
|
||||
id='ui.addressSelect.search.address'
|
||||
defaultMessage='address' />
|
||||
} } />
|
||||
}
|
||||
label={ label }
|
||||
onBlur={ this.onBlur }
|
||||
onChange={ this.onChange }
|
||||
onUpdateInput={ allowInput && this.onUpdateInput }
|
||||
renderItem={ this.renderItem }
|
||||
value={ searchText } />
|
||||
{ icon }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderIdentityIcon (inputValue) {
|
||||
const { error, value, label } = this.props;
|
||||
|
||||
if (error || !inputValue || value.length !== 42) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classes = [ styles.icon ];
|
||||
|
||||
if (!label) {
|
||||
classes.push(styles.noLabel);
|
||||
}
|
||||
|
||||
return (
|
||||
<IdentityIcon
|
||||
address={ value }
|
||||
center
|
||||
<div
|
||||
className={ classes.join(' ') }
|
||||
inline />
|
||||
onBlur={ this.handleMainBlur }
|
||||
onClick={ this.handleFocus }
|
||||
onFocus={ this.handleMainFocus }
|
||||
onKeyDown={ this.handleInputAddresKeydown }
|
||||
ref='inputAddress'
|
||||
tabIndex={ 0 }
|
||||
>
|
||||
{ input }
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderItem = (entry) => {
|
||||
const { address, name } = entry;
|
||||
renderInput () {
|
||||
const { focused } = this.state;
|
||||
const { accountsInfo, disabled, error, hint, label, value } = this.props;
|
||||
|
||||
const _balance = this.getBalance(address);
|
||||
const balance = _balance ? _balance.toNumber() : _balance;
|
||||
const input = (
|
||||
<InputAddress
|
||||
accountsInfo={ accountsInfo }
|
||||
allowCopy={ false }
|
||||
disabled={ disabled }
|
||||
error={ error }
|
||||
hint={ hint }
|
||||
focused={ focused }
|
||||
label={ label }
|
||||
readOnly
|
||||
tabIndex={ -1 }
|
||||
text
|
||||
value={ value }
|
||||
/>
|
||||
);
|
||||
|
||||
if (!this.items[address] || this.items[address].balance !== balance) {
|
||||
this.items[address] = {
|
||||
address,
|
||||
balance,
|
||||
text: name && name.toUpperCase() || address,
|
||||
value: this.renderMenuItem(address)
|
||||
};
|
||||
if (disabled) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return this.items[address];
|
||||
return (
|
||||
<div className={ styles.inputAddress }>
|
||||
{ input }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getBalance (address) {
|
||||
const { balances = {} } = this.props;
|
||||
renderContent () {
|
||||
const { muiTheme } = this.context;
|
||||
const { hint, disabled, label } = this.props;
|
||||
const { expanded, inputFocused } = this.state;
|
||||
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = `addressSelect_${++currentId}`;
|
||||
|
||||
return (
|
||||
<Portal
|
||||
className={ styles.inputContainer }
|
||||
onClose={ this.handleClose }
|
||||
onKeyDown={ this.handleKeyDown }
|
||||
open={ expanded }
|
||||
>
|
||||
<label className={ styles.label } htmlFor={ id }>
|
||||
{ label }
|
||||
</label>
|
||||
<input
|
||||
id={ id }
|
||||
className={ styles.input }
|
||||
placeholder={ hint }
|
||||
|
||||
onBlur={ this.handleInputBlur }
|
||||
onFocus={ this.handleInputFocus }
|
||||
onChange={ this.handleChange }
|
||||
|
||||
ref={ this.setInputRef }
|
||||
/>
|
||||
|
||||
<div className={ styles.underline }>
|
||||
<TextFieldUnderline
|
||||
focus={ inputFocused }
|
||||
focusStyle={ BOTTOM_BORDER_STYLE }
|
||||
muiTheme={ muiTheme }
|
||||
style={ BOTTOM_BORDER_STYLE }
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ this.renderCurrentInput() }
|
||||
{ this.renderAccounts() }
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
renderCurrentInput () {
|
||||
const { inputValue } = this.state;
|
||||
|
||||
if (!this.props.allowInput || !inputValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { address, addressError } = validateAddress(inputValue);
|
||||
|
||||
if (addressError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this.renderAccountCard({ address }) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAccounts () {
|
||||
const { values } = this.state;
|
||||
|
||||
if (values.length === 0) {
|
||||
return (
|
||||
<div className={ styles.categories }>
|
||||
<div className={ styles.empty }>
|
||||
<FormattedMessage
|
||||
id='addressSelect.noAccount'
|
||||
defaultMessage='No account matches this query...'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const categories = values.map((category) => {
|
||||
return this.renderCategory(category.label, category.values);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ styles.categories }>
|
||||
{ categories }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCategory (name, values = []) {
|
||||
let content;
|
||||
|
||||
if (values.length === 0) {
|
||||
content = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='addressSelect.noAccount'
|
||||
defaultMessage='No account matches this query...'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
const cards = values
|
||||
.map((account) => this.renderAccountCard(account));
|
||||
|
||||
content = (
|
||||
<div className={ styles.cards }>
|
||||
<div>{ cards }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.category } key={ name }>
|
||||
<div className={ styles.title }>{ name }</div>
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAccountCard (_account) {
|
||||
const { balances, accountsInfo } = this.props;
|
||||
const { address, index = null } = _account;
|
||||
|
||||
const balance = balances[address];
|
||||
|
||||
if (!balance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ethToken = balance.tokens.find((tok) => tok.token && tok.token.tag && tok.token.tag.toLowerCase() === 'eth');
|
||||
|
||||
if (!ethToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ethToken.value;
|
||||
}
|
||||
|
||||
renderBalance (address) {
|
||||
const balance = this.getBalance(address) || 0;
|
||||
const value = fromWei(balance);
|
||||
const account = {
|
||||
...accountsInfo[address],
|
||||
address, index
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ styles.balance }>
|
||||
{ value.toFormat(3) }<small> { 'ETH' }</small>
|
||||
</div>
|
||||
<AccountCard
|
||||
account={ account }
|
||||
balance={ balance }
|
||||
key={ `account_${index}` }
|
||||
onClick={ this.handleClick }
|
||||
onFocus={ this.focusItem }
|
||||
ref={ `account_${index}` }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderMenuItem (address) {
|
||||
const balance = this.props.balances
|
||||
? this.renderBalance(address)
|
||||
: null;
|
||||
|
||||
const item = (
|
||||
<div className={ styles.account }>
|
||||
<IdentityIcon
|
||||
address={ address }
|
||||
center
|
||||
className={ styles.image }
|
||||
inline />
|
||||
<IdentityName
|
||||
address={ address }
|
||||
className={ styles.name } />
|
||||
{ balance }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={ styles.menuItem }
|
||||
key={ address }
|
||||
label={ item }
|
||||
value={ address }>
|
||||
{ item }
|
||||
</MenuItem>
|
||||
);
|
||||
setInputRef = (refId) => {
|
||||
this.inputRef = refId;
|
||||
}
|
||||
|
||||
getSearchText () {
|
||||
const entry = this.getEntry();
|
||||
|
||||
return entry && entry.name
|
||||
? entry.name.toUpperCase()
|
||||
: this.state.value;
|
||||
}
|
||||
|
||||
getEntry () {
|
||||
const { entries, value } = this.state;
|
||||
return value ? entries[value] : null;
|
||||
}
|
||||
|
||||
handleFilter = (searchText, name, item) => {
|
||||
const { address } = item;
|
||||
const entry = this.state.entries[address];
|
||||
const lowCaseSearch = (searchText || '').toLowerCase();
|
||||
|
||||
return [entry.name, entry.address]
|
||||
.some(text => text.toLowerCase().indexOf(lowCaseSearch) !== -1);
|
||||
}
|
||||
|
||||
onChange = (entry, empty) => {
|
||||
handleCustomInput = () => {
|
||||
const { allowInput } = this.props;
|
||||
const { value } = this.state;
|
||||
const { inputValue, values } = this.state;
|
||||
|
||||
const address = entry && entry.address
|
||||
? entry.address
|
||||
: ((empty && !allowInput) ? '' : value);
|
||||
// If input is HEX and allowInput === true, send it
|
||||
if (allowInput && inputValue && /^(0x)?([0-9a-f])+$/i.test(inputValue)) {
|
||||
return this.handleClick(inputValue);
|
||||
}
|
||||
|
||||
this.props.onChange(null, address);
|
||||
// If only one value, select it
|
||||
if (values.length === 1 && values[0].values.length === 1) {
|
||||
const value = values[0].values[0];
|
||||
return this.handleClick(value.address);
|
||||
}
|
||||
}
|
||||
|
||||
onUpdateInput = (query, choices) => {
|
||||
const { api } = this.context;
|
||||
handleInputAddresKeydown = (event) => {
|
||||
const code = keycode(event);
|
||||
|
||||
const address = query.trim();
|
||||
// Simulate click on input address if enter is pressed
|
||||
if (code === 'enter') {
|
||||
return this.handleDOMAction('inputAddress', 'click');
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^0x/.test(address) && api.util.isAddressValid(`0x${address}`)) {
|
||||
const checksumed = api.util.toChecksumAddress(`0x${address}`);
|
||||
return this.props.onChange(null, checksumed);
|
||||
handleKeyDown = (event) => {
|
||||
const codeName = keycode(event);
|
||||
|
||||
if (event.ctrlKey) {
|
||||
return event;
|
||||
}
|
||||
|
||||
switch (codeName) {
|
||||
case 'enter':
|
||||
const index = this.state.focusedItem;
|
||||
if (!index) {
|
||||
return this.handleCustomInput();
|
||||
}
|
||||
|
||||
return this.handleDOMAction(`account_${index}`, 'click');
|
||||
|
||||
case 'left':
|
||||
case 'right':
|
||||
case 'up':
|
||||
case 'down':
|
||||
return this.handleNavigation(codeName, event);
|
||||
|
||||
default:
|
||||
const code = codes[codeName];
|
||||
|
||||
// @see https://github.com/timoxley/keycode/blob/master/index.js
|
||||
// lower case chars
|
||||
if (code >= (97 - 32) && code <= (122 - 32)) {
|
||||
return this.handleDOMAction(this.inputRef, 'focus');
|
||||
}
|
||||
|
||||
// numbers
|
||||
if (code >= 48 && code <= 57) {
|
||||
return this.handleDOMAction(this.inputRef, 'focus');
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
handleDOMAction = (ref, method) => {
|
||||
const refItem = typeof ref === 'string' ? this.refs[ref] : ref;
|
||||
const element = ReactDOM.findDOMNode(refItem);
|
||||
|
||||
if (!element || typeof element[method] !== 'function') {
|
||||
console.warn('could not find', ref, 'or method', method);
|
||||
return;
|
||||
}
|
||||
|
||||
return element[method]();
|
||||
}
|
||||
|
||||
focusItem = (index) => {
|
||||
this.setState({ focusedItem: index });
|
||||
return this.handleDOMAction(`account_${index}`, 'focus');
|
||||
}
|
||||
|
||||
handleNavigation = (direction, event) => {
|
||||
const { focusedItem, focusedCat, values } = this.state;
|
||||
|
||||
// Don't do anything if no values
|
||||
if (values.length === 0) {
|
||||
return event;
|
||||
}
|
||||
|
||||
// Focus on the first element if none selected yet if going down
|
||||
if (!focusedItem) {
|
||||
if (direction !== 'down') {
|
||||
return event;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const nextValues = values[focusedCat || 0];
|
||||
const nextFocus = nextValues ? nextValues.values[0] : null;
|
||||
return this.focusItem(nextFocus && nextFocus.index || 1);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// Find the previous focused category
|
||||
const prevCategoryIndex = values.findIndex((category) => {
|
||||
return category.values.find((value) => value.index === focusedItem);
|
||||
});
|
||||
const prevFocusIndex = values[prevCategoryIndex].values.findIndex((a) => a.index === focusedItem);
|
||||
|
||||
let nextCategory = prevCategoryIndex;
|
||||
let nextFocusIndex;
|
||||
|
||||
// If down: increase index if possible
|
||||
if (direction === 'down') {
|
||||
const prevN = values[prevCategoryIndex].values.length;
|
||||
nextFocusIndex = Math.min(prevFocusIndex + 1, prevN - 1);
|
||||
}
|
||||
|
||||
// If up: decrease index if possible
|
||||
if (direction === 'up') {
|
||||
// Focus on search if at the top
|
||||
if (prevFocusIndex === 0) {
|
||||
return this.handleDOMAction(this.inputRef, 'focus');
|
||||
}
|
||||
|
||||
nextFocusIndex = prevFocusIndex - 1;
|
||||
}
|
||||
|
||||
// If right: next category
|
||||
if (direction === 'right') {
|
||||
nextCategory = Math.min(prevCategoryIndex + 1, values.length - 1);
|
||||
}
|
||||
|
||||
// If right: previous category
|
||||
if (direction === 'left') {
|
||||
nextCategory = Math.max(prevCategoryIndex - 1, 0);
|
||||
}
|
||||
|
||||
// If left or right: try to keep the horizontal index
|
||||
if (direction === 'left' || direction === 'right') {
|
||||
this.setState({ focusedCat: nextCategory });
|
||||
nextFocusIndex = Math.min(prevFocusIndex, values[nextCategory].values.length - 1);
|
||||
}
|
||||
|
||||
const nextFocus = values[nextCategory].values[nextFocusIndex].index;
|
||||
return this.focusItem(nextFocus);
|
||||
}
|
||||
|
||||
handleClick = (address) => {
|
||||
// Don't do anything if it's only text-selection
|
||||
if (window.getSelection && window.getSelection().type === 'Range') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onChange(null, address);
|
||||
this.handleClose();
|
||||
}
|
||||
|
||||
handleMainBlur = () => {
|
||||
if (window.document.hasFocus() && !this.state.expanded) {
|
||||
this.closing = false;
|
||||
this.setState({ focused: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleMainFocus = () => {
|
||||
if (this.state.focused) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ focused: true }, () => {
|
||||
if (this.closing) {
|
||||
this.closing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleFocus();
|
||||
});
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ expanded: true, focusedItem: null, focusedCat: null }, () => {
|
||||
window.setTimeout(() => {
|
||||
this.handleDOMAction(this.inputRef, 'focus');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.closing = true;
|
||||
|
||||
if (this.refs.inputAddress) {
|
||||
this.handleDOMAction('inputAddress', 'focus');
|
||||
}
|
||||
|
||||
this.setState({ expanded: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the given values based on the given
|
||||
* filter
|
||||
*/
|
||||
filterValues = (values = [], _filter = '') => {
|
||||
const filter = _filter.toLowerCase();
|
||||
|
||||
return values
|
||||
// Remove empty accounts
|
||||
.filter((a) => a)
|
||||
.filter((account) => {
|
||||
const address = account.address.toLowerCase();
|
||||
const inAddress = address.includes(filter);
|
||||
|
||||
if (!account.name || inAddress) {
|
||||
return inAddress;
|
||||
}
|
||||
|
||||
const name = account.name.toLowerCase();
|
||||
const inName = name.includes(filter);
|
||||
const { meta = {} } = account;
|
||||
|
||||
if (!meta.tags || inName) {
|
||||
return inName;
|
||||
}
|
||||
|
||||
const tags = (meta.tags || []).join('');
|
||||
return tags.includes(filter);
|
||||
})
|
||||
.sort((accA, accB) => {
|
||||
const nameA = accA.name || accA.address;
|
||||
const nameB = accB.name || accB.address;
|
||||
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
}
|
||||
|
||||
handleInputBlur = () => {
|
||||
this.setState({ inputFocused: false });
|
||||
}
|
||||
|
||||
handleInputFocus = () => {
|
||||
this.setState({ focusedItem: null, inputFocused: true });
|
||||
}
|
||||
|
||||
handleChange = (event = { target: {} }) => {
|
||||
const { value = '' } = event.target;
|
||||
let index = 0;
|
||||
|
||||
const values = this.values
|
||||
.map((category) => {
|
||||
const filteredValues = this
|
||||
.filterValues(category.values, value)
|
||||
.map((value) => {
|
||||
index++;
|
||||
return { ...value, index: parseInt(index) };
|
||||
});
|
||||
|
||||
return {
|
||||
label: category.label,
|
||||
values: filteredValues
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({
|
||||
values,
|
||||
focusedItem: null,
|
||||
inputValue: value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { accountsInfo } = state.personal;
|
||||
const { balances } = state.balances;
|
||||
|
||||
return {
|
||||
accountsInfo,
|
||||
balances
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(AddressSelect);
|
||||
|
||||
Reference in New Issue
Block a user