Send Form Style Adjustments (#1368)

* Remove title from account, tighten buttons and subtabs.

* Send everything button in input.

* Request tx to full width, adjust transaction fee spacing.

* Fix token balances button spacing.

* Fix address identicon flying offscreen. Tighten up identicon, show border even when theres no identicon.

* Add isSelfAddress boolean to AddressField, use it on WalletInfo tab.

* Use short amount again.

* Unused
This commit is contained in:
William O'Beirne 2018-03-22 14:30:51 -04:00 committed by Daniel Ternyak
parent 3284769ac9
commit bdaf40a0ce
25 changed files with 231 additions and 113 deletions

View File

@ -6,14 +6,18 @@ import { Input } from 'components/ui';
interface Props {
isReadOnly?: boolean;
isSelfAddress?: boolean;
}
export const AddressField: React.SFC<Props> = ({ isReadOnly }) => (
export const AddressField: React.SFC<Props> = ({ isReadOnly, isSelfAddress }) => (
<AddressFieldFactory
isSelfAddress={isSelfAddress}
withProps={({ currentTo, isValid, onChange, readOnly }) => (
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('SEND_ADDR_SHORT')}</div>
<div className="input-group-header">
{translate(isSelfAddress ? 'X_ADDRESS' : 'SEND_ADDR')}
</div>
<Input
className={`input-group-input ${isValid ? '' : 'invalid'}`}
type="text"

View File

@ -11,6 +11,7 @@ interface DispatchProps {
interface OwnProps {
to: string | null;
isSelfAddress?: boolean;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
@ -33,7 +34,13 @@ class AddressFieldFactoryClass extends React.Component<Props> {
}
public render() {
return <AddressInputFactory onChange={this.setAddress} withProps={this.props.withProps} />;
return (
<AddressInputFactory
isSelfAddress={this.props.isSelfAddress}
onChange={this.setAddress}
withProps={this.props.withProps}
/>
);
}
private setAddress = (ev: React.FormEvent<HTMLInputElement>) => {
@ -45,13 +52,16 @@ class AddressFieldFactoryClass extends React.Component<Props> {
const AddressFieldFactory = connect(null, { setCurrentTo })(AddressFieldFactoryClass);
interface DefaultAddressFieldProps {
isSelfAddress?: boolean;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({ withProps }) => (
const DefaultAddressField: React.SFC<DefaultAddressFieldProps> = ({ isSelfAddress, withProps }) => (
<Query
params={['to']}
withQuery={({ to }) => <AddressFieldFactory to={to} withProps={withProps} />}
withQuery={({ to }) => (
<AddressFieldFactory to={to} isSelfAddress={isSelfAddress} withProps={withProps} />
)}
/>
);

View File

@ -0,0 +1,25 @@
@import 'common/sass/variables';
.AddressInput {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
&-input {
flex-grow: 1;
}
&-identicon {
padding-top: .75rem;
transform: translateX(20%);
@media (max-width: $screen-sm) {
padding-top: 1.5rem;
.Identicon {
width: 3.4rem !important;
height: 3.4rem !important;
}
}
}
}

View File

@ -6,8 +6,11 @@ import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { CallbackProps } from 'components/AddressFieldFactory';
import { addHexPrefix } from 'ethereumjs-util';
import { getWalletInst } from 'selectors/wallet';
import { getResolvingDomain } from 'selectors/ens';
import { isValidENSAddress } from 'libs/validators';
import { Address } from 'libs/units';
import './AddressInputFactory.scss';
interface StateProps {
currentTo: ICurrentTo;
@ -16,6 +19,7 @@ interface StateProps {
}
interface OwnProps {
isSelfAddress?: boolean;
onChange(ev: React.FormEvent<HTMLInputElement>): void;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
@ -29,12 +33,12 @@ const ENSStatus: React.SFC<{ isLoading: boolean; ensAddress: string; rawAddress:
const text = 'Loading ENS address...';
if (isLoading) {
return (
<>
<React.Fragment>
<Spinner /> {text}
</>
</React.Fragment>
);
} else {
return isENS ? <>{`Resolved Address: ${rawAddress}`}</> : null;
return isENS ? <React.Fragment>{`Resolved Address: ${rawAddress}`}</React.Fragment> : null;
}
};
@ -42,12 +46,12 @@ type Props = OwnProps & StateProps;
class AddressInputFactoryClass extends Component<Props> {
public render() {
const { currentTo, onChange, isValid, withProps, isResolving } = this.props;
const { currentTo, onChange, isValid, withProps, isSelfAddress, isResolving } = this.props;
const { value } = currentTo;
const addr = addHexPrefix(value ? value.toString('hex') : '0');
return (
<div className="row form-group">
<div className="col-xs-11">
<div className="AddressInput form-group">
<div className="AddressInput-input">
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>
@ -55,13 +59,13 @@ class AddressInputFactoryClass extends Component<Props> {
currentTo,
isValid,
onChange,
readOnly: !!readOnly || this.props.isResolving
readOnly: !!(readOnly || this.props.isResolving || isSelfAddress)
})
}
/>
<ENSStatus ensAddress={currentTo.raw} isLoading={isResolving} rawAddress={addr} />
</div>
<div className="col-xs-1" style={{ padding: 0 }}>
<div className="AddressInput-identicon">
<Identicon address={addr} />
</div>
</div>
@ -69,8 +73,22 @@ class AddressInputFactoryClass extends Component<Props> {
}
}
export const AddressInputFactory = connect((state: AppState) => ({
currentTo: getCurrentTo(state),
isResolving: getResolvingDomain(state),
isValid: isValidCurrentTo(state)
}))(AddressInputFactoryClass);
export const AddressInputFactory = connect((state: AppState, ownProps: OwnProps) => {
let currentTo: ICurrentTo;
if (ownProps.isSelfAddress) {
const wallet = getWalletInst(state);
const addr = wallet ? wallet.getAddressString() : '';
currentTo = {
raw: addr,
value: Address(addr)
};
} else {
currentTo = getCurrentTo(state);
}
return {
currentTo,
isResolving: getResolvingDomain(state),
isValid: isValidCurrentTo(state)
};
})(AddressInputFactoryClass);

View File

@ -1,35 +1,38 @@
import React from 'react';
import { AmountFieldFactory } from './AmountFieldFactory';
import { UnitDropDown } from 'components';
import { UnitDropDown, SendEverything } from 'components';
import translate from 'translations';
import { Input } from 'components/ui';
interface Props {
hasUnitDropdown?: boolean;
hasSendEverything?: boolean;
showAllTokens?: boolean;
customValidator?(rawAmount: string): boolean;
}
export const AmountField: React.SFC<Props> = ({
hasUnitDropdown,
hasSendEverything,
showAllTokens,
customValidator
}) => (
<AmountFieldFactory
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
<div className="input-group-wrapper">
<label className="input-group input-group-inline">
<div className="AmountField input-group-wrapper">
<label className="AmountField-group input-group input-group-inline">
<div className="input-group-header">{translate('SEND_AMOUNT_SHORT')}</div>
<Input
className={`input-group-input ${
isAmountValid(raw, customValidator, isValid) ? '' : 'invalid'
}`}
type="number"
placeholder={'1'}
placeholder="1"
value={raw}
readOnly={!!readOnly}
onChange={onChange}
/>
{hasSendEverything && <SendEverything />}
{hasUnitDropdown && <UnitDropDown showAllTokens={showAllTokens} />}
</label>
</div>

View File

@ -2,6 +2,8 @@
.AddCustom {
&-field {
margin-bottom: 0;
&-error {
font-size: 13px;
font-weight: normal;
@ -14,6 +16,7 @@
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: 0 -$space-xs;
&-help {
text-align: center;
@ -24,7 +27,7 @@
&-btn {
padding: 0.5rem 1.5rem;
margin: 0.25rem 0.5rem;
margin: 0 $space-xs;
}
}
}

View File

@ -49,7 +49,10 @@ export default class TokenBalances extends React.PureComponent<Props, State> {
help = 'Select which tokens you would like to keep track of';
bottom = (
<div className="TokenBalances-buttons">
<button className="btn btn-primary btn-block" onClick={this.handleSetWalletTokens}>
<button
className="TokenBalances-buttons-btn btn btn-primary btn-block"
onClick={this.handleSetWalletTokens}
>
<span>{translate('X_SAVE')}</span>
</button>
<p className="TokenBalances-buttons-help">{translate('PROMPT_ADD_CUSTOM_TKN')}</p>
@ -68,10 +71,16 @@ export default class TokenBalances extends React.PureComponent<Props, State> {
} else {
bottom = (
<div className="TokenBalances-buttons">
<button className="btn btn-default btn-xs" onClick={this.toggleShowCustomTokenForm}>
<button
className="TokenBalances-buttons-btn btn btn-default btn-xs"
onClick={this.toggleShowCustomTokenForm}
>
<span>{translate('SEND_CUSTOM')}</span>
</button>
<button className="btn btn-default btn-xs" onClick={this.props.scanWalletForTokens}>
<button
className="TokenBalances-buttons-btn btn btn-default btn-xs"
onClick={this.props.scanWalletForTokens}
>
<span>{translate('SCAN_TOKENS')}</span>
</button>
</div>

View File

@ -37,9 +37,13 @@
display: flex;
flex-wrap: wrap;
justify-content: center;
& > &-btn {
margin: 0.25rem 0.5rem;
margin: 0 #{-$space-xs} #{-$space-xs};
&-btn {
flex-grow: 1;
margin: 0 $space-xs $space-xs;
}
&-help {
padding-top: 10px;
text-align: center;

View File

@ -14,7 +14,7 @@ interface Props {
export const GasLimitField: React.SFC<Props> = ({ customLabel, disabled }) => (
<GasLimitFieldFactory
withProps={({ gasLimit: { raw }, onChange, readOnly, gasEstimationPending }) => (
<div className="input-group-wrapper AdvancedGas-gas-price">
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">
{customLabel ? customLabel : translate('TRANS_GAS')}

View File

@ -0,0 +1,46 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
$width: 40px;
$default-opacity: 0.4;
@keyframes send-everything-enter {
0% {
opacity: 0;
}
100% {
opacity: $default-opacity;
}
}
.SendEverything {
@include reset-button;
display: block;
position: relative;
height: $input-height-base;
width: $width;
margin-left: -$width;
font-size: 1.4rem;
border-left: 1px solid #e5ecf3;
opacity: $default-opacity;
transition: $transition;
animation: send-everything-enter 200ms ease 1;
@include show-tooltip-on-hover;
&-icon {
transition: transform 150ms ease;
}
&:disabled {
display: none;
}
&:hover {
color: $brand-primary;
opacity: 1;
.SendEverything-icon {
transform: translateY(-2px);
}
}
}

View File

@ -1,11 +1,13 @@
import { Query } from 'components/renderCbs';
import React, { Component } from 'react';
import { TokenValue, Wei } from 'libs/units';
import translate from 'translations';
import { connect } from 'react-redux';
import { Query } from 'components/renderCbs';
import { Tooltip } from 'components/ui';
import { TokenValue, Wei } from 'libs/units';
import translate, { translateRaw } from 'translations';
import { sendEverythingRequested, TSendEverythingRequested } from 'actions/transaction';
import { getCurrentBalance } from 'selectors/wallet';
import { AppState } from 'reducers';
import './SendEverything.scss';
interface DispatchProps {
sendEverythingRequested: TSendEverythingRequested;
@ -17,21 +19,22 @@ type Props = StateProps & DispatchProps;
class SendEverythingClass extends Component<Props> {
public render() {
if (!this.props.currentBalance) {
return null;
}
const { currentBalance } = this.props;
return (
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>
!readOnly ? (
<span className="help-block">
<a onClick={this.onSendEverything}>
<span>{translate('SEND_TRANSFERTOTAL')}</span>
</a>
</span>
) : null
}
withQuery={({ readOnly }) => (
<button
className="SendEverything"
disabled={!!readOnly || !currentBalance}
onClick={this.onSendEverything}
aria-label={translateRaw('SEND_TRANSFERTOTAL')}
>
<i className="SendEverything-icon fa fa-angle-double-up" />
<Tooltip>{translate('SEND_TRANSFERTOTAL')}</Tooltip>
</button>
)}
/>
);
}

View File

@ -1,7 +1,7 @@
@import 'common/sass/variables';
.SubTabs {
margin-top: 15px;
margin-top: $space-xs;
&-tabs {
display: inline-block;

View File

@ -5,6 +5,6 @@
display: inline-block;
position: relative;
margin-top: $space-sm;
left: -8px;
left: -14px;
}
}

View File

@ -5,6 +5,8 @@
margin-bottom: 0;
&-calculate-limit {
font-size: $font-size-small;
.checkbox {
display: flex;
align-items: center;
@ -15,7 +17,6 @@
margin-right: 8px;
}
span {
font-size: 1rem;
font-weight: 400;
}
}

View File

@ -71,19 +71,21 @@ class AdvancedGas extends React.Component<Props, State> {
<div className="AdvancedGas-flex-wrapper flex-wrapper">
{gasPriceField && (
<div className="input-group-wrapper AdvancedGas-gas-price">
<label className="input-group">
<div className="input-group-header">
{translateRaw('OFFLINE_STEP2_LABEL_3')} (gwei)
</div>
<Input
className={!!gasPrice.raw && !validGasPrice ? 'is-invalid' : ''}
type="number"
placeholder="40"
value={gasPrice.raw}
onChange={this.handleGasPriceChange}
/>
</label>
<div className="AdvancedGas-gas-price">
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">
{translateRaw('OFFLINE_STEP2_LABEL_3')} (gwei)
</div>
<Input
className={!!gasPrice.raw && !validGasPrice ? 'is-invalid' : ''}
type="number"
placeholder="40"
value={gasPrice.raw}
onChange={this.handleGasPriceChange}
/>
</label>
</div>
</div>
)}

View File

@ -19,32 +19,32 @@ export default function Identicon(props: Props) {
className={`Identicon ${className}`}
title="Address Identicon"
style={{ width: size, height: size, position: 'relative' }}
aria-hidden={!identiconDataUrl}
>
{identiconDataUrl && (
<React.Fragment>
<img
src={identiconDataUrl}
alt="Unique Address Image"
style={{
height: '100%',
width: '100%',
padding: '0px',
borderRadius: '50%'
}}
/>
<div
className="border"
style={{
position: 'absolute',
height: 'inherit',
width: 'inherit',
top: 0,
boxShadow: '0 3px 8px 0 rgba(0, 0, 0, 0.1), inset 0 0 3px 0 rgba(0, 0, 0, 0.1)',
borderRadius: '50%'
}}
/>
</React.Fragment>
<img
src={identiconDataUrl}
alt="Unique Address Image"
style={{
height: '100%',
width: '100%',
padding: '0px',
borderRadius: '50%'
}}
/>
)}
<div
className="border"
style={{
position: 'absolute',
height: '100%',
width: '100%',
top: 0,
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.15), inset 0 0 3px 0 rgba(0, 0, 0, 0.15)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</div>
);
}

View File

@ -52,13 +52,15 @@
width: 100%;
border: 1px solid #e5ecf3;
border-radius: 2px;
height: $input-height-base;
padding: 0.75rem 1rem;
font-weight: 400;
font-size: 1rem;
font-size: $input-margin-bottom;
color: rgba(0, 0, 0, 0.87);
box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05);
transition: border-color 120ms, box-shadow 120ms;
margin-bottom: 1rem;
&.border-rad-right-0 {
border-top-right-radius: 0;
border-bottom-right-radius: 0;

View File

@ -6,10 +6,9 @@
&-open {
position: absolute;
top: 50%;
top: 0;
right: 0;
padding-right: none;
transform: translateY(-50%);
&-text {
padding-right: 8px;
@ -19,7 +18,7 @@
&-close {
@include reset-button;
position: absolute;
top: 56px;
top: 6px;
right: 6px;
margin: 0.5rem 1rem;
z-index: 1;

View File

@ -8,7 +8,7 @@ import closeIcon from 'assets/images/close.svg';
import './UnlockHeader.scss';
interface Props {
title: string;
title?: string;
wallet: IWallet;
disabledWallets?: DisabledWallets;
showGenerateLink?: boolean;
@ -35,7 +35,7 @@ export class UnlockHeader extends React.PureComponent<Props, State> {
return (
<article className="UnlockHeader">
<h1 className="UnlockHeader-title">{title}</h1>
{title && <h1 className="UnlockHeader-title">{title}</h1>}
{wallet &&
!isExpanded && (
<button

View File

@ -5,7 +5,6 @@ import {
AddressField,
AmountField,
TXMetaDataPanel,
SendEverything,
CurrentCustomMessage,
GenerateTransaction,
SendButton,
@ -20,10 +19,10 @@ import { NonStandardTransaction } from './components';
const content = (
<div className="Tab-content-pane">
<AddressField />
<div className="row form-group">
<div className="col-xs-12">
<AmountField hasUnitDropdown={true} />
<SendEverything />
<AmountField hasUnitDropdown={true} hasSendEverything={true} />
</div>
</div>

View File

@ -97,7 +97,7 @@ class RequestPayment extends React.Component<Props, {}> {
<AddressField isReadOnly={true} />
<div className="row form-group">
<div className="col-xs-11">
<div className="col-xs-12">
<AmountField
hasUnitDropdown={true}
showAllTokens={true}
@ -107,7 +107,7 @@ class RequestPayment extends React.Component<Props, {}> {
</div>
<div className="row form-group">
<div className="col-xs-11">
<div className="col-xs-12">
<TXMetaDataPanel
initialState="advanced"
disableToggle={true}

View File

@ -3,8 +3,8 @@ import { toChecksumAddress } from 'ethereumjs-util';
import translate, { translateRaw } from 'translations';
import { IWallet } from 'libs/wallet';
import { print } from 'components/PrintableWallet';
import { Identicon, QRCode, Input } from 'components/ui';
import { GenerateKeystoreModal, TogglablePassword } from 'components';
import { QRCode } from 'components/ui';
import { GenerateKeystoreModal, TogglablePassword, AddressField } from 'components';
import './WalletInfo.scss';
interface Props {
@ -42,19 +42,7 @@ export default class WalletInfo extends React.PureComponent<Props, State> {
return (
<div className="WalletInfo">
<div className="Tab-content-pane">
<div className="row form-group">
<div className="col-xs-11">
<div className="input-group-wrapper">
<label className="input-group">
<div className="input-group-header">{translate('X_ADDRESS')}</div>
<Input readOnly={true} value={address} />
</label>
</div>
</div>
<div className="col-xs-1" style={{ padding: 0 }}>
<Identicon address={address} />
</div>
</div>
<AddressField isSelfAddress={true} />
{privateKey && (
<div className="row form-group">

View File

@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import translate, { translateRaw } from 'translations';
import translate from 'translations';
import TabSection from 'containers/TabSection';
import { UnlockHeader } from 'components/ui';
import { getWalletInst } from 'selectors/wallet';
@ -61,7 +61,7 @@ class SendTransaction extends React.Component<Props> {
return (
<TabSection>
<section className="Tab-content">
<UnlockHeader title={translateRaw('ACCOUNT')} showGenerateLink={true} />
<UnlockHeader showGenerateLink={true} />
{wallet && (
<div className="SubTabs row">
<div className="col-sm-8">

View File

@ -9,7 +9,8 @@
font-size: 1rem;
box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05);
transition: border-color 120ms, box-shadow 120ms;
height: 3rem;
height: $input-height-base;
&-control {
min-width: 7.5rem;
height: inherit;

View File

@ -8,10 +8,11 @@ $input-padding-x: 1rem;
$input-padding-y: 0.75rem;
$input-padding: $input-padding-y $input-padding-x;
$input-height-base: 2.55rem;
$input-height-base: 3rem;
$input-height-large: 4rem;
$input-height-small: 2rem;
$input-margin-bottom: 1rem;
$form-group-margin-bottom: $space-sm;
$legend-color: $gray-dark;