Refactor Dropdowns, Rainbow Node Selector (#244)
* Convert all dropdowns to use a single dropdown shell component. Restyle header ones to look like v3. * Right align some. * Color dropdown component, which node selector uses. * Prettier fixes.
This commit is contained in:
parent
ad83b5a181
commit
1a09c6a7a6
|
@ -1,43 +1,46 @@
|
||||||
import { gasPriceDefaults } from 'config/data';
|
import { gasPriceDefaults } from 'config/data';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import DropdownShell from 'components/ui/DropdownShell';
|
||||||
import './GasPriceDropdown.scss';
|
import './GasPriceDropdown.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: number;
|
value: number;
|
||||||
onChange(gasPrice: number): void;
|
onChange(gasPrice: number): void;
|
||||||
}
|
}
|
||||||
interface State {
|
|
||||||
expanded: boolean;
|
|
||||||
}
|
|
||||||
export default class GasPriceDropdown extends Component<Props, State> {
|
|
||||||
public state = { expanded: false };
|
|
||||||
|
|
||||||
|
export default class GasPriceDropdown extends Component<Props, {}> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.updateGasPrice = throttle(this.updateGasPrice, 50);
|
this.updateGasPrice = throttle(this.updateGasPrice, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { expanded } = this.state;
|
const { value } = this.props;
|
||||||
return (
|
return (
|
||||||
<span className={`dropdown ${expanded ? 'open' : ''}`}>
|
<DropdownShell
|
||||||
<a
|
color="white"
|
||||||
aria-haspopup="true"
|
size="smr"
|
||||||
aria-label="adjust gas price"
|
ariaLabel={`adjust gas price. current price is ${value} gwei`}
|
||||||
className="dropdown-toggle"
|
renderLabel={this.renderLabel}
|
||||||
onClick={this.toggleExpanded}
|
renderOptions={this.renderOptions}
|
||||||
>
|
/>
|
||||||
<span>Gas Price</span>: {this.props.value} Gwei
|
);
|
||||||
<i className="caret" />
|
}
|
||||||
</a>
|
|
||||||
{expanded &&
|
private renderLabel = () => {
|
||||||
<ul className="dropdown-menu GasPrice-dropdown-menu">
|
return `Gas Price: ${this.props.value} Gwei`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderOptions = () => {
|
||||||
|
const { value } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="GasPrice-dropdown-menu dropdown-menu dropdown-menu-right">
|
||||||
<div className="GasPrice-header">
|
<div className="GasPrice-header">
|
||||||
<span>Gas Price</span>: {this.props.value} Gwei
|
<span>Gas Price</span>: {value} Gwei
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
value={this.props.value}
|
value={value}
|
||||||
min={gasPriceDefaults.gasPriceMinGwei}
|
min={gasPriceDefaults.gasPriceMinGwei}
|
||||||
max={gasPriceDefaults.gasPriceMaxGwei}
|
max={gasPriceDefaults.gasPriceMaxGwei}
|
||||||
onChange={this.handleGasPriceChange}
|
onChange={this.handleGasPriceChange}
|
||||||
|
@ -53,8 +56,8 @@ export default class GasPriceDropdown extends Component<Props, State> {
|
||||||
</p>
|
</p>
|
||||||
<p className="small GasPrice-description">
|
<p className="small GasPrice-description">
|
||||||
Gas Price is the amount you pay per unit of gas.{' '}
|
Gas Price is the amount you pay per unit of gas.{' '}
|
||||||
<code>TX fee = gas price * gas limit</code> & is paid to miners
|
<code>TX fee = gas price * gas limit</code> & is paid to miners for
|
||||||
for including your TX in a block. Higher the gas price = faster
|
including your TX in a block. Higher the gas price = faster
|
||||||
transaction, but more expensive. Default is <code>21 GWEI</code>.
|
transaction, but more expensive. Default is <code>21 GWEI</code>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -67,24 +70,17 @@ export default class GasPriceDropdown extends Component<Props, State> {
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ul>}
|
</div>
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
public toggleExpanded = () => {
|
|
||||||
this.setState(state => {
|
|
||||||
return {
|
|
||||||
expanded: !state.expanded
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public updateGasPrice = (value: string) => {
|
private updateGasPrice = (value: string) => {
|
||||||
this.props.onChange(parseInt(value, 10));
|
this.props.onChange(parseInt(value, 10));
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleGasPriceChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
private handleGasPriceChange = (
|
||||||
|
e: React.SyntheticEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
this.updateGasPrice((e.target as HTMLInputElement).value);
|
this.updateGasPrice((e.target as HTMLInputElement).value);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,66 +92,37 @@ $small-size: 900px;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-tagline {
|
&-right {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
color: white;
|
color: white;
|
||||||
flex: 1 auto;
|
flex: 1 auto;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding: 5px 0;
|
padding: 0 0 5px;
|
||||||
@include small-query {
|
@include small-query {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
display: inline;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-version {
|
&-version {
|
||||||
max-width: 395px;
|
max-width: 395px;
|
||||||
}
|
margin-right: 10px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
&-dropdown {
|
||||||
color: white;
|
margin-left: 6px;
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 400;
|
|
||||||
transition: 250ms all ease;
|
|
||||||
|
|
||||||
&:hover,
|
&-add {
|
||||||
&:active {
|
text-align: center;
|
||||||
opacity: .8;
|
padding-top: $space-sm !important;
|
||||||
color: white;
|
padding-bottom: $space-sm !important;
|
||||||
text-decoration: none;
|
|
||||||
transition: 250ms all ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO - Move to dropdown component?
|
|
||||||
.dropdown {
|
|
||||||
margin-left: 15px;
|
|
||||||
padding: 0;
|
|
||||||
text-align: right;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
right: -10px;
|
|
||||||
left: auto;
|
|
||||||
min-width: auto;
|
|
||||||
left: auto;
|
|
||||||
|
|
||||||
& > li > a {
|
|
||||||
font-size: 15px;
|
|
||||||
padding: 5px 30px 5px 15px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
text-decoration: none;
|
|
||||||
color: $brand-primary;
|
|
||||||
background-color: $gray-lightest;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { TChangeGasPrice, TChangeLanguage, TChangeNode } from 'actions/config';
|
import { TChangeGasPrice, TChangeLanguage, TChangeNode } from 'actions/config';
|
||||||
import logo from 'assets/images/logo-myetherwallet.svg';
|
import logo from 'assets/images/logo-myetherwallet.svg';
|
||||||
import { Dropdown } from 'components/ui';
|
import { Dropdown, ColorDropdown } from 'components/ui';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
|
@ -36,7 +36,18 @@ export default class Header extends Component<Props, {}> {
|
||||||
const LanguageDropDown = Dropdown as new () => Dropdown<
|
const LanguageDropDown = Dropdown as new () => Dropdown<
|
||||||
typeof selectedLanguage
|
typeof selectedLanguage
|
||||||
>;
|
>;
|
||||||
const NodeDropDown = Dropdown as new () => Dropdown<keyof typeof NODES>;
|
const nodeOptions = Object.keys(NODES).map(key => {
|
||||||
|
return {
|
||||||
|
value: key,
|
||||||
|
name: (
|
||||||
|
<span>
|
||||||
|
{NODES[key].network} <small>({NODES[key].service})</small>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
color: NETWORKS[NODES[key].network].color
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Header">
|
<div className="Header">
|
||||||
{ANNOUNCEMENT_MESSAGE && (
|
{ANNOUNCEMENT_MESSAGE && (
|
||||||
|
@ -64,37 +75,40 @@ export default class Header extends Component<Props, {}> {
|
||||||
alt="MyEtherWallet"
|
alt="MyEtherWallet"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="Header-branding-title-tagline">
|
<div className="Header-branding-right">
|
||||||
<span className="Header-branding-title-tagline-version">
|
<span className="Header-branding-right-version">v{VERSION}</span>
|
||||||
v{VERSION}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
|
<div className="Header-branding-right-dropdown">
|
||||||
<GasPriceDropdown
|
<GasPriceDropdown
|
||||||
value={this.props.gasPriceGwei}
|
value={this.props.gasPriceGwei}
|
||||||
onChange={this.props.changeGasPrice}
|
onChange={this.props.changeGasPrice}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="Header-branding-right-dropdown">
|
||||||
<LanguageDropDown
|
<LanguageDropDown
|
||||||
ariaLabel={`change language. current language ${languages[
|
ariaLabel={`change language. current language ${languages[
|
||||||
selectedLanguage
|
selectedLanguage
|
||||||
]}`}
|
]}`}
|
||||||
options={Object.values(languages)}
|
options={Object.values(languages)}
|
||||||
value={languages[selectedLanguage]}
|
value={languages[selectedLanguage]}
|
||||||
extra={[
|
extra={
|
||||||
<li key={'separator'} role="separator" className="divider" />,
|
<li key="disclaimer">
|
||||||
<li key={'disclaimer'}>
|
|
||||||
<a data-toggle="modal" data-target="#disclaimerModal">
|
<a data-toggle="modal" data-target="#disclaimerModal">
|
||||||
Disclaimer
|
Disclaimer
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
]}
|
}
|
||||||
onChange={this.changeLanguage}
|
onChange={this.changeLanguage}
|
||||||
|
size="smr"
|
||||||
|
color="white"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<NodeDropDown
|
<div className="Header-branding-right-dropdown">
|
||||||
|
<ColorDropdown
|
||||||
ariaLabel={`change node. current node ${selectedNode.network} node by ${selectedNode.service}`}
|
ariaLabel={`change node. current node ${selectedNode.network} node by ${selectedNode.service}`}
|
||||||
options={Object.keys(NODES)}
|
options={nodeOptions}
|
||||||
formatTitle={this.nodeNetworkAndService}
|
|
||||||
value={nodeSelection}
|
value={nodeSelection}
|
||||||
extra={
|
extra={
|
||||||
<li>
|
<li>
|
||||||
|
@ -102,8 +116,11 @@ export default class Header extends Component<Props, {}> {
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
onChange={changeNode}
|
onChange={changeNode}
|
||||||
|
size="smr"
|
||||||
|
color="white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -121,10 +138,4 @@ export default class Header extends Component<Props, {}> {
|
||||||
this.props.changeLanguage(key);
|
this.props.changeLanguage(key);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private nodeNetworkAndService = (option: string) => [
|
|
||||||
NODES[option].network,
|
|
||||||
' ',
|
|
||||||
<small key="service">({NODES[option].service}) </small>
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
// @flow
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import DropdownShell from './DropdownShell';
|
||||||
|
|
||||||
|
interface Option<T> {
|
||||||
|
name: any;
|
||||||
|
value: T;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props<T> {
|
||||||
|
value: T;
|
||||||
|
options: Option<T>[];
|
||||||
|
label?: string;
|
||||||
|
ariaLabel: string;
|
||||||
|
extra?: any;
|
||||||
|
size?: string;
|
||||||
|
color?: string;
|
||||||
|
menuAlign?: string;
|
||||||
|
onChange(value: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ColorDropdown<T> extends Component<Props<T>, {}> {
|
||||||
|
private dropdownShell: DropdownShell | null;
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { ariaLabel, color, size } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownShell
|
||||||
|
renderLabel={this.renderLabel}
|
||||||
|
renderOptions={this.renderOptions}
|
||||||
|
size={size}
|
||||||
|
color={color}
|
||||||
|
ariaLabel={ariaLabel}
|
||||||
|
ref={el => (this.dropdownShell = el)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLabel = () => {
|
||||||
|
const label = this.props.label ? `${this.props.label}:` : '';
|
||||||
|
const activeOption = this.getActiveOption();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{label} {activeOption ? activeOption.name : '-'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderOptions = () => {
|
||||||
|
const { options, value, menuAlign, extra } = this.props;
|
||||||
|
|
||||||
|
const activeOption = this.getActiveOption();
|
||||||
|
|
||||||
|
const listItems = options.reduce((prev: any[], opt) => {
|
||||||
|
const prevOpt = prev.length ? prev[prev.length - 1] : null;
|
||||||
|
if (prevOpt && !prevOpt.divider && prevOpt.color !== opt.color) {
|
||||||
|
prev.push({ divider: true });
|
||||||
|
}
|
||||||
|
prev.push(opt);
|
||||||
|
return prev;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const menuClass = classnames({
|
||||||
|
'dropdown-menu': true,
|
||||||
|
[`dropdown-menu-${menuAlign || ''}`]: !!menuAlign
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={menuClass}>
|
||||||
|
{listItems.map((option, i) => {
|
||||||
|
if (option.divider) {
|
||||||
|
return <li key={i} role="separator" className="divider" />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<li key={i} style={{ borderLeft: `2px solid ${option.color}` }}>
|
||||||
|
<a
|
||||||
|
className={option.value === value ? 'active' : ''}
|
||||||
|
onClick={this.onChange.bind(null, option.value)}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
{extra && <li key="separator" role="separator" className="divider" />}
|
||||||
|
{extra}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onChange = (value: any) => {
|
||||||
|
this.props.onChange(value);
|
||||||
|
if (this.dropdownShell) {
|
||||||
|
this.dropdownShell.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getActiveOption() {
|
||||||
|
return this.props.options.find(opt => opt.value === this.props.value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,42 +1,57 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import DropdownShell from './DropdownShell';
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
value: T;
|
value: T;
|
||||||
options: T[];
|
options: T[];
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
|
label?: string;
|
||||||
extra?: any;
|
extra?: any;
|
||||||
|
size?: string;
|
||||||
|
color?: string;
|
||||||
|
menuAlign?: string;
|
||||||
formatTitle?(option: T): any;
|
formatTitle?(option: T): any;
|
||||||
onChange(value: T): void;
|
onChange(value: T): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export default class DropdownComponent<T> extends Component<Props<T>, {}> {
|
||||||
expanded: boolean;
|
private dropdownShell: DropdownShell | null;
|
||||||
}
|
|
||||||
|
|
||||||
export default class DropdownComponent<T> extends Component<Props<T>, State> {
|
|
||||||
public state = {
|
|
||||||
expanded: false
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { options, value, ariaLabel, extra } = this.props;
|
const { ariaLabel, color, size } = this.props;
|
||||||
const { expanded } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`dropdown ${expanded ? 'open' : ''}`}>
|
<DropdownShell
|
||||||
<a
|
renderLabel={this.renderLabel}
|
||||||
tabIndex={0}
|
renderOptions={this.renderOptions}
|
||||||
aria-haspopup="true"
|
size={size}
|
||||||
aria-expanded="false"
|
color={color}
|
||||||
aria-label={ariaLabel}
|
ariaLabel={ariaLabel}
|
||||||
className="dropdown-toggle"
|
ref={el => (this.dropdownShell = el)}
|
||||||
onClick={this.toggleExpanded}
|
/>
|
||||||
>
|
);
|
||||||
{this.props.formatTitle ? this.formatTitle(value) : value}
|
}
|
||||||
<i className="caret" />
|
|
||||||
</a>
|
private renderLabel = () => {
|
||||||
{expanded && (
|
const { label, value } = this.props;
|
||||||
<ul className="dropdown-menu">
|
const labelStr = this.props.label ? `${this.props.label}:` : '';
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{labelStr} {this.formatTitle(value)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderOptions = () => {
|
||||||
|
const { options, value, menuAlign, extra } = this.props;
|
||||||
|
const menuClass = classnames({
|
||||||
|
'dropdown-menu': true,
|
||||||
|
[`dropdown-menu-${menuAlign || ''}`]: !!menuAlign
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={menuClass}>
|
||||||
{options.map((option, i) => {
|
{options.map((option, i) => {
|
||||||
return (
|
return (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
|
@ -49,29 +64,24 @@ export default class DropdownComponent<T> extends Component<Props<T>, State> {
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{extra && <li key={'separator'} role="separator" className="divider" />}
|
||||||
{extra}
|
{extra}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
public formatTitle = (option: any) => {
|
private formatTitle = (option: any) => {
|
||||||
if (this.props.formatTitle) {
|
if (this.props.formatTitle) {
|
||||||
return this.props.formatTitle(option);
|
return this.props.formatTitle(option);
|
||||||
|
} else {
|
||||||
|
return option;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public toggleExpanded = () => {
|
private onChange = (value: any) => {
|
||||||
this.setState(state => {
|
|
||||||
return {
|
|
||||||
expanded: !state.expanded
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public onChange = (value: any) => {
|
|
||||||
this.props.onChange(value);
|
this.props.onChange(value);
|
||||||
this.setState({ expanded: false });
|
if (this.dropdownShell) {
|
||||||
|
this.dropdownShell.close();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
// @flow
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
ariaLabel: string;
|
||||||
|
size?: string;
|
||||||
|
color?: string;
|
||||||
|
renderLabel(): any;
|
||||||
|
renderOptions(): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
expanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DropdownComponent extends Component<Props, State> {
|
||||||
|
public static defaultProps = {
|
||||||
|
color: 'default',
|
||||||
|
size: 'sm'
|
||||||
|
};
|
||||||
|
|
||||||
|
public state: State = {
|
||||||
|
expanded: false
|
||||||
|
};
|
||||||
|
|
||||||
|
private dropdown: HTMLElement | null;
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
document.addEventListener('click', this.clickOffHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
document.removeEventListener('click', this.clickOffHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { ariaLabel, color, size, renderOptions, renderLabel } = this.props;
|
||||||
|
const { expanded } = this.state;
|
||||||
|
const toggleClasses = classnames([
|
||||||
|
'dropdown-toggle',
|
||||||
|
'btn',
|
||||||
|
`btn-${color}`,
|
||||||
|
`btn-${size}`
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`dropdown ${expanded ? 'open' : ''}`}
|
||||||
|
ref={el => (this.dropdown = el)}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
tabIndex={0}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={expanded}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={toggleClasses}
|
||||||
|
onClick={this.toggle}
|
||||||
|
>
|
||||||
|
{renderLabel()}
|
||||||
|
<i className="caret" />
|
||||||
|
</a>
|
||||||
|
{expanded && renderOptions()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggle = () => {
|
||||||
|
this.setState({ expanded: !this.state.expanded });
|
||||||
|
};
|
||||||
|
|
||||||
|
public open = () => {
|
||||||
|
this.setState({ expanded: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
public close = () => {
|
||||||
|
this.setState({ expanded: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
private clickOffHandler = (ev: MouseEvent) => {
|
||||||
|
// Only calculate if dropdown is open & we have the ref
|
||||||
|
if (!this.state.expanded || !this.dropdown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's an element that's not inside of the dropdown, close it up
|
||||||
|
if (
|
||||||
|
this.dropdown !== ev.target &&
|
||||||
|
ev.target instanceof HTMLElement &&
|
||||||
|
!this.dropdown.contains(ev.target)
|
||||||
|
) {
|
||||||
|
this.setState({ expanded: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,61 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
value?: string;
|
|
||||||
options: string[];
|
|
||||||
onChange(value: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
expanded: boolean;
|
|
||||||
}
|
|
||||||
export default class SimpleDropDown extends Component<Props, State> {
|
|
||||||
public state = {
|
|
||||||
expanded: false
|
|
||||||
};
|
|
||||||
|
|
||||||
public toggleExpanded = () => {
|
|
||||||
this.setState(state => {
|
|
||||||
return { expanded: !state.expanded };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public onClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
|
|
||||||
const value = (event.target as HTMLAnchorElement).getAttribute('data-value') || '';
|
|
||||||
this.props.onChange(value);
|
|
||||||
this.setState({ expanded: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const { options, value } = this.props;
|
|
||||||
const { expanded } = this.state;
|
|
||||||
return (
|
|
||||||
<div className={`dropdown ${expanded ? 'open' : ''}`}>
|
|
||||||
<a
|
|
||||||
className="btn btn-default dropdown-toggle"
|
|
||||||
onClick={this.toggleExpanded}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
<i className="caret" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{expanded &&
|
|
||||||
<ul className="dropdown-menu dropdown-menu-right">
|
|
||||||
{options.map((option, i) => {
|
|
||||||
return (
|
|
||||||
<li key={i}>
|
|
||||||
<a
|
|
||||||
className={option === value ? 'active' : ''}
|
|
||||||
onClick={this.onClick}
|
|
||||||
data-value={option}
|
|
||||||
>
|
|
||||||
{option}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Dropdown from './Dropdown';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: string;
|
||||||
|
options: string[];
|
||||||
|
ariaLabel?: string;
|
||||||
|
onChange(value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SimpleDropdown extends Component<Props, void> {
|
||||||
|
public render() {
|
||||||
|
const { options, value, onChange, ariaLabel } = this.props;
|
||||||
|
|
||||||
|
const StringDropdown = Dropdown as new () => Dropdown<string>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StringDropdown
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
ariaLabel={ariaLabel || "dropdown"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
|
export { default as ColorDropdown } from './ColorDropdown';
|
||||||
export { default as Dropdown } from './Dropdown';
|
export { default as Dropdown } from './Dropdown';
|
||||||
|
export { default as DropdownShell } from './DropdownShell';
|
||||||
export { default as Identicon } from './Identicon';
|
export { default as Identicon } from './Identicon';
|
||||||
export { default as Modal } from './Modal';
|
export { default as Modal } from './Modal';
|
||||||
export { default as UnlockHeader } from './UnlockHeader';
|
export { default as UnlockHeader } from './UnlockHeader';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import SimpleDropDown from 'components/ui/SimpleDropDown';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import SimpleDropdown from 'components/ui/SimpleDropdown';
|
||||||
|
|
||||||
interface UnitDropdownProps {
|
interface UnitDropdownProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -22,7 +22,7 @@ export default class UnitDropdown extends React.Component<
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="input-group-btn">
|
<div className="input-group-btn">
|
||||||
<SimpleDropDown
|
<SimpleDropdown
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
options={options}
|
options={options}
|
||||||
|
|
|
@ -43,6 +43,16 @@
|
||||||
);
|
);
|
||||||
padding: .1rem .6rem .2rem;
|
padding: .1rem .6rem .2rem;
|
||||||
}
|
}
|
||||||
|
// This is a "smaller" small, to accomodate overrides done in v3.
|
||||||
|
.btn-smr {
|
||||||
|
@include button-size(
|
||||||
|
.4rem,
|
||||||
|
1rem,
|
||||||
|
14px,
|
||||||
|
$line-height-base,
|
||||||
|
$border-radius
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Custom color
|
// Custom color
|
||||||
.btn-white {
|
.btn-white {
|
||||||
|
|
|
@ -7,6 +7,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If it's a span, we probably want it inline-block, not inline
|
||||||
|
span.dropdown {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
Loading…
Reference in New Issue