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:
parent
6e8d807b22
commit
9cac0298a2
|
@ -1,36 +1,35 @@
|
|||
// Mixins
|
||||
// --------------------------------------------------
|
||||
// Utilities
|
||||
@import "mixins/hide-text.less";
|
||||
@import "mixins/opacity.less";
|
||||
@import "mixins/image.less";
|
||||
@import "mixins/labels.less";
|
||||
@import "mixins/reset-filter.less";
|
||||
@import "mixins/resize.less";
|
||||
@import "mixins/responsive-visibility.less";
|
||||
@import "mixins/size.less";
|
||||
@import "mixins/tab-focus.less";
|
||||
@import "mixins/reset-text.less";
|
||||
@import "mixins/text-emphasis.less";
|
||||
@import "mixins/text-overflow.less";
|
||||
@import "mixins/vendor-prefixes.less";
|
||||
@import 'mixins/hide-text.less';
|
||||
@import 'mixins/opacity.less';
|
||||
@import 'mixins/image.less';
|
||||
@import 'mixins/labels.less';
|
||||
@import 'mixins/reset-filter.less';
|
||||
@import 'mixins/resize.less';
|
||||
@import 'mixins/responsive-visibility.less';
|
||||
@import 'mixins/size.less';
|
||||
@import 'mixins/reset-text.less';
|
||||
@import 'mixins/text-emphasis.less';
|
||||
@import 'mixins/text-overflow.less';
|
||||
@import 'mixins/vendor-prefixes.less';
|
||||
// Components
|
||||
@import "mixins/alerts.less";
|
||||
@import "mixins/buttons.less";
|
||||
@import "mixins/panels.less";
|
||||
@import "mixins/pagination.less";
|
||||
@import "mixins/list-group.less";
|
||||
@import "mixins/nav-divider.less";
|
||||
@import "mixins/forms.less";
|
||||
@import "mixins/progress-bar.less";
|
||||
@import "mixins/table-row.less";
|
||||
@import 'mixins/alerts.less';
|
||||
@import 'mixins/buttons.less';
|
||||
@import 'mixins/panels.less';
|
||||
@import 'mixins/pagination.less';
|
||||
@import 'mixins/list-group.less';
|
||||
@import 'mixins/nav-divider.less';
|
||||
@import 'mixins/forms.less';
|
||||
@import 'mixins/progress-bar.less';
|
||||
@import 'mixins/table-row.less';
|
||||
// Skins
|
||||
@import "mixins/background-variant.less";
|
||||
@import "mixins/border-radius.less";
|
||||
@import "mixins/gradients.less";
|
||||
@import 'mixins/background-variant.less';
|
||||
@import 'mixins/border-radius.less';
|
||||
@import 'mixins/gradients.less';
|
||||
// Layout
|
||||
@import "mixins/clearfix.less";
|
||||
@import "mixins/center-block.less";
|
||||
@import "mixins/nav-vertical-align.less";
|
||||
@import "mixins/grid-framework.less";
|
||||
@import "mixins/grid.less";
|
||||
@import 'mixins/clearfix.less';
|
||||
@import 'mixins/center-block.less';
|
||||
@import 'mixins/nav-vertical-align.less';
|
||||
@import 'mixins/grid-framework.less';
|
||||
@import 'mixins/grid.less';
|
||||
|
|
|
@ -20,10 +20,10 @@
|
|||
// Set the border and box shadow on specific inputs to match
|
||||
.form-control {
|
||||
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 {
|
||||
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
|
||||
|
@ -51,11 +51,10 @@
|
|||
// Example usage: change the default blue border and shadow to white for better
|
||||
// contrast against a dark gray background.
|
||||
.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 {
|
||||
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}');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
// WebKit-style focus
|
||||
|
||||
.tab-focus() {
|
||||
outline: thin dotted;
|
||||
outline-offset: 3px;
|
||||
}
|
|
@ -7,10 +7,6 @@
|
|||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
&:hover,
|
||||
&:active {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export const AmountField: React.SFC<Props> = ({
|
|||
<AmountFieldFactory
|
||||
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
|
||||
<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>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
|
|
|
@ -17,7 +17,7 @@ export const Coinbase: React.SFC<Props> = ({ address }) => (
|
|||
<h5 key="2">Buy ETH with USD</h5>
|
||||
</div>
|
||||
<div className="Promos-promo-images">
|
||||
<img src={CoinbaseLogo} />
|
||||
<img src={CoinbaseLogo} alt="Coinbase logo" />
|
||||
</div>
|
||||
</div>
|
||||
</NewTabLink>
|
||||
|
|
|
@ -11,8 +11,8 @@ export const HardwareWallets: React.SFC = () => (
|
|||
<h6>Learn more about protecting your funds.</h6>
|
||||
</div>
|
||||
<div className="Promos-promo-images">
|
||||
<img src={ledgerLogo} />
|
||||
<img src={trezorLogo} />
|
||||
<img src={ledgerLogo} alt="Ledger Logo" />
|
||||
<img src={trezorLogo} alt="Trezor Logo" />
|
||||
</div>
|
||||
</div>
|
||||
</HelpLink>
|
||||
|
|
|
@ -13,7 +13,7 @@ export const Shapeshift: React.SFC = () => (
|
|||
</h5>
|
||||
</div>
|
||||
<div className="Promos-promo-images">
|
||||
<img src={ShapeshiftLogo} />
|
||||
<img src={ShapeshiftLogo} alt="Shapeshift Logo" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
height: 12px;
|
||||
border: 3px solid $gray-lightest;
|
||||
border-radius: 100%;
|
||||
outline: none;
|
||||
opacity: 0.6;
|
||||
|
||||
&.is-active {
|
||||
|
@ -96,7 +95,7 @@
|
|||
|
||||
// Per-promo customizations
|
||||
&--shapeshift {
|
||||
background-color: #263A52;
|
||||
background-color: #263a52;
|
||||
|
||||
.Promos-promo-images {
|
||||
max-width: 130px;
|
||||
|
|
|
@ -66,11 +66,11 @@ export default class AddCustomTokenForm extends React.PureComponent<Props, State
|
|||
{fields.map(field => {
|
||||
return (
|
||||
<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
|
||||
className={`${
|
||||
errors[field.name] ? 'invalid' : field.value ? 'valid' : ''
|
||||
} AddCustom-field-input input-sm`}
|
||||
} input-group-input-small`}
|
||||
type="text"
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
|
|
|
@ -54,6 +54,7 @@ export default class TokenRow extends React.PureComponent<Props, State> {
|
|||
{!!custom && (
|
||||
<img
|
||||
src={removeIcon}
|
||||
alt="Remove"
|
||||
className="TokenRow-symbol-remove"
|
||||
title="Remove Token"
|
||||
onClick={this.onRemove}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@import "common/sass/variables";
|
||||
@import "common/sass/mixins";
|
||||
@import 'common/sass/variables';
|
||||
@import 'common/sass/mixins';
|
||||
|
||||
.BetaAgreement {
|
||||
@include cover-message;
|
||||
|
@ -20,7 +20,6 @@
|
|||
margin: 0 auto;
|
||||
border: none;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
transition: $transition;
|
||||
|
||||
&.is-continue {
|
||||
|
|
|
@ -55,13 +55,13 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
|
|||
|
||||
return (
|
||||
<Modal
|
||||
title={translate('Generate Keystore File')}
|
||||
title={translateRaw('Generate Keystore File')}
|
||||
isOpen={this.props.isOpen}
|
||||
handleClose={this.handleClose}
|
||||
>
|
||||
<form className="GenKeystore" onSubmit={this.handleSubmit}>
|
||||
<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>
|
||||
<TogglablePassword
|
||||
name="privateKey"
|
||||
|
@ -74,7 +74,7 @@ export default class GenerateKeystoreModal extends React.Component<Props, State>
|
|||
</label>
|
||||
</div>
|
||||
<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>
|
||||
<TogglablePassword
|
||||
name="password"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import Modal, { IButton } from 'components/ui/Modal';
|
||||
import translate from 'translations';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import { CustomNetworkConfig } from 'types/network';
|
||||
import { CustomNodeConfig } from 'types/node';
|
||||
import { TAddCustomNetwork, addCustomNetwork, AddCustomNodeAction } from 'actions/config';
|
||||
|
@ -13,19 +13,13 @@ import {
|
|||
} from 'selectors/config';
|
||||
import { CustomNode } from 'libs/nodes';
|
||||
import { Input } from 'components/ui';
|
||||
import Dropdown from 'components/ui/Dropdown';
|
||||
import './CustomNodeModal.scss';
|
||||
|
||||
const CUSTOM = 'custom';
|
||||
|
||||
interface InputProps {
|
||||
name: keyof Omit<State, 'hasAuth'>;
|
||||
placeholder?: string;
|
||||
type?: string;
|
||||
autoComplete?: 'off';
|
||||
onFocus?(): void;
|
||||
onBlur?(): void;
|
||||
}
|
||||
const CUSTOM = { label: 'Custom', value: 'custom' };
|
||||
|
||||
interface OwnProps {
|
||||
isOpen: boolean;
|
||||
addCustomNode(payload: AddCustomNodeAction['payload']): void;
|
||||
handleClose(): void;
|
||||
}
|
||||
|
@ -55,7 +49,7 @@ interface State {
|
|||
type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
class CustomNodeModal extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
public INITIAL_STATE = {
|
||||
name: '',
|
||||
url: '',
|
||||
network: Object.keys(this.props.staticNetworks)[0],
|
||||
|
@ -66,9 +60,17 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
username: '',
|
||||
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() {
|
||||
const { customNetworks, handleClose, staticNetworks } = this.props;
|
||||
const { customNetworks, handleClose, staticNetworks, isOpen } = this.props;
|
||||
const { network } = this.state;
|
||||
const isHttps = window.location.protocol.includes('https');
|
||||
const invalids = this.getInvalids();
|
||||
|
@ -88,158 +90,152 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
];
|
||||
|
||||
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 (
|
||||
<Modal
|
||||
title={translate('NODE_Title')}
|
||||
isOpen={true}
|
||||
title={translateRaw('NODE_Title')}
|
||||
isOpen={isOpen}
|
||||
buttons={buttons}
|
||||
handleClose={handleClose}
|
||||
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 && (
|
||||
<div className="alert alert-warning small">
|
||||
You already have a node called '{conflictedNode.name}' that matches this one, saving
|
||||
this will overwrite it
|
||||
{conflictedNode && (
|
||||
<div className="alert alert-warning small">
|
||||
You already have a node called '{conflictedNode.name}' that matches this one, saving
|
||||
this will overwrite it
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="CustomNodeModal">
|
||||
<div className="flex-wrapper">
|
||||
<label className="col-sm-9 input-group flex-grow-1">
|
||||
<div className="input-group-header">Node Name</div>
|
||||
<Input
|
||||
className={`input-group-input ${this.state.name && invalids.name ? 'invalid' : ''}`}
|
||||
type="text"
|
||||
placeholder="My Node"
|
||||
value={this.state.name}
|
||||
onChange={e => this.setState({ name: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="col-sm-3 input-group">
|
||||
<div className="input-group-header">Network</div>
|
||||
<Dropdown
|
||||
className="input-group-dropdown"
|
||||
value={network}
|
||||
options={options}
|
||||
clearable={false}
|
||||
onChange={(e: { label: string; value: string }) =>
|
||||
this.setState({ network: e.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{network === CUSTOM.value && (
|
||||
<div className="flex-wrapper">
|
||||
<label className="col-sm-6 input-group input-group-inline">
|
||||
<div className="input-group-header">Network Name</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
this.state.customNetworkId && invalids.customNetworkId ? 'invalid' : ''
|
||||
}`}
|
||||
type="text"
|
||||
placeholder="My Custom Network"
|
||||
value={this.state.customNetworkId}
|
||||
onChange={e => this.setState({ customNetworkId: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="col-sm-3 input-group input-group-inline">
|
||||
<div className="input-group-header">Currency</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
this.state.customNetworkUnit && invalids.customNetworkUnit ? 'invalid' : ''
|
||||
}`}
|
||||
type="text"
|
||||
placeholder="ETH"
|
||||
value={this.state.customNetworkUnit}
|
||||
onChange={e => this.setState({ customNetworkUnit: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="col-sm-3 input-group input-group-inline">
|
||||
<div className="input-group-header">Chain ID</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
this.state.customNetworkChainId && invalids.customNetworkChainId
|
||||
? 'invalid'
|
||||
: ''
|
||||
}`}
|
||||
type="text"
|
||||
placeholder="1"
|
||||
value={this.state.customNetworkChainId}
|
||||
onChange={e => this.setState({ customNetworkChainId: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form>
|
||||
<div className="row">
|
||||
<div className="col-sm-7">
|
||||
<label>{translate('NODE_Name')}</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'name',
|
||||
placeholder: 'My Node'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-5">
|
||||
<label>Network</label>
|
||||
<select
|
||||
className="form-control"
|
||||
name="network"
|
||||
value={network}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
{Object.keys(staticNetworks).map(net => (
|
||||
<option key={net} value={net}>
|
||||
{net}
|
||||
</option>
|
||||
))}
|
||||
{Object.entries(customNetworks).map(([id, net]) => (
|
||||
<option key={id} value={id}>
|
||||
{net.name} (Custom)
|
||||
</option>
|
||||
))}
|
||||
<option value={CUSTOM}>Custom...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label className="input-group input-group-inline">
|
||||
<div className="input-group-header">URL</div>
|
||||
<Input
|
||||
className={`input-group-input ${this.state.url && invalids.url ? 'invalid' : ''}`}
|
||||
type="text"
|
||||
placeholder="https://127.0.0.1:8545/"
|
||||
value={this.state.url}
|
||||
onChange={e => this.setState({ url: e.currentTarget.value })}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{network === CUSTOM && (
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<label className="is-required">Network Name</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'customNetworkId',
|
||||
placeholder: 'My Custom Network'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<label className="is-required">Currency</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'customNetworkUnit',
|
||||
placeholder: 'ETH'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<label>Chain ID</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'customNetworkChainId',
|
||||
placeholder: 'e.g. 1'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="hasAuth"
|
||||
checked={this.state.hasAuth}
|
||||
onChange={() => this.setState({ hasAuth: !this.state.hasAuth })}
|
||||
/>
|
||||
<span>HTTP Basic Authentication</span>
|
||||
</label>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<label>URL</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'url',
|
||||
placeholder: 'e.g. https://127.0.0.1:8545/',
|
||||
autoComplete: 'off'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
{this.state.hasAuth && (
|
||||
<div className="flex-wrapper ">
|
||||
<label className="col-sm-6 input-group input-group-inline">
|
||||
<div className="input-group-header">Username</div>
|
||||
<Input
|
||||
className={`input-group-input ${
|
||||
this.state.username && invalids.username ? 'invalid' : ''
|
||||
}`}
|
||||
type="text"
|
||||
value={this.state.username}
|
||||
onChange={e => this.setState({ username: e.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="col-sm-6 input-group input-group-inline">
|
||||
<div className="input-group-header">Password</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 className="row">
|
||||
<div className="col-sm-12">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="hasAuth"
|
||||
checked={this.state.hasAuth}
|
||||
onChange={this.handleCheckbox}
|
||||
/>{' '}
|
||||
<span>HTTP Basic Authentication</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.hasAuth && (
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<label className="is-required">Username</label>
|
||||
{this.renderInput({ name: 'username' }, invalids)}
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<label className="is-required">Password</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</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 } {
|
||||
const {
|
||||
url,
|
||||
|
@ -278,7 +274,7 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
// If they have a custom network, make sure info is provided
|
||||
if (network === CUSTOM) {
|
||||
if (network === CUSTOM.value) {
|
||||
if (!customNetworkId) {
|
||||
invalids.customNetworkId = true;
|
||||
}
|
||||
|
@ -315,7 +311,7 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
const { network, url, name, username, password } = this.state;
|
||||
|
||||
const networkId =
|
||||
network === CUSTOM
|
||||
network === CUSTOM.value
|
||||
? this.makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
|
||||
: network;
|
||||
|
||||
|
@ -348,20 +344,10 @@ class CustomNodeModal extends React.Component<Props, State> {
|
|||
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 = () => {
|
||||
const node = this.makeCustomNodeConfigFromState();
|
||||
|
||||
if (this.state.network === CUSTOM) {
|
||||
if (this.state.network === CUSTOM.value) {
|
||||
const network = this.makeCustomNetworkConfigFromState();
|
||||
|
||||
this.props.addCustomNetwork({ config: network, id: node.network });
|
||||
|
|
|
@ -192,12 +192,11 @@ class Header extends Component<Props, State> {
|
|||
|
||||
<Navigation color={!network.isCustom && network.color} />
|
||||
|
||||
{isAddingCustomNode && (
|
||||
<CustomNodeModal
|
||||
addCustomNode={this.addCustomNode}
|
||||
handleClose={this.closeCustomNodeModal}
|
||||
/>
|
||||
)}
|
||||
<CustomNodeModal
|
||||
isOpen={isAddingCustomNode}
|
||||
addCustomNode={this.addCustomNode}
|
||||
handleClose={this.closeCustomNodeModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -100,8 +100,8 @@ export default class PaperWallet extends React.Component<Props, {}> {
|
|||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<img src={sidebarImg} style={styles.sidebar} />
|
||||
<img src={ethLogo} style={styles.ethLogo} />
|
||||
<img src={sidebarImg} style={styles.sidebar} alt="MyCrypto Logo" />
|
||||
<img src={ethLogo} style={styles.ethLogo} alt="ETH Logo" />
|
||||
|
||||
<div style={styles.block}>
|
||||
<div style={styles.box}>
|
||||
|
@ -111,7 +111,7 @@ export default class PaperWallet extends React.Component<Props, {}> {
|
|||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ interface Props {
|
|||
isValid?: boolean;
|
||||
isVisible?: boolean;
|
||||
validity?: 'valid' | 'invalid' | 'semivalid';
|
||||
readOnly?: boolean;
|
||||
|
||||
// Textarea-only props
|
||||
isTextareaWhenVisible?: boolean;
|
||||
|
@ -61,18 +62,16 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
|||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
handleToggleVisibility
|
||||
handleToggleVisibility,
|
||||
readOnly
|
||||
} = this.props;
|
||||
const { isVisible } = this.state;
|
||||
const validClass = validity
|
||||
? `is-${validity}`
|
||||
: isValid === null || isValid === undefined ? '' : isValid ? 'is-valid' : 'is-invalid';
|
||||
|
||||
return (
|
||||
<div className={`TogglablePassword input-group input-group-inline-dropdown ${className}`}>
|
||||
<div className={`TogglablePassword input-group input-group-inline ${className}`}>
|
||||
{isTextareaWhenVisible && isVisible ? (
|
||||
<TextArea
|
||||
className={validClass}
|
||||
className={validity || !isValid ? 'invalid' : ''}
|
||||
value={value}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
|
@ -83,6 +82,7 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
|||
placeholder={placeholder}
|
||||
rows={this.props.rows || 3}
|
||||
aria-label={ariaLabel}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
|
@ -90,12 +90,13 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
|||
name={name}
|
||||
disabled={disabled}
|
||||
type={isVisible ? 'text' : 'password'}
|
||||
className={`${validClass}`}
|
||||
className={`${validity || !isValid ? 'invalid' : ''} border-rad-right-0`}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
aria-label={ariaLabel}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
|
|
|
@ -40,7 +40,6 @@ $speed: 500ms;
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -80,7 +79,6 @@ $speed: 500ms;
|
|||
}
|
||||
|
||||
&:active {
|
||||
outline: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
@ -106,7 +104,7 @@ $speed: 500ms;
|
|||
.DecryptContent {
|
||||
&-enter {
|
||||
opacity: 0;
|
||||
transition: opacity $speed * .25 ease $speed * .125;
|
||||
transition: opacity $speed * 0.25 ease $speed * 0.125;
|
||||
|
||||
&-active {
|
||||
opacity: 1;
|
||||
|
@ -119,7 +117,7 @@ $speed: 500ms;
|
|||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity $speed * .25 ease;
|
||||
transition: opacity $speed * 0.25 ease;
|
||||
pointer-events: none;
|
||||
|
||||
&-active {
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
transition: transform 150ms ease, box-shadow 150ms ease;
|
||||
animation: wallet-button-enter 400ms ease 1;
|
||||
animation-fill-mode: backwards;
|
||||
outline: none;
|
||||
|
||||
@for $i from 0 to 5 {
|
||||
&:nth-child(#{$i}) {
|
||||
|
@ -60,7 +59,6 @@
|
|||
}
|
||||
|
||||
&.is-disabled {
|
||||
outline: none;
|
||||
cursor: not-allowed;
|
||||
@include show-tooltip-on-hover;
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ interface Icon {
|
|||
icon: string;
|
||||
tooltip: string;
|
||||
href?: string;
|
||||
arialabel: string;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps;
|
||||
|
@ -50,18 +51,21 @@ export class WalletButton extends React.PureComponent<Props> {
|
|||
if (isReadOnly) {
|
||||
icons.push({
|
||||
icon: 'eye',
|
||||
tooltip: translateRaw('You cannot send using address only')
|
||||
tooltip: translateRaw('You cannot send using address only'),
|
||||
arialabel: 'Read Only'
|
||||
});
|
||||
} else {
|
||||
if (isSecure) {
|
||||
icons.push({
|
||||
icon: 'shield',
|
||||
tooltip: translateRaw('This wallet type is secure')
|
||||
tooltip: translateRaw('This wallet type is secure'),
|
||||
arialabel: 'Secure wallet type'
|
||||
});
|
||||
} else {
|
||||
icons.push({
|
||||
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({
|
||||
icon: 'question-circle',
|
||||
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-title">
|
||||
{icon && <img className="WalletButton-title-icon" src={icon} />}
|
||||
{icon && <img className="WalletButton-title-icon" src={icon} alt={name + ' logo'} />}
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
|
||||
{description && <div className="WalletButton-description">{description}</div>}
|
||||
{example && <div className="WalletButton-example">{example}</div>}
|
||||
{description && (
|
||||
<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">
|
||||
{icons.map(i => (
|
||||
<span className="WalletButton-icons-icon" key={i.icon} onClick={this.stopPropogation}>
|
||||
{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}`} />
|
||||
</NewTabLink>
|
||||
) : (
|
||||
<i className={`fa fa-${i.icon}`} />
|
||||
<i className={`fa fa-${i.icon}`} aria-label={i.arialabel} />
|
||||
)}
|
||||
{!isDisabled && <Tooltip size="sm">{i.tooltip}</Tooltip>}
|
||||
</span>
|
||||
|
|
|
@ -92,6 +92,7 @@ export default class ColorDropdown<T> extends PureComponent<Props<T>, {}> {
|
|||
className="ColorDropdown-item-remove"
|
||||
onClick={this.onRemove.bind(null, option.onRemove)}
|
||||
src={removeIcon}
|
||||
alt="remove"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
|
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
const Help = ({ size = 'x1', link }: Props) => {
|
||||
return (
|
||||
<a href={link} className={`Help Help-${size}`} target="_blank" rel="noopener noreferrer">
|
||||
<img src={icon} />
|
||||
<img src={icon} alt="help" />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ export default function Identicon(props: Props) {
|
|||
<React.Fragment>
|
||||
<img
|
||||
src={identiconDataUrl}
|
||||
alt="Unique Address Image"
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
|
|
|
@ -12,6 +12,22 @@
|
|||
> .TogglablePassword {
|
||||
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 {
|
||||
display: flex;
|
||||
font-size: 1rem;
|
||||
|
@ -29,6 +45,9 @@
|
|||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
}
|
||||
&-dropdown {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
&-input {
|
||||
width: 100%;
|
||||
border: 1px solid #e5ecf3;
|
||||
|
@ -40,6 +59,17 @@
|
|||
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;
|
||||
}
|
||||
&.border-rad-left-0 {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
&-small {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
&::placeholder {
|
||||
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 {
|
||||
.Select-input {
|
||||
left: 24px;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -13,17 +13,6 @@ $m-footer-padding: 0.5rem 2rem 1rem 2rem;
|
|||
$m-close-size: 26px;
|
||||
$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 {
|
||||
position: fixed;
|
||||
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),
|
||||
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 {
|
||||
background: linear-gradient(to bottom, #fff0, #fff);
|
||||
position: fixed;
|
||||
|
@ -112,7 +112,7 @@ $m-anim-speed: 400ms;
|
|||
}
|
||||
|
||||
// Mobile styles
|
||||
@media(max-width: $screen-sm) {
|
||||
@media (max-width: $screen-sm) {
|
||||
top: $m-window-padding-h-mobile;
|
||||
width: calc(100% - #{$m-window-padding-w-mobile}) !important;
|
||||
max-width: calc(100% - #{$m-window-padding-w-mobile * 2});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ const OfflineSymbol = ({ offline, size }: OfflineSymbolProps) => {
|
|||
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;
|
||||
|
|
|
@ -35,6 +35,7 @@ export default class QRCode extends React.PureComponent<Props, State> {
|
|||
return (
|
||||
<img
|
||||
src={qr}
|
||||
alt="QR Code"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
|
|
|
@ -11,7 +11,7 @@ interface SpinnerProps {
|
|||
const Spinner = ({ size = 'x1', light = false }: SpinnerProps) => {
|
||||
const color = light ? 'Spinner-light' : 'Spinner-dark';
|
||||
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" />
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
padding: 0.4rem 1rem;
|
||||
border-radius: 2px;
|
||||
height: 2.5rem;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:active,
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
|
|
|
@ -18,7 +18,7 @@ interface Props<T> {
|
|||
const ValueComp: React.SFC = (props: any) => {
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
@ -46,7 +46,7 @@ const OptionComp: React.SFC = (props: any) => {
|
|||
onMouseEnter={handleMouseEnter}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -120,7 +120,11 @@ class OnboardModal extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<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">
|
||||
<Stepper
|
||||
steps={steps}
|
||||
|
@ -171,7 +175,7 @@ class OnboardModal extends React.Component<Props, State> {
|
|||
localStorage.setItem(ONBOARD_LOCAL_STORAGE_KEY, String(prevSlideNum));
|
||||
this.props.decrementSlide();
|
||||
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));
|
||||
this.props.incrementSlide();
|
||||
if (this.modal) {
|
||||
this.modal.scrollContentToTop();
|
||||
this.modal.modalBody.scrollContentToTop();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ interface Props {
|
|||
export class Notifications extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<TransitionGroup className="Notifications">
|
||||
<TransitionGroup className="Notifications" aria-live="polite">
|
||||
{this.props.notifications.map(n => {
|
||||
return (
|
||||
<CSSTransition classNames="NotificationAnimation" timeout={500} key={n.id}>
|
||||
|
|
|
@ -33,10 +33,12 @@ class NameInput extends Component<Props, State> {
|
|||
return (
|
||||
<form className="ENSInput" onSubmit={this.onSubmit}>
|
||||
<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
|
||||
value={domainToCheck}
|
||||
className={!domainToCheck ? '' : isValidDomain ? 'is-valid' : 'is-invalid'}
|
||||
className={`${
|
||||
!domainToCheck ? '' : isValidDomain ? '' : 'invalid'
|
||||
} border-rad-right-0`}
|
||||
type="text"
|
||||
placeholder="mycrypto"
|
||||
onChange={this.onChange}
|
||||
|
|
|
@ -53,7 +53,11 @@ const CryptoWarning: React.SFC<{}> = () => (
|
|||
className="CryptoWarning-browsers-browser"
|
||||
>
|
||||
<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>
|
||||
</NewTabLink>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "common/sass/variables";
|
||||
@import 'common/sass/variables';
|
||||
|
||||
.GenPaper {
|
||||
&-title {
|
||||
|
@ -6,8 +6,17 @@
|
|||
}
|
||||
|
||||
&-private {
|
||||
max-width: 700px;
|
||||
max-width: 680px;
|
||||
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,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import PrintableWallet from 'components/PrintableWallet';
|
||||
import { IV3Wallet } from 'ethereumjs-wallet';
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
import './PaperWallet.scss';
|
||||
import Template from '../Template';
|
||||
|
@ -17,18 +17,20 @@ const PaperWallet: React.SFC<Props> = props => (
|
|||
<Template>
|
||||
<div className="GenPaper">
|
||||
{/* Private Key */}
|
||||
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
|
||||
<Input
|
||||
className="GenPaper-private"
|
||||
value={stripHexPrefix(props.privateKey)}
|
||||
aria-label={translate('x_PrivKey', true)}
|
||||
aria-describedby="x_PrivKeyDesc"
|
||||
type="text"
|
||||
readOnly={true}
|
||||
/>
|
||||
<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
|
||||
value={stripHexPrefix(props.privateKey)}
|
||||
aria-label={translateRaw('x_PrivKey')}
|
||||
aria-describedby="x_PrivKeyDesc"
|
||||
type="text"
|
||||
readOnly={true}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Download Paper Wallet */}
|
||||
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
|
||||
<h2 className="GenPaper-title">{translate('x_Print')}</h2>
|
||||
<div className="GenPaper-paper">
|
||||
<PrintableWallet address={props.keystore.address} privateKey={props.privateKey} />
|
||||
</div>
|
||||
|
|
|
@ -28,10 +28,10 @@ export default class MnemonicWord extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<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>
|
||||
<Input
|
||||
className={classnames('MnemonicWord-word-input', word === value && 'valid')}
|
||||
className={`MnemonicWord-word-input ${!isReadOnly && 'border-rad-right-0'}`}
|
||||
value={readOnly ? word : value}
|
||||
onChange={this.handleChange}
|
||||
readOnly={readOnly}
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
top: 36px;
|
||||
left: 30px;
|
||||
opacity: 0.3;
|
||||
outline: none;
|
||||
color: $text-color;
|
||||
|
||||
@media (max-width: $screen-sm) {
|
||||
|
|
|
@ -341,7 +341,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
|
|||
<div className="flex-spacer" />
|
||||
<div className="input-group-wrapper">
|
||||
<div className="input-group-header">Deposit</div>
|
||||
<label className="input-group input-group-inline-dropdown">
|
||||
<label className="input-group input-group-inline">
|
||||
<Input
|
||||
id="origin-swap-input"
|
||||
className={`input-group-input ${
|
||||
|
@ -365,7 +365,7 @@ export default class CurrencySwap extends PureComponent<Props, State> {
|
|||
</div>
|
||||
|
||||
<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>
|
||||
<Input
|
||||
id="destination-swap-input"
|
||||
|
|
|
@ -132,7 +132,7 @@ class CurrentRates extends PureComponent<Props> {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img src={providerLogo} width={120} height={49} />
|
||||
<img src={providerLogo} width={120} height={49} alt="Shapeshift Logo" />
|
||||
</a>
|
||||
</section>
|
||||
</article>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { RestartSwapAction } from 'actions/swap';
|
||||
import bityLogo from 'assets/images/logo-bity.svg';
|
||||
import shapeshiftLogo from 'assets/images/shapeshift-dark.svg';
|
||||
import { bityReferralURL } from 'config';
|
||||
import { shapeshiftReferralURL, bitboxReferralURL } from 'config';
|
||||
import React, { PureComponent } from 'react';
|
||||
import translate from 'translations';
|
||||
import './SwapInfoHeader.scss';
|
||||
|
@ -29,11 +29,11 @@ export default class SwapInfoHeaderTitle extends PureComponent<SwapInfoHeaderTit
|
|||
<div className="col-xs-3">
|
||||
<a
|
||||
className="SwapInfo-top-logo"
|
||||
href={bityReferralURL}
|
||||
href={provider === 'shapeshift' ? shapeshiftReferralURL : bitboxReferralURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img className="SwapInfo-top-logo-img" src={logoToRender} />
|
||||
<img className="SwapInfo-top-logo-img" src={logoToRender} alt={provider + ' logo'} />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -39,3 +39,8 @@
|
|||
[data-whatintent='mouse'] *:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// This is fine because the outline effect is reproduced with border and box-shadow styles on input elements
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,19 @@
|
|||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
&-1 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
&-2 {
|
||||
flex-grow: 2;
|
||||
}
|
||||
&-3 {
|
||||
flex-grow: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-spacer {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
|
|
@ -8,9 +8,3 @@
|
|||
.btn-group > .btn-group {
|
||||
float: left;
|
||||
}
|
||||
|
||||
// On active and open, don't show outline
|
||||
.btn-group .dropdown-toggle:active,
|
||||
.btn-group.open .dropdown-toggle {
|
||||
outline: 0;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ input[readonly] {
|
|||
|
||||
&: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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,9 @@
|
|||
border-bottom-right-radius: 2px;
|
||||
border-color: inherit;
|
||||
}
|
||||
&-menu {
|
||||
max-height: 8.625rem;
|
||||
}
|
||||
&.invalid.has-blurred {
|
||||
border-color: $brand-danger;
|
||||
box-shadow: inset 0px 0px 0px 1px $brand-danger;
|
||||
|
|
Loading…
Reference in New Issue