Improve accessibility (a11y) (#1267)

* Manage modal focus

* Add isOpen prop to CustomNodeModal

* Remove outline overrides

* Update outline style for inputs

* Fix modal focus management & Cleanup CustomNodeModal

* Add aria-label on modal close button

* Fix modal scroll to top

* Add aria-live property for notifications

* Add aria-busy to Spinner component

* Fix border styles for generatewallet password inputs

* Update token balances inputs

* Remove multiple h1's & Update styles

* Add alt text to all img elements

* Update swap link from bity to shapeshift

* Update aria-labels and alt text

* Only show keystore password input when required

* Revert "Only show keystore password input when required"

This reverts commit 7ec5de52da0982cd3131f365b142f6915638d831.

* address changes requested
This commit is contained in:
James Prado 2018-03-08 14:28:43 -05:00 committed by Daniel Ternyak
parent 6e8d807b22
commit 9cac0298a2
50 changed files with 570 additions and 474 deletions

View File

@ -1,36 +1,35 @@
// Mixins // Mixins
// -------------------------------------------------- // --------------------------------------------------
// Utilities // Utilities
@import "mixins/hide-text.less"; @import 'mixins/hide-text.less';
@import "mixins/opacity.less"; @import 'mixins/opacity.less';
@import "mixins/image.less"; @import 'mixins/image.less';
@import "mixins/labels.less"; @import 'mixins/labels.less';
@import "mixins/reset-filter.less"; @import 'mixins/reset-filter.less';
@import "mixins/resize.less"; @import 'mixins/resize.less';
@import "mixins/responsive-visibility.less"; @import 'mixins/responsive-visibility.less';
@import "mixins/size.less"; @import 'mixins/size.less';
@import "mixins/tab-focus.less"; @import 'mixins/reset-text.less';
@import "mixins/reset-text.less"; @import 'mixins/text-emphasis.less';
@import "mixins/text-emphasis.less"; @import 'mixins/text-overflow.less';
@import "mixins/text-overflow.less"; @import 'mixins/vendor-prefixes.less';
@import "mixins/vendor-prefixes.less";
// Components // Components
@import "mixins/alerts.less"; @import 'mixins/alerts.less';
@import "mixins/buttons.less"; @import 'mixins/buttons.less';
@import "mixins/panels.less"; @import 'mixins/panels.less';
@import "mixins/pagination.less"; @import 'mixins/pagination.less';
@import "mixins/list-group.less"; @import 'mixins/list-group.less';
@import "mixins/nav-divider.less"; @import 'mixins/nav-divider.less';
@import "mixins/forms.less"; @import 'mixins/forms.less';
@import "mixins/progress-bar.less"; @import 'mixins/progress-bar.less';
@import "mixins/table-row.less"; @import 'mixins/table-row.less';
// Skins // Skins
@import "mixins/background-variant.less"; @import 'mixins/background-variant.less';
@import "mixins/border-radius.less"; @import 'mixins/border-radius.less';
@import "mixins/gradients.less"; @import 'mixins/gradients.less';
// Layout // Layout
@import "mixins/clearfix.less"; @import 'mixins/clearfix.less';
@import "mixins/center-block.less"; @import 'mixins/center-block.less';
@import "mixins/nav-vertical-align.less"; @import 'mixins/nav-vertical-align.less';
@import "mixins/grid-framework.less"; @import 'mixins/grid-framework.less';
@import "mixins/grid.less"; @import 'mixins/grid.less';

View File

@ -20,10 +20,10 @@
// Set the border and box shadow on specific inputs to match // Set the border and box shadow on specific inputs to match
.form-control { .form-control {
border-color: @border-color; border-color: @border-color;
.box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075)); // Redeclare so transitions work .box-shadow(inset 0 1px 1px rgba(0, 0, 0, 0.075)); // Redeclare so transitions work
&:focus { &:focus {
border-color: darken(@border-color, 10%); border-color: darken(@border-color, 10%);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 3px rgba(@brand-primary, .5); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 3px rgba(@brand-primary, 0.5);
} }
} }
// Set validation states also for addons // Set validation states also for addons
@ -51,11 +51,10 @@
// Example usage: change the default blue border and shadow to white for better // Example usage: change the default blue border and shadow to white for better
// contrast against a dark gray background. // contrast against a dark gray background.
.form-control-focus(@color: @input-border-focus) { .form-control-focus(@color: @input-border-focus) {
@color-rgba: rgba(red(@color), green(@color), blue(@color), .6); @color-rgba: rgba(red(@color), green(@color), blue(@color), 0.6);
&:focus { &:focus {
border-color: @color; border-color: @color;
outline: 0; .box-shadow(~'inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}');
.box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}");
} }
} }

View File

@ -1,6 +0,0 @@
// WebKit-style focus
.tab-focus() {
outline: thin dotted;
outline-offset: 3px;
}

View File

@ -7,10 +7,6 @@
position: absolute; position: absolute;
left: 5px; left: 5px;
top: 5px; top: 5px;
&:hover,
&:active {
outline: 0;
}
} }
} }

View File

@ -18,7 +18,7 @@ export const AmountField: React.SFC<Props> = ({
<AmountFieldFactory <AmountFieldFactory
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => ( withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
<div className="input-group-wrapper"> <div className="input-group-wrapper">
<label className="input-group input-group-inline-dropdown"> <label className="input-group input-group-inline">
<div className="input-group-header">{translate('SEND_amount')}</div> <div className="input-group-header">{translate('SEND_amount')}</div>
<Input <Input
className={`input-group-input ${ className={`input-group-input ${

View File

@ -17,7 +17,7 @@ export const Coinbase: React.SFC<Props> = ({ address }) => (
<h5 key="2">Buy ETH with USD</h5> <h5 key="2">Buy ETH with USD</h5>
</div> </div>
<div className="Promos-promo-images"> <div className="Promos-promo-images">
<img src={CoinbaseLogo} /> <img src={CoinbaseLogo} alt="Coinbase logo" />
</div> </div>
</div> </div>
</NewTabLink> </NewTabLink>

View File

@ -11,8 +11,8 @@ export const HardwareWallets: React.SFC = () => (
<h6>Learn more about protecting your funds.</h6> <h6>Learn more about protecting your funds.</h6>
</div> </div>
<div className="Promos-promo-images"> <div className="Promos-promo-images">
<img src={ledgerLogo} /> <img src={ledgerLogo} alt="Ledger Logo" />
<img src={trezorLogo} /> <img src={trezorLogo} alt="Trezor Logo" />
</div> </div>
</div> </div>
</HelpLink> </HelpLink>

View File

@ -13,7 +13,7 @@ export const Shapeshift: React.SFC = () => (
</h5> </h5>
</div> </div>
<div className="Promos-promo-images"> <div className="Promos-promo-images">
<img src={ShapeshiftLogo} /> <img src={ShapeshiftLogo} alt="Shapeshift Logo" />
</div> </div>
</div> </div>
</Link> </Link>

View File

@ -85,7 +85,6 @@
height: 12px; height: 12px;
border: 3px solid $gray-lightest; border: 3px solid $gray-lightest;
border-radius: 100%; border-radius: 100%;
outline: none;
opacity: 0.6; opacity: 0.6;
&.is-active { &.is-active {
@ -96,7 +95,7 @@
// Per-promo customizations // Per-promo customizations
&--shapeshift { &--shapeshift {
background-color: #263A52; background-color: #263a52;
.Promos-promo-images { .Promos-promo-images {
max-width: 130px; max-width: 130px;

View File

@ -66,11 +66,11 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
{fields.map(field => { {fields.map(field => {
return ( return (
<label className="AddCustom-field form-group" key={field.name}> <label className="AddCustom-field form-group" key={field.name}>
<span className="AddCustom-field-label">{field.label}</span> <div className="input-group-header">{field.label}</div>
<Input <Input
className={`${ className={`${
errors[field.name] ? 'invalid' : field.value ? 'valid' : '' errors[field.name] ? 'invalid' : field.value ? 'valid' : ''
} AddCustom-field-input input-sm`} } input-group-input-small`}
type="text" type="text"
name={field.name} name={field.name}
value={field.value} value={field.value}

View File

@ -54,6 +54,7 @@ export default class TokenRow extends React.PureComponent<Props, State> {
{!!custom && ( {!!custom && (
<img <img
src={removeIcon} src={removeIcon}
alt="Remove"
className="TokenRow-symbol-remove" className="TokenRow-symbol-remove"
title="Remove Token" title="Remove Token"
onClick={this.onRemove} onClick={this.onRemove}

View File

@ -1,5 +1,5 @@
@import "common/sass/variables"; @import 'common/sass/variables';
@import "common/sass/mixins"; @import 'common/sass/mixins';
.BetaAgreement { .BetaAgreement {
@include cover-message; @include cover-message;
@ -20,7 +20,6 @@
margin: 0 auto; margin: 0 auto;
border: none; border: none;
padding: 0; padding: 0;
outline: none;
transition: $transition; transition: $transition;
&.is-continue { &.is-continue {

View File

@ -55,13 +55,13 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
return ( return (
<Modal <Modal
title={translate('Generate Keystore File')} title={translateRaw('Generate Keystore File')}
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
handleClose={this.handleClose} handleClose={this.handleClose}
> >
<form className="GenKeystore" onSubmit={this.handleSubmit}> <form className="GenKeystore" onSubmit={this.handleSubmit}>
<div className="input-group-wrapper GenKeystore-field"> <div className="input-group-wrapper GenKeystore-field">
<label className="input-group input-group-inline-dropdown"> <label className="input-group input-group-inline">
<div className="input-group-header">Private Key</div> <div className="input-group-header">Private Key</div>
<TogglablePassword <TogglablePassword
name="privateKey" name="privateKey"
@ -74,7 +74,7 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
</label> </label>
</div> </div>
<div className="input-group-wrapper GenKeystore-field"> <div className="input-group-wrapper GenKeystore-field">
<label className="input-group input-group-inline-dropdown"> <label className="input-group input-group-inline">
<div className="input-group-header">Password</div> <div className="input-group-header">Password</div>
<TogglablePassword <TogglablePassword
name="password" name="password"

View File

@ -0,0 +1,15 @@
.CustomNodeModal {
.flex-wrapper {
margin: 0px -8px;
> .input-group {
margin: 0px 8px;
> .input-group-input {
width: 100%;
}
}
}
input[type='checkbox'] {
margin-right: 1rem;
margin-bottom: 1rem;
}
}

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import Modal, { IButton } from 'components/ui/Modal'; import Modal, { IButton } from 'components/ui/Modal';
import translate from 'translations'; import translate, { translateRaw } from 'translations';
import { CustomNetworkConfig } from 'types/network'; import { CustomNetworkConfig } from 'types/network';
import { CustomNodeConfig } from 'types/node'; import { CustomNodeConfig } from 'types/node';
import { TAddCustomNetwork, addCustomNetwork, AddCustomNodeAction } from 'actions/config'; import { TAddCustomNetwork, addCustomNetwork, AddCustomNodeAction } from 'actions/config';
@ -13,19 +13,13 @@ import {
} from 'selectors/config'; } from 'selectors/config';
import { CustomNode } from 'libs/nodes'; import { CustomNode } from 'libs/nodes';
import { Input } from 'components/ui'; import { Input } from 'components/ui';
import Dropdown from 'components/ui/Dropdown';
import './CustomNodeModal.scss';
const CUSTOM = 'custom'; const CUSTOM = { label: 'Custom', value: 'custom' };
interface InputProps {
name: keyof Omit<State, 'hasAuth'>;
placeholder?: string;
type?: string;
autoComplete?: 'off';
onFocus?(): void;
onBlur?(): void;
}
interface OwnProps { interface OwnProps {
isOpen: boolean;
addCustomNode(payload: AddCustomNodeAction['payload']): void; addCustomNode(payload: AddCustomNodeAction['payload']): void;
handleClose(): void; handleClose(): void;
} }
@ -55,7 +49,7 @@ interface State {
type Props = OwnProps & StateProps & DispatchProps; type Props = OwnProps & StateProps & DispatchProps;
class CustomNodeModal extends React.Component<Props, State> { class CustomNodeModal extends React.Component<Props, State> {
public state: State = { public INITIAL_STATE = {
name: '', name: '',
url: '', url: '',
network: Object.keys(this.props.staticNetworks)[0], network: Object.keys(this.props.staticNetworks)[0],
@ -66,9 +60,17 @@ class CustomNodeModal extends React.Component<Props, State> {
username: '', username: '',
password: '' password: ''
}; };
public state: State = this.INITIAL_STATE;
public componentDidUpdate(prevProps: Props) {
// Reset state when modal opens
if (!prevProps.isOpen && prevProps.isOpen !== this.props.isOpen) {
this.setState(this.INITIAL_STATE);
}
}
public render() { public render() {
const { customNetworks, handleClose, staticNetworks } = this.props; const { customNetworks, handleClose, staticNetworks, isOpen } = this.props;
const { network } = this.state; const { network } = this.state;
const isHttps = window.location.protocol.includes('https'); const isHttps = window.location.protocol.includes('https');
const invalids = this.getInvalids(); const invalids = this.getInvalids();
@ -88,16 +90,21 @@ class CustomNodeModal extends React.Component<Props, State> {
]; ];
const conflictedNode = this.getConflictedNode(); const conflictedNode = this.getConflictedNode();
const staticNetwrks = Object.keys(staticNetworks).map(net => {
return { label: net, value: net };
});
const customNetwrks = Object.entries(customNetworks).map(([id, net]) => {
return { label: net.name + ' (Custom)', value: id };
});
const options = [...staticNetwrks, ...customNetwrks, CUSTOM];
return ( return (
<Modal <Modal
title={translate('NODE_Title')} title={translateRaw('NODE_Title')}
isOpen={true} isOpen={isOpen}
buttons={buttons} buttons={buttons}
handleClose={handleClose} handleClose={handleClose}
maxWidth={580} maxWidth={580}
> >
<div>
{isHttps && <div className="alert alert-warning small">{translate('NODE_Warning')}</div>} {isHttps && <div className="alert alert-warning small">{translate('NODE_Warning')}</div>}
{conflictedNode && ( {conflictedNode && (
@ -107,139 +114,128 @@ class CustomNodeModal extends React.Component<Props, State> {
</div> </div>
)} )}
<form> <form className="CustomNodeModal">
<div className="row"> <div className="flex-wrapper">
<div className="col-sm-7"> <label className="col-sm-9 input-group flex-grow-1">
<label>{translate('NODE_Name')}</label> <div className="input-group-header">Node Name</div>
{this.renderInput( <Input
{ className={`input-group-input ${this.state.name && invalids.name ? 'invalid' : ''}`}
name: 'name', type="text"
placeholder: 'My Node' placeholder="My Node"
}, value={this.state.name}
invalids onChange={e => this.setState({ name: e.currentTarget.value })}
)} />
</div> </label>
<div className="col-sm-5"> <label className="col-sm-3 input-group">
<label>Network</label> <div className="input-group-header">Network</div>
<select <Dropdown
className="form-control" className="input-group-dropdown"
name="network"
value={network} value={network}
onChange={this.handleChange} options={options}
> clearable={false}
{Object.keys(staticNetworks).map(net => ( onChange={(e: { label: string; value: string }) =>
<option key={net} value={net}> this.setState({ network: e.value })
{net} }
</option> />
))} </label>
{Object.entries(customNetworks).map(([id, net]) => (
<option key={id} value={id}>
{net.name} (Custom)
</option>
))}
<option value={CUSTOM}>Custom...</option>
</select>
</div>
</div> </div>
{network === CUSTOM && ( {network === CUSTOM.value && (
<div className="row"> <div className="flex-wrapper">
<div className="col-sm-6"> <label className="col-sm-6 input-group input-group-inline">
<label className="is-required">Network Name</label> <div className="input-group-header">Network Name</div>
{this.renderInput( <Input
{ className={`input-group-input ${
name: 'customNetworkId', this.state.customNetworkId && invalids.customNetworkId ? 'invalid' : ''
placeholder: 'My Custom Network' }`}
}, type="text"
invalids placeholder="My Custom Network"
)} value={this.state.customNetworkId}
</div> onChange={e => this.setState({ customNetworkId: e.currentTarget.value })}
<div className="col-sm-3"> />
<label className="is-required">Currency</label> </label>
{this.renderInput( <label className="col-sm-3 input-group input-group-inline">
{ <div className="input-group-header">Currency</div>
name: 'customNetworkUnit', <Input
placeholder: 'ETH' className={`input-group-input ${
}, this.state.customNetworkUnit && invalids.customNetworkUnit ? 'invalid' : ''
invalids }`}
)} type="text"
</div> placeholder="ETH"
<div className="col-sm-3"> value={this.state.customNetworkUnit}
<label>Chain ID</label> onChange={e => this.setState({ customNetworkUnit: e.currentTarget.value })}
{this.renderInput( />
{ </label>
name: 'customNetworkChainId', <label className="col-sm-3 input-group input-group-inline">
placeholder: 'e.g. 1' <div className="input-group-header">Chain ID</div>
}, <Input
invalids className={`input-group-input ${
)} this.state.customNetworkChainId && invalids.customNetworkChainId
</div> ? 'invalid'
: ''
}`}
type="text"
placeholder="1"
value={this.state.customNetworkChainId}
onChange={e => this.setState({ customNetworkChainId: e.currentTarget.value })}
/>
</label>
</div> </div>
)} )}
<div className="row"> <label className="input-group input-group-inline">
<div className="col-sm-12"> <div className="input-group-header">URL</div>
<label>URL</label> <Input
{this.renderInput( className={`input-group-input ${this.state.url && invalids.url ? 'invalid' : ''}`}
{ type="text"
name: 'url', placeholder="https://127.0.0.1:8545/"
placeholder: 'e.g. https://127.0.0.1:8545/', value={this.state.url}
autoComplete: 'off' onChange={e => this.setState({ url: e.currentTarget.value })}
}, autoComplete="off"
invalids />
)} </label>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<label> <label>
<input <input
type="checkbox" type="checkbox"
name="hasAuth" name="hasAuth"
checked={this.state.hasAuth} checked={this.state.hasAuth}
onChange={this.handleCheckbox} onChange={() => this.setState({ hasAuth: !this.state.hasAuth })}
/>{' '} />
<span>HTTP Basic Authentication</span> <span>HTTP Basic Authentication</span>
</label> </label>
</div>
</div>
{this.state.hasAuth && ( {this.state.hasAuth && (
<div className="row"> <div className="flex-wrapper ">
<div className="col-sm-6"> <label className="col-sm-6 input-group input-group-inline">
<label className="is-required">Username</label> <div className="input-group-header">Username</div>
{this.renderInput({ name: 'username' }, invalids)} <Input
</div> className={`input-group-input ${
<div className="col-sm-6"> this.state.username && invalids.username ? 'invalid' : ''
<label className="is-required">Password</label> }`}
{this.renderInput( type="text"
{ value={this.state.username}
name: 'password', onChange={e => this.setState({ username: e.currentTarget.value })}
type: 'password' />
}, </label>
invalids <label className="col-sm-6 input-group input-group-inline">
)} <div className="input-group-header">Password</div>
</div> <Input
className={`input-group-input ${
this.state.password && invalids.password ? 'invalid' : ''
}`}
type="password"
value={this.state.password}
onChange={e => this.setState({ password: e.currentTarget.value })}
/>
</label>
</div> </div>
)} )}
</form> </form>
</div>
</Modal> </Modal>
); );
} }
private renderInput(input: InputProps, invalids: { [key: string]: boolean }) {
return (
<Input
className={`${this.state[input.name] && invalids[input.name] ? 'invalid' : ''}`}
value={this.state[input.name]}
onChange={this.handleChange}
autoComplete="off"
{...input}
/>
);
}
private getInvalids(): { [key: string]: boolean } { private getInvalids(): { [key: string]: boolean } {
const { const {
url, url,
@ -278,7 +274,7 @@ class CustomNodeModal extends React.Component<Props, State> {
} }
// If they have a custom network, make sure info is provided // If they have a custom network, make sure info is provided
if (network === CUSTOM) { if (network === CUSTOM.value) {
if (!customNetworkId) { if (!customNetworkId) {
invalids.customNetworkId = true; invalids.customNetworkId = true;
} }
@ -315,7 +311,7 @@ class CustomNodeModal extends React.Component<Props, State> {
const { network, url, name, username, password } = this.state; const { network, url, name, username, password } = this.state;
const networkId = const networkId =
network === CUSTOM network === CUSTOM.value
? this.makeCustomNetworkId(this.makeCustomNetworkConfigFromState()) ? this.makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
: network; : network;
@ -348,20 +344,10 @@ class CustomNodeModal extends React.Component<Props, State> {
return customNodes[config.id]; return customNodes[config.id];
} }
private handleChange = (ev: React.FormEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = ev.currentTarget;
this.setState({ [name as any]: value });
};
private handleCheckbox = (ev: React.FormEvent<HTMLInputElement>) => {
const { name } = ev.currentTarget;
this.setState({ [name as any]: !this.state[name as keyof State] });
};
private saveAndAdd = () => { private saveAndAdd = () => {
const node = this.makeCustomNodeConfigFromState(); const node = this.makeCustomNodeConfigFromState();
if (this.state.network === CUSTOM) { if (this.state.network === CUSTOM.value) {
const network = this.makeCustomNetworkConfigFromState(); const network = this.makeCustomNetworkConfigFromState();
this.props.addCustomNetwork({ config: network, id: node.network }); this.props.addCustomNetwork({ config: network, id: node.network });

View File

@ -192,12 +192,11 @@ class Header extends Component<Props, State> {
<Navigation color={!network.isCustom && network.color} /> <Navigation color={!network.isCustom && network.color} />
{isAddingCustomNode && (
<CustomNodeModal <CustomNodeModal
isOpen={isAddingCustomNode}
addCustomNode={this.addCustomNode} addCustomNode={this.addCustomNode}
handleClose={this.closeCustomNodeModal} handleClose={this.closeCustomNodeModal}
/> />
)}
</div> </div>
); );
} }

View File

@ -100,8 +100,8 @@ export default class PaperWallet extends React.Component<Props, {}> {
return ( return (
<div style={styles.container}> <div style={styles.container}>
<img src={sidebarImg} style={styles.sidebar} /> <img src={sidebarImg} style={styles.sidebar} alt="MyCrypto Logo" />
<img src={ethLogo} style={styles.ethLogo} /> <img src={ethLogo} style={styles.ethLogo} alt="ETH Logo" />
<div style={styles.block}> <div style={styles.block}>
<div style={styles.box}> <div style={styles.box}>
@ -111,7 +111,7 @@ export default class PaperWallet extends React.Component<Props, {}> {
</div> </div>
<div style={styles.block}> <div style={styles.block}>
<img src={notesBg} style={styles.box} /> <img src={notesBg} style={styles.box} aria-hidden={true} />
<p style={styles.blockText}>AMOUNT / NOTES</p> <p style={styles.blockText}>AMOUNT / NOTES</p>
</div> </div>

View File

@ -18,6 +18,7 @@ interface Props {
isValid?: boolean; isValid?: boolean;
isVisible?: boolean; isVisible?: boolean;
validity?: 'valid' | 'invalid' | 'semivalid'; validity?: 'valid' | 'invalid' | 'semivalid';
readOnly?: boolean;
// Textarea-only props // Textarea-only props
isTextareaWhenVisible?: boolean; isTextareaWhenVisible?: boolean;
@ -61,18 +62,16 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
onChange, onChange,
onFocus, onFocus,
onBlur, onBlur,
handleToggleVisibility handleToggleVisibility,
readOnly
} = this.props; } = this.props;
const { isVisible } = this.state; const { isVisible } = this.state;
const validClass = validity
? `is-${validity}`
: isValid === null || isValid === undefined ? '' : isValid ? 'is-valid' : 'is-invalid';
return ( return (
<div className={`TogglablePassword input-group input-group-inline-dropdown ${className}`}> <div className={`TogglablePassword input-group input-group-inline ${className}`}>
{isTextareaWhenVisible && isVisible ? ( {isTextareaWhenVisible && isVisible ? (
<TextArea <TextArea
className={validClass} className={validity || !isValid ? 'invalid' : ''}
value={value} value={value}
name={name} name={name}
disabled={disabled} disabled={disabled}
@ -83,6 +82,7 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
placeholder={placeholder} placeholder={placeholder}
rows={this.props.rows || 3} rows={this.props.rows || 3}
aria-label={ariaLabel} aria-label={ariaLabel}
readOnly={readOnly}
/> />
) : ( ) : (
<Input <Input
@ -90,12 +90,13 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
name={name} name={name}
disabled={disabled} disabled={disabled}
type={isVisible ? 'text' : 'password'} type={isVisible ? 'text' : 'password'}
className={`${validClass}`} className={`${validity || !isValid ? 'invalid' : ''} border-rad-right-0`}
placeholder={placeholder} placeholder={placeholder}
onChange={onChange} onChange={onChange}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
aria-label={ariaLabel} aria-label={ariaLabel}
readOnly={readOnly}
/> />
)} )}
<span <span

View File

@ -40,7 +40,6 @@ $speed: 500ms;
margin: 0; margin: 0;
} }
&:last-child { &:last-child {
margin: 0; margin: 0;
} }
@ -80,7 +79,6 @@ $speed: 500ms;
} }
&:active { &:active {
outline: none;
opacity: 1; opacity: 1;
} }
@ -106,7 +104,7 @@ $speed: 500ms;
.DecryptContent { .DecryptContent {
&-enter { &-enter {
opacity: 0; opacity: 0;
transition: opacity $speed * .25 ease $speed * .125; transition: opacity $speed * 0.25 ease $speed * 0.125;
&-active { &-active {
opacity: 1; opacity: 1;
@ -119,7 +117,7 @@ $speed: 500ms;
left: 0; left: 0;
width: 100%; width: 100%;
opacity: 1; opacity: 1;
transition: opacity $speed * .25 ease; transition: opacity $speed * 0.25 ease;
pointer-events: none; pointer-events: none;
&-active { &-active {

View File

@ -29,7 +29,6 @@
transition: transform 150ms ease, box-shadow 150ms ease; transition: transform 150ms ease, box-shadow 150ms ease;
animation: wallet-button-enter 400ms ease 1; animation: wallet-button-enter 400ms ease 1;
animation-fill-mode: backwards; animation-fill-mode: backwards;
outline: none;
@for $i from 0 to 5 { @for $i from 0 to 5 {
&:nth-child(#{$i}) { &:nth-child(#{$i}) {
@ -60,7 +59,6 @@
} }
&.is-disabled { &.is-disabled {
outline: none;
cursor: not-allowed; cursor: not-allowed;
@include show-tooltip-on-hover; @include show-tooltip-on-hover;

View File

@ -28,6 +28,7 @@ interface Icon {
icon: string; icon: string;
tooltip: string; tooltip: string;
href?: string; href?: string;
arialabel: string;
} }
type Props = OwnProps & StateProps; type Props = OwnProps & StateProps;
@ -50,18 +51,21 @@ export class WalletButton extends React.PureComponent<Props> {
if (isReadOnly) { if (isReadOnly) {
icons.push({ icons.push({
icon: 'eye', icon: 'eye',
tooltip: translateRaw('You cannot send using address only') tooltip: translateRaw('You cannot send using address only'),
arialabel: 'Read Only'
}); });
} else { } else {
if (isSecure) { if (isSecure) {
icons.push({ icons.push({
icon: 'shield', icon: 'shield',
tooltip: translateRaw('This wallet type is secure') tooltip: translateRaw('This wallet type is secure'),
arialabel: 'Secure wallet type'
}); });
} else { } else {
icons.push({ icons.push({
icon: 'exclamation-triangle', icon: 'exclamation-triangle',
tooltip: translateRaw('This wallet type is insecure') tooltip: translateRaw('This wallet type is insecure'),
arialabel: 'Insecure wallet type'
}); });
} }
} }
@ -69,7 +73,8 @@ export class WalletButton extends React.PureComponent<Props> {
icons.push({ icons.push({
icon: 'question-circle', icon: 'question-circle',
tooltip: translateRaw('NAV_Help'), tooltip: translateRaw('NAV_Help'),
href: helpLink href: helpLink,
arialabel: 'More info'
}); });
} }
@ -86,22 +91,30 @@ export class WalletButton extends React.PureComponent<Props> {
> >
<div className="WalletButton-inner"> <div className="WalletButton-inner">
<div className="WalletButton-title"> <div className="WalletButton-title">
{icon && <img className="WalletButton-title-icon" src={icon} />} {icon && <img className="WalletButton-title-icon" src={icon} alt={name + ' logo'} />}
<span>{name}</span> <span>{name}</span>
</div> </div>
{description && <div className="WalletButton-description">{description}</div>} {description && (
{example && <div className="WalletButton-example">{example}</div>} <div className="WalletButton-description" aria-label="description">
{description}
</div>
)}
{example && (
<div className="WalletButton-example" aria-label="example" aria-hidden={true}>
{example}
</div>
)}
<div className="WalletButton-icons"> <div className="WalletButton-icons">
{icons.map(i => ( {icons.map(i => (
<span className="WalletButton-icons-icon" key={i.icon} onClick={this.stopPropogation}> <span className="WalletButton-icons-icon" key={i.icon} onClick={this.stopPropogation}>
{i.href ? ( {i.href ? (
<NewTabLink href={i.href} onClick={this.stopPropogation}> <NewTabLink href={i.href} onClick={this.stopPropogation} aria-label={i.arialabel}>
<i className={`fa fa-${i.icon}`} /> <i className={`fa fa-${i.icon}`} />
</NewTabLink> </NewTabLink>
) : ( ) : (
<i className={`fa fa-${i.icon}`} /> <i className={`fa fa-${i.icon}`} aria-label={i.arialabel} />
)} )}
{!isDisabled && <Tooltip size="sm">{i.tooltip}</Tooltip>} {!isDisabled && <Tooltip size="sm">{i.tooltip}</Tooltip>}
</span> </span>

View File

@ -92,6 +92,7 @@ export default class ColorDropdown<T> extends PureComponent<Props<T>, {}> {
className="ColorDropdown-item-remove" className="ColorDropdown-item-remove"
onClick={this.onRemove.bind(null, option.onRemove)} onClick={this.onRemove.bind(null, option.onRemove)}
src={removeIcon} src={removeIcon}
alt="remove"
/> />
)} )}
</a> </a>

View File

@ -12,7 +12,7 @@ interface Props {
const Help = ({ size = 'x1', link }: Props) => { const Help = ({ size = 'x1', link }: Props) => {
return ( return (
<a href={link} className={`Help Help-${size}`} target="_blank" rel="noopener noreferrer"> <a href={link} className={`Help Help-${size}`} target="_blank" rel="noopener noreferrer">
<img src={icon} /> <img src={icon} alt="help" />
</a> </a>
); );
}; };

View File

@ -24,6 +24,7 @@ export default function Identicon(props: Props) {
<React.Fragment> <React.Fragment>
<img <img
src={identiconDataUrl} src={identiconDataUrl}
alt="Unique Address Image"
style={{ style={{
height: '100%', height: '100%',
width: '100%', width: '100%',

View File

@ -12,6 +12,22 @@
> .TogglablePassword { > .TogglablePassword {
width: 100%; width: 100%;
} }
&-inline {
display: flex;
flex-direction: row;
font-size: 1rem;
flex-wrap: wrap;
> .input-group-header {
width: 100%;
}
> .input-group-input {
flex-grow: 1;
width: auto;
}
> .Select {
margin-left: 8px;
}
}
&-header { &-header {
display: flex; display: flex;
font-size: 1rem; font-size: 1rem;
@ -29,6 +45,9 @@
color: rgba(0, 0, 0, 0.54); color: rgba(0, 0, 0, 0.54);
} }
} }
&-dropdown {
margin-bottom: 1rem;
}
&-input { &-input {
width: 100%; width: 100%;
border: 1px solid #e5ecf3; border: 1px solid #e5ecf3;
@ -40,6 +59,17 @@
box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05); box-shadow: inset 0 1px 0 0 rgba(63, 63, 68, 0.05);
transition: border-color 120ms, box-shadow 120ms; transition: border-color 120ms, box-shadow 120ms;
margin-bottom: 1rem; margin-bottom: 1rem;
&.border-rad-right-0 {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&.border-rad-left-0 {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&-small {
padding: 0.5rem 0.75rem;
}
&::placeholder { &::placeholder {
color: rgba(0, 0, 0, 0.3); color: rgba(0, 0, 0, 0.3);
} }
@ -60,23 +90,6 @@
} }
} }
.input-group-inline-dropdown {
display: flex;
flex-direction: row;
font-size: 1rem;
flex-wrap: wrap;
> .input-group-header {
width: 100%;
}
> .input-group-input {
flex-grow: 1;
width: auto;
}
> .Select {
margin-left: 8px;
}
}
.Swap-dropdown { .Swap-dropdown {
.Select-input { .Select-input {
left: 24px; left: 24px;

View File

@ -1,149 +0,0 @@
import closeIcon from 'assets/images/close.svg';
import React, { PureComponent } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import './Modal.scss';
export interface IButton {
text: string | React.ReactElement<string>;
type?: 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'link';
disabled?: boolean;
onClick?(): void;
}
interface Props {
isOpen?: boolean;
title?: string | React.ReactElement<any>;
disableButtons?: boolean;
children: any;
buttons?: IButton[];
maxWidth?: number;
handleClose?(): void;
}
interface ModalStyle {
width?: string;
maxWidth?: string;
}
const Fade = ({ children, ...props }: any) => (
<CSSTransition {...props} timeout={300} classNames="animate-modal">
{children}
</CSSTransition>
);
export default class Modal extends PureComponent<Props, {}> {
private modalContent: HTMLElement | null = null;
public componentDidMount() {
this.updateBodyClass();
document.addEventListener('keydown', this.escapeListner);
}
public componentDidUpdate() {
this.updateBodyClass();
}
public updateBodyClass() {
document.body.classList.toggle('no-scroll', !!this.props.isOpen);
}
public componentWillUnmount() {
document.removeEventListener('keydown', this.escapeListner);
document.body.classList.remove('no-scroll');
}
public render() {
const { isOpen, title, children, buttons, handleClose, maxWidth } = this.props;
const hasButtons = buttons && buttons.length;
const modalStyle: ModalStyle = {};
if (maxWidth) {
modalStyle.width = '100%';
modalStyle.maxWidth = `${maxWidth}px`;
}
return (
<TransitionGroup>
{isOpen && (
<Fade>
<div>
<div className="Modalshade" />
<div className="Modal" style={modalStyle}>
{title && (
<div className="Modal-header flex-wrapper">
<h2 className="Modal-header-title">{title}</h2>
<div className="flex-spacer" />
<button className="Modal-header-close" onClick={handleClose}>
<img className="Modal-header-close-icon" src={closeIcon} />
</button>
</div>
)}
<div className="Modal-content" ref={el => (this.modalContent = el)}>
{isOpen && children}
<div className="Modal-fade" />
</div>
{hasButtons && <div className="Modal-footer">{this.renderButtons()}</div>}
</div>
</div>
</Fade>
)}
</TransitionGroup>
);
}
public scrollContentToTop = () => {
if (this.modalContent) {
this.modalContent.scrollTop = 0;
}
};
private escapeListner = (ev: KeyboardEvent) => {
if (!this.props.isOpen) {
return;
}
// Don't trigger if they hit escape while on an input
if (ev.target) {
if (
(ev.target as HTMLElement).tagName === 'INPUT' ||
(ev.target as HTMLElement).tagName === 'SELECT' ||
(ev.target as HTMLElement).tagName === 'TEXTAREA' ||
(ev.target as HTMLElement).isContentEditable
) {
return;
}
}
if (ev.key === 'Escape' || ev.keyCode === 27) {
if (!this.props.handleClose) {
return;
}
this.props.handleClose();
}
};
private renderButtons = () => {
const { disableButtons, buttons } = this.props;
if (!buttons || !buttons.length) {
return;
}
return buttons.map((btn, idx) => {
let btnClass = 'Modal-footer-btn btn';
if (btn.type) {
btnClass += ` btn-${btn.type}`;
}
return (
<button
className={btnClass}
onClick={btn.onClick}
key={idx}
disabled={disableButtons || btn.disabled}
>
{btn.text}
</button>
);
});
};
}

View File

@ -0,0 +1,131 @@
import React, { CSSProperties } from 'react';
import closeIcon from 'assets/images/close.svg';
import { IButton } from 'components/ui/Modal';
interface Props {
title?: string;
children: any;
modalStyle?: CSSProperties;
hasButtons?: number;
buttons?: IButton[];
disableButtons?: any;
handleClose(): void;
}
export default class ModalBody extends React.Component<Props> {
private modal: HTMLElement;
private modalContent: HTMLElement;
private focusedElementBeforeModal: HTMLElement;
private firstTabStop: HTMLElement;
private lastTabStop: HTMLElement;
public componentDidMount() {
this.focusedElementBeforeModal = document.activeElement as HTMLElement;
// Find all focusable children
const focusableElementsString =
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
const focusableElements = Array.prototype.slice.call(
this.modal.querySelectorAll(focusableElementsString)
);
// Convert NodeList to Array
this.firstTabStop = focusableElements[0];
this.lastTabStop = focusableElements[focusableElements.length - 1];
// Focus first child
this.firstTabStop.focus();
this.modal.addEventListener('keydown', this.keyDownListener);
}
public componentWillUnmount() {
document.removeEventListener('keydown', this.keyDownListener);
}
public scrollContentToTop = () => {
this.modalContent.scrollTop = 0;
};
public render() {
const { title, children, modalStyle, hasButtons, handleClose } = this.props;
return (
<div
className="Modal"
style={modalStyle}
role="dialog"
aria-labelledby="Modal-header-title"
ref={div => {
this.modal = div as HTMLElement;
}}
>
{title && (
<div className="Modal-header flex-wrapper">
<h2 className="Modal-header-title">{title}</h2>
<div className="flex-spacer" />
<button className="Modal-header-close" aria-label="Close" onClick={handleClose}>
<img className="Modal-header-close-icon" src={closeIcon} alt="Close" />
</button>
</div>
)}
<div className="Modal-content" ref={div => (this.modalContent = div as HTMLElement)}>
{children}
<div className="Modal-fade" />
</div>
{hasButtons && <div className="Modal-footer">{this.renderButtons()}</div>}
</div>
);
}
private renderButtons = () => {
const { disableButtons, buttons } = this.props;
if (!buttons || !buttons.length) {
return;
}
return buttons.map((btn, idx: number) => {
let btnClass = 'Modal-footer-btn btn';
if (btn.type) {
btnClass += ` btn-${btn.type}`;
}
return (
<button
className={btnClass}
onClick={btn.onClick}
key={idx}
disabled={disableButtons || btn.disabled}
>
{btn.text}
</button>
);
});
};
private keyDownListener = (e: KeyboardEvent) => {
// Check for TAB key press
if (e.keyCode === 9) {
// SHIFT + TAB
if (e.shiftKey) {
if (document.activeElement === this.firstTabStop) {
e.preventDefault();
this.lastTabStop.focus();
}
// TAB
} else {
if (document.activeElement === this.lastTabStop) {
e.preventDefault();
this.firstTabStop.focus();
}
}
}
// Check for ESC key press
if (e.keyCode === 27) {
this.focusedElementBeforeModal.focus();
this.props.handleClose();
}
};
}

View File

@ -13,17 +13,6 @@ $m-footer-padding: 0.5rem 2rem 1rem 2rem;
$m-close-size: 26px; $m-close-size: 26px;
$m-anim-speed: 400ms; $m-anim-speed: 400ms;
.Modalshade {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(#000, 0.54);
z-index: $zindex-modal-background;
display: block;
}
.Modal { .Modal {
position: fixed; position: fixed;
top: $m-window-padding-h; top: $m-window-padding-h;
@ -45,6 +34,17 @@ $m-anim-speed: 400ms;
box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14),
0px 6px 30px 5px rgba(0, 0, 0, 0.12); 0px 6px 30px 5px rgba(0, 0, 0, 0.12);
&-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(#000, 0.54);
z-index: $zindex-modal-background;
display: block;
}
&-fade { &-fade {
background: linear-gradient(to bottom, #fff0, #fff); background: linear-gradient(to bottom, #fff0, #fff);
position: fixed; position: fixed;
@ -112,7 +112,7 @@ $m-anim-speed: 400ms;
} }
// Mobile styles // Mobile styles
@media(max-width: $screen-sm) { @media (max-width: $screen-sm) {
top: $m-window-padding-h-mobile; top: $m-window-padding-h-mobile;
width: calc(100% - #{$m-window-padding-w-mobile}) !important; width: calc(100% - #{$m-window-padding-w-mobile}) !important;
max-width: calc(100% - #{$m-window-padding-w-mobile * 2}); max-width: calc(100% - #{$m-window-padding-w-mobile * 2});

View File

@ -0,0 +1,70 @@
import React, { PureComponent } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import ModalBody from './ModalBody';
import './index.scss';
export interface IButton {
text: string | React.ReactElement<string>;
type?: 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger' | 'link';
disabled?: boolean;
onClick?(): void;
}
interface Props {
isOpen?: boolean;
title?: string;
disableButtons?: boolean;
children: any;
buttons?: IButton[];
maxWidth?: number;
handleClose(): void;
}
interface ModalStyle {
width?: string;
maxWidth?: string;
}
const Fade = ({ children, ...props }: any) => (
<CSSTransition {...props} timeout={300} classNames="animate-modal">
{children}
</CSSTransition>
);
export default class Modal extends PureComponent<Props, {}> {
public modalBody: ModalBody;
public componentDidUpdate(prevProps: Props) {
if (prevProps.isOpen !== this.props.isOpen) {
document.body.classList.toggle('no-scroll', !!this.props.isOpen);
}
}
public componentWillUnmount() {
document.body.classList.remove('no-scroll');
}
public render() {
const { isOpen, title, children, buttons, handleClose, maxWidth } = this.props;
const hasButtons = buttons && buttons.length;
const modalStyle: ModalStyle = {};
if (maxWidth) {
modalStyle.width = '100%';
modalStyle.maxWidth = `${maxWidth}px`;
}
const modalBodyProps = { title, children, modalStyle, hasButtons, buttons, handleClose };
return (
<TransitionGroup>
{isOpen && (
<Fade>
<div>
<div className="Modal-overlay" onClick={handleClose} />
<ModalBody {...modalBodyProps} ref={div => (this.modalBody = div as ModalBody)} />
</div>
</Fade>
)}
</TransitionGroup>
);
}
}

View File

@ -26,7 +26,7 @@ const OfflineSymbol = ({ offline, size }: OfflineSymbolProps) => {
break; break;
} }
return <img src={offline ? wifiOff : wifiOn} width={width} height={height} />; return <img src={offline ? wifiOff : wifiOn} alt="wifi status" width={width} height={height} />;
}; };
export default OfflineSymbol; export default OfflineSymbol;

View File

@ -35,6 +35,7 @@ export default class QRCode extends React.PureComponent<Props, State> {
return ( return (
<img <img
src={qr} src={qr}
alt="QR Code"
style={{ style={{
width: '100%', width: '100%',
height: '100%' height: '100%'

View File

@ -11,7 +11,7 @@ interface SpinnerProps {
const Spinner = ({ size = 'x1', light = false }: SpinnerProps) => { const Spinner = ({ size = 'x1', light = false }: SpinnerProps) => {
const color = light ? 'Spinner-light' : 'Spinner-dark'; const color = light ? 'Spinner-light' : 'Spinner-dark';
return ( return (
<svg className={`Spinner Spinner-${size} ${color}`} viewBox="0 0 50 50"> <svg className={`Spinner Spinner-${size} ${color}`} viewBox="0 0 50 50" aria-busy="true">
<circle className="path" cx="25" cy="25" r="20" fill="none" strokeWidth="5" /> <circle className="path" cx="25" cy="25" r="20" fill="none" strokeWidth="5" />
</svg> </svg>
); );

View File

@ -8,9 +8,6 @@
padding: 0.4rem 1rem; padding: 0.4rem 1rem;
border-radius: 2px; border-radius: 2px;
height: 2.5rem; height: 2.5rem;
&:focus {
outline: none;
}
&:active, &:active,
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;

View File

@ -18,7 +18,7 @@ interface Props<T> {
const ValueComp: React.SFC = (props: any) => { const ValueComp: React.SFC = (props: any) => {
return ( return (
<div className={`${props.className} swap-option-wrapper`}> <div className={`${props.className} swap-option-wrapper`}>
<img src={props.value.img} className="swap-option-img" /> <img src={props.value.img} className="swap-option-img" alt={props.value.label + ' logo'} />
<span className="swap-option-label">{props.value.label}</span> <span className="swap-option-label">{props.value.label}</span>
</div> </div>
); );
@ -46,7 +46,7 @@ const OptionComp: React.SFC = (props: any) => {
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
> >
<img src={props.option.img} className="swap-option-img" /> <img src={props.option.img} className="swap-option-img" alt={props.option.label + ' logo'} />
<span className="swap-option-label">{props.option.label}</span> <span className="swap-option-label">{props.option.label}</span>
</div> </div>
); );

View File

@ -120,7 +120,11 @@ class OnboardModal extends React.Component<Props, State> {
return ( return (
<div className="OnboardModal"> <div className="OnboardModal">
<Modal isOpen={isOpen} buttons={buttons} ref={el => (this.modal = el)}> <Modal
isOpen={isOpen}
buttons={buttons}
handleClose={() => (slideNumber === NUMBER_OF_SLIDES ? this.closeModal : null)}
>
<div className="OnboardModal-stepper"> <div className="OnboardModal-stepper">
<Stepper <Stepper
steps={steps} steps={steps}
@ -171,7 +175,7 @@ class OnboardModal extends React.Component<Props, State> {
localStorage.setItem(ONBOARD_LOCAL_STORAGE_KEY, String(prevSlideNum)); localStorage.setItem(ONBOARD_LOCAL_STORAGE_KEY, String(prevSlideNum));
this.props.decrementSlide(); this.props.decrementSlide();
if (this.modal) { if (this.modal) {
this.modal.scrollContentToTop(); this.modal.modalBody.scrollContentToTop();
} }
}; };
@ -180,7 +184,7 @@ class OnboardModal extends React.Component<Props, State> {
localStorage.setItem(ONBOARD_LOCAL_STORAGE_KEY, String(nextSlideNum)); localStorage.setItem(ONBOARD_LOCAL_STORAGE_KEY, String(nextSlideNum));
this.props.incrementSlide(); this.props.incrementSlide();
if (this.modal) { if (this.modal) {
this.modal.scrollContentToTop(); this.modal.modalBody.scrollContentToTop();
} }
}; };
} }

View File

@ -14,7 +14,7 @@ interface Props {
export class Notifications extends React.Component<Props, {}> { export class Notifications extends React.Component<Props, {}> {
public render() { public render() {
return ( return (
<TransitionGroup className="Notifications"> <TransitionGroup className="Notifications" aria-live="polite">
{this.props.notifications.map(n => { {this.props.notifications.map(n => {
return ( return (
<CSSTransition classNames="NotificationAnimation" timeout={500} key={n.id}> <CSSTransition classNames="NotificationAnimation" timeout={500} key={n.id}>

View File

@ -33,10 +33,12 @@ class NameInput extends Component<Props, State> {
return ( return (
<form className="ENSInput" onSubmit={this.onSubmit}> <form className="ENSInput" onSubmit={this.onSubmit}>
<div className="input-group-wrapper"> <div className="input-group-wrapper">
<label className="input-group input-group-inline-dropdown ENSInput-name"> <label className="input-group input-group-inline ENSInput-name">
<Input <Input
value={domainToCheck} value={domainToCheck}
className={!domainToCheck ? '' : isValidDomain ? 'is-valid' : 'is-invalid'} className={`${
!domainToCheck ? '' : isValidDomain ? '' : 'invalid'
} border-rad-right-0`}
type="text" type="text"
placeholder="mycrypto" placeholder="mycrypto"
onChange={this.onChange} onChange={this.onChange}

View File

@ -53,7 +53,11 @@ const CryptoWarning: React.SFC<{}> = () => (
className="CryptoWarning-browsers-browser" className="CryptoWarning-browsers-browser"
> >
<div> <div>
<img className="CryptoWarning-browsers-browser-icon" src={browser.icon} /> <img
className="CryptoWarning-browsers-browser-icon"
src={browser.icon}
alt={browser.name + ' logo'}
/>
<div className="CryptoWarning-browsers-browser-name">{browser.name}</div> <div className="CryptoWarning-browsers-browser-name">{browser.name}</div>
</div> </div>
</NewTabLink> </NewTabLink>

View File

@ -1,4 +1,4 @@
@import "common/sass/variables"; @import 'common/sass/variables';
.GenPaper { .GenPaper {
&-title { &-title {
@ -6,8 +6,17 @@
} }
&-private { &-private {
max-width: 700px; max-width: 680px;
margin: 0 auto $space * 3; margin: 0 auto $space * 3;
margin-top: 12px;
> .input-group-header {
margin-bottom: 1rem;
// This selector is an exception, it targets the span returned using `translate`.
> span {
font-size: 2rem;
margin: auto;
}
}
} }
&-paper, &-paper,

View File

@ -1,7 +1,7 @@
import PrintableWallet from 'components/PrintableWallet'; import PrintableWallet from 'components/PrintableWallet';
import { IV3Wallet } from 'ethereumjs-wallet'; import { IV3Wallet } from 'ethereumjs-wallet';
import React from 'react'; import React from 'react';
import translate from 'translations'; import translate, { translateRaw } from 'translations';
import { stripHexPrefix } from 'libs/values'; import { stripHexPrefix } from 'libs/values';
import './PaperWallet.scss'; import './PaperWallet.scss';
import Template from '../Template'; import Template from '../Template';
@ -17,18 +17,20 @@ const PaperWallet: React.SFC<Props> = props => (
<Template> <Template>
<div className="GenPaper"> <div className="GenPaper">
{/* Private Key */} {/* Private Key */}
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1> <label className="input-group GenPaper-private">
{/* translateRaw isn't used here because it wont properly render the ` characters as a string of code in markdown*/}
<h1 className="input-group-header">{translate('GEN_Label_5')}</h1>
<Input <Input
className="GenPaper-private"
value={stripHexPrefix(props.privateKey)} value={stripHexPrefix(props.privateKey)}
aria-label={translate('x_PrivKey', true)} aria-label={translateRaw('x_PrivKey')}
aria-describedby="x_PrivKeyDesc" aria-describedby="x_PrivKeyDesc"
type="text" type="text"
readOnly={true} readOnly={true}
/> />
</label>
{/* Download Paper Wallet */} {/* Download Paper Wallet */}
<h1 className="GenPaper-title">{translate('x_Print')}</h1> <h2 className="GenPaper-title">{translate('x_Print')}</h2>
<div className="GenPaper-paper"> <div className="GenPaper-paper">
<PrintableWallet address={props.keystore.address} privateKey={props.privateKey} /> <PrintableWallet address={props.keystore.address} privateKey={props.privateKey} />
</div> </div>

View File

@ -28,10 +28,10 @@ export default class MnemonicWord extends React.Component<Props, State> {
return ( return (
<div className="input-group-wrapper MnemonicWord"> <div className="input-group-wrapper MnemonicWord">
<label className="input-group input-group-inline-dropdown ENSInput-name"> <label className="input-group input-group-inline ENSInput-name">
<span className="input-group-addon input-group-addon--transparent">{index + 1}.</span> <span className="input-group-addon input-group-addon--transparent">{index + 1}.</span>
<Input <Input
className={classnames('MnemonicWord-word-input', word === value && 'valid')} className={`MnemonicWord-word-input ${!isReadOnly && 'border-rad-right-0'}`}
value={readOnly ? word : value} value={readOnly ? word : value}
onChange={this.handleChange} onChange={this.handleChange}
readOnly={readOnly} readOnly={readOnly}

View File

@ -14,7 +14,6 @@
top: 36px; top: 36px;
left: 30px; left: 30px;
opacity: 0.3; opacity: 0.3;
outline: none;
color: $text-color; color: $text-color;
@media (max-width: $screen-sm) { @media (max-width: $screen-sm) {

View File

@ -341,7 +341,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
<div className="flex-spacer" /> <div className="flex-spacer" />
<div className="input-group-wrapper"> <div className="input-group-wrapper">
<div className="input-group-header">Deposit</div> <div className="input-group-header">Deposit</div>
<label className="input-group input-group-inline-dropdown"> <label className="input-group input-group-inline">
<Input <Input
id="origin-swap-input" id="origin-swap-input"
className={`input-group-input ${ className={`input-group-input ${
@ -365,7 +365,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
</div> </div>
<div className="input-group-wrapper"> <div className="input-group-wrapper">
<label className="input-group input-group-inline-dropdown"> <label className="input-group input-group-inline">
<div className="input-group-header">Recieve</div> <div className="input-group-header">Recieve</div>
<Input <Input
id="destination-swap-input" id="destination-swap-input"

View File

@ -132,7 +132,7 @@ class CurrentRates extends PureComponent<Props> {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<img src={providerLogo} width={120} height={49} /> <img src={providerLogo} width={120} height={49} alt="Shapeshift Logo" />
</a> </a>
</section> </section>
</article> </article>

View File

@ -1,7 +1,7 @@
import { RestartSwapAction } from 'actions/swap'; import { RestartSwapAction } from 'actions/swap';
import bityLogo from 'assets/images/logo-bity.svg'; import bityLogo from 'assets/images/logo-bity.svg';
import shapeshiftLogo from 'assets/images/shapeshift-dark.svg'; import shapeshiftLogo from 'assets/images/shapeshift-dark.svg';
import { bityReferralURL } from 'config'; import { shapeshiftReferralURL, bitboxReferralURL } from 'config';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import translate from 'translations'; import translate from 'translations';
import './SwapInfoHeader.scss'; import './SwapInfoHeader.scss';
@ -29,11 +29,11 @@ export default class SwapInfoHeaderTitle extends PureComponent<SwapInfoHeaderTit
<div className="col-xs-3"> <div className="col-xs-3">
<a <a
className="SwapInfo-top-logo" className="SwapInfo-top-logo"
href={bityReferralURL} href={provider === 'shapeshift' ? shapeshiftReferralURL : bitboxReferralURL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<img className="SwapInfo-top-logo-img" src={logoToRender} /> <img className="SwapInfo-top-logo-img" src={logoToRender} alt={provider + ' logo'} />
</a> </a>
</div> </div>
</section> </section>

View File

@ -39,3 +39,8 @@
[data-whatintent='mouse'] *:focus { [data-whatintent='mouse'] *:focus {
outline: none; outline: none;
} }
// This is fine because the outline effect is reproduced with border and box-shadow styles on input elements
input {
outline: none;
}

View File

@ -7,6 +7,19 @@
flex-wrap: nowrap; flex-wrap: nowrap;
} }
} }
.flex-grow {
&-1 {
flex-grow: 1;
}
&-2 {
flex-grow: 2;
}
&-3 {
flex-grow: 3;
}
}
.flex-spacer { .flex-spacer {
flex-grow: 2; flex-grow: 2;
} }

View File

@ -8,9 +8,3 @@
.btn-group > .btn-group { .btn-group > .btn-group {
float: left; float: left;
} }
// On active and open, don't show outline
.btn-group .dropdown-toggle:active,
.btn-group.open .dropdown-toggle {
outline: 0;
}

View File

@ -35,7 +35,6 @@ input[readonly] {
&:focus { &:focus {
border-color: $input-border-focus; border-color: $input-border-focus;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba($brand-primary, 0.5); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba($brand-primary, 0.5);
} }
} }

View File

@ -60,6 +60,9 @@
border-bottom-right-radius: 2px; border-bottom-right-radius: 2px;
border-color: inherit; border-color: inherit;
} }
&-menu {
max-height: 8.625rem;
}
&.invalid.has-blurred { &.invalid.has-blurred {
border-color: $brand-danger; border-color: $brand-danger;
box-shadow: inset 0px 0px 0px 1px $brand-danger; box-shadow: inset 0px 0px 0px 1px $brand-danger;