From 1a09c6a7a6799dbde0e8568625166fb94029392a Mon Sep 17 00:00:00 2001 From: William O'Beirne Date: Mon, 2 Oct 2017 15:36:59 -0700 Subject: [PATCH] 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. --- .../Header/components/GasPriceDropdown.tsx | 120 +++++++++--------- common/components/Header/index.scss | 77 ++++------- common/components/Header/index.tsx | 99 ++++++++------- common/components/ui/ColorDropdown.tsx | 106 ++++++++++++++++ common/components/ui/Dropdown.tsx | 112 ++++++++-------- common/components/ui/DropdownShell.tsx | 95 ++++++++++++++ common/components/ui/SimpleDropDown.tsx | 61 --------- common/components/ui/SimpleDropdown.tsx | 26 ++++ common/components/ui/index.ts | 2 + .../components/UnitDropdown.tsx | 4 +- common/sass/styles/overrides/buttons.scss | 10 ++ common/sass/styles/overrides/dropdowns.scss | 5 + package.json | 8 +- 13 files changed, 448 insertions(+), 277 deletions(-) create mode 100644 common/components/ui/ColorDropdown.tsx create mode 100644 common/components/ui/DropdownShell.tsx delete mode 100644 common/components/ui/SimpleDropDown.tsx create mode 100644 common/components/ui/SimpleDropdown.tsx diff --git a/common/components/Header/components/GasPriceDropdown.tsx b/common/components/Header/components/GasPriceDropdown.tsx index b50640c8..5567d572 100644 --- a/common/components/Header/components/GasPriceDropdown.tsx +++ b/common/components/Header/components/GasPriceDropdown.tsx @@ -1,90 +1,86 @@ import { gasPriceDefaults } from 'config/data'; import throttle from 'lodash/throttle'; import React, { Component } from 'react'; +import DropdownShell from 'components/ui/DropdownShell'; import './GasPriceDropdown.scss'; interface Props { value: number; onChange(gasPrice: number): void; } -interface State { - expanded: boolean; -} -export default class GasPriceDropdown extends Component { - public state = { expanded: false }; +export default class GasPriceDropdown extends Component { constructor(props: Props) { super(props); this.updateGasPrice = throttle(this.updateGasPrice, 50); } public render() { - const { expanded } = this.state; + const { value } = this.props; return ( - - - Gas Price: {this.props.value} Gwei - - - {expanded && -
    -
    - Gas Price: {this.props.value} Gwei - -

    - Not So Fast -

    -

    - Fast -

    -

    - Fast AF -

    -

    - Gas Price is the amount you pay per unit of gas.{' '} - TX fee = gas price * gas limit & is paid to miners - for including your TX in a block. Higher the gas price = faster - transaction, but more expensive. Default is 21 GWEI. -

    -

    - {/* TODO: maybe not hardcode a link? :) */} - - Read more - -

    -
    -
} -
+ ); } - public toggleExpanded = () => { - this.setState(state => { - return { - expanded: !state.expanded - }; - }); + private renderLabel = () => { + return `Gas Price: ${this.props.value} Gwei`; }; - public updateGasPrice = (value: string) => { + private renderOptions = () => { + const { value } = this.props; + return ( +
+
+ Gas Price: {value} Gwei + +

+ Not So Fast +

+

+ Fast +

+

+ Fast AF +

+

+ Gas Price is the amount you pay per unit of gas.{' '} + TX fee = gas price * gas limit & is paid to miners for + including your TX in a block. Higher the gas price = faster + transaction, but more expensive. Default is 21 GWEI. +

+

+ {/* TODO: maybe not hardcode a link? :) */} + + Read more + +

+
+
+ ); + }; + + private updateGasPrice = (value: string) => { this.props.onChange(parseInt(value, 10)); }; - public handleGasPriceChange = (e: React.SyntheticEvent) => { + private handleGasPriceChange = ( + e: React.SyntheticEvent + ) => { this.updateGasPrice((e.target as HTMLInputElement).value); }; } diff --git a/common/components/Header/index.scss b/common/components/Header/index.scss index bab2bfa4..b063d3af 100644 --- a/common/components/Header/index.scss +++ b/common/components/Header/index.scss @@ -92,66 +92,37 @@ $small-size: 900px; padding: 5px 0; min-width: 220px; } - - &-tagline { - font-size: 18px; - font-weight: 200; - color: white; - flex: 1 auto; - text-align: right; - padding: 5px 0; - @include small-query { - text-align: center; - } - > * { - display: inline; - vertical-align: middle; - } - - &-version { - max-width: 395px; - } - } } - a { + &-right { + font-size: 18px; + font-weight: 200; color: white; - cursor: pointer; - font-weight: 400; - transition: 250ms all ease; - - &:hover, - &:active { - opacity: .8; - color: white; - text-decoration: none; - transition: 250ms all ease; - } - } - - // TODO - Move to dropdown component? - .dropdown { - margin-left: 15px; - padding: 0; + flex: 1 auto; text-align: right; - white-space: nowrap; + padding: 0 0 5px; + @include small-query { + text-align: center; + } - .dropdown-menu { - right: -10px; - left: auto; - min-width: auto; - left: auto; + > * { + display: inline-block; + vertical-align: middle; + margin-top: 5px; + } - & > li > a { - font-size: 15px; - padding: 5px 30px 5px 15px; - position: relative; + &-version { + max-width: 395px; + margin-right: 10px; + } - &.active { - text-decoration: none; - color: $brand-primary; - background-color: $gray-lightest; - } + &-dropdown { + margin-left: 6px; + + &-add { + text-align: center; + padding-top: $space-sm !important; + padding-bottom: $space-sm !important; } } } diff --git a/common/components/Header/index.tsx b/common/components/Header/index.tsx index 72114936..ec4d9fba 100644 --- a/common/components/Header/index.tsx +++ b/common/components/Header/index.tsx @@ -1,6 +1,6 @@ import { TChangeGasPrice, TChangeLanguage, TChangeNode } from 'actions/config'; 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 { Link } from 'react-router-dom'; import { @@ -36,7 +36,18 @@ export default class Header extends Component { const LanguageDropDown = Dropdown as new () => Dropdown< typeof selectedLanguage >; - const NodeDropDown = Dropdown as new () => Dropdown; + const nodeOptions = Object.keys(NODES).map(key => { + return { + value: key, + name: ( + + {NODES[key].network} ({NODES[key].service}) + + ), + color: NETWORKS[NODES[key].network].color + }; + }); + return (
{ANNOUNCEMENT_MESSAGE && ( @@ -64,45 +75,51 @@ export default class Header extends Component { alt="MyEtherWallet" /> -
- - v{VERSION} - +
+ v{VERSION} - +
+ +
- , -
  • - - Disclaimer - -
  • - ]} - onChange={this.changeLanguage} - /> +
    + + + Disclaimer + + + } + onChange={this.changeLanguage} + size="smr" + color="white" + /> +
    - - Add Custom Node - - } - onChange={changeNode} - /> +
    + + Add Custom Node + + } + onChange={changeNode} + size="smr" + color="white" + /> +
    @@ -121,10 +138,4 @@ export default class Header extends Component { this.props.changeLanguage(key); } }; - - private nodeNetworkAndService = (option: string) => [ - NODES[option].network, - ' ', - ({NODES[option].service}) - ]; } diff --git a/common/components/ui/ColorDropdown.tsx b/common/components/ui/ColorDropdown.tsx new file mode 100644 index 00000000..b75b964a --- /dev/null +++ b/common/components/ui/ColorDropdown.tsx @@ -0,0 +1,106 @@ +// @flow +import React, { Component } from 'react'; +import classnames from 'classnames'; +import DropdownShell from './DropdownShell'; + +interface Option { + name: any; + value: T; + color?: string; +} + +interface Props { + value: T; + options: Option[]; + label?: string; + ariaLabel: string; + extra?: any; + size?: string; + color?: string; + menuAlign?: string; + onChange(value: T): void; +} + +export default class ColorDropdown extends Component, {}> { + private dropdownShell: DropdownShell | null; + + public render() { + const { ariaLabel, color, size } = this.props; + + return ( + (this.dropdownShell = el)} + /> + ); + } + + private renderLabel = () => { + const label = this.props.label ? `${this.props.label}:` : ''; + const activeOption = this.getActiveOption(); + + return ( + + {label} {activeOption ? activeOption.name : '-'} + + ); + }; + + 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 ( +
      + {listItems.map((option, i) => { + if (option.divider) { + return
    • ; + } else { + return ( +
    • + + {option.name} + +
    • + ); + } + })} + {extra &&
    • } + {extra} +
    + ); + }; + + 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); + } +} diff --git a/common/components/ui/Dropdown.tsx b/common/components/ui/Dropdown.tsx index 141a448d..c65d19c9 100644 --- a/common/components/ui/Dropdown.tsx +++ b/common/components/ui/Dropdown.tsx @@ -1,77 +1,87 @@ import React, { Component } from 'react'; +import classnames from 'classnames'; +import DropdownShell from './DropdownShell'; interface Props { value: T; options: T[]; ariaLabel: string; + label?: string; extra?: any; + size?: string; + color?: string; + menuAlign?: string; formatTitle?(option: T): any; onChange(value: T): void; } -interface State { - expanded: boolean; -} - -export default class DropdownComponent extends Component, State> { - public state = { - expanded: false - }; +export default class DropdownComponent extends Component, {}> { + private dropdownShell: DropdownShell | null; public render() { - const { options, value, ariaLabel, extra } = this.props; - const { expanded } = this.state; + const { ariaLabel, color, size } = this.props; return ( - - - {expanded && ( - - )} - + (this.dropdownShell = el)} + /> ); } - public formatTitle = (option: any) => { + private renderLabel = () => { + const { label, value } = this.props; + const labelStr = this.props.label ? `${this.props.label}:` : ''; + return ( + + {labelStr} {this.formatTitle(value)} + + ); + }; + + private renderOptions = () => { + const { options, value, menuAlign, extra } = this.props; + const menuClass = classnames({ + 'dropdown-menu': true, + [`dropdown-menu-${menuAlign || ''}`]: !!menuAlign + }); + + return ( + + ); + }; + + private formatTitle = (option: any) => { if (this.props.formatTitle) { return this.props.formatTitle(option); + } else { + return option; } }; - public toggleExpanded = () => { - this.setState(state => { - return { - expanded: !state.expanded - }; - }); - }; - - public onChange = (value: any) => { + private onChange = (value: any) => { this.props.onChange(value); - this.setState({ expanded: false }); + if (this.dropdownShell) { + this.dropdownShell.close(); + } }; } diff --git a/common/components/ui/DropdownShell.tsx b/common/components/ui/DropdownShell.tsx new file mode 100644 index 00000000..0b2020f6 --- /dev/null +++ b/common/components/ui/DropdownShell.tsx @@ -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 { + 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 ( + (this.dropdown = el)} + > + + {renderLabel()} + + + {expanded && renderOptions()} + + ); + } + + 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 }); + } + }; +} diff --git a/common/components/ui/SimpleDropDown.tsx b/common/components/ui/SimpleDropDown.tsx deleted file mode 100644 index 90793f8d..00000000 --- a/common/components/ui/SimpleDropDown.tsx +++ /dev/null @@ -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 { - public state = { - expanded: false - }; - - public toggleExpanded = () => { - this.setState(state => { - return { expanded: !state.expanded }; - }); - }; - - public onClick = (event: React.SyntheticEvent) => { - 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 ( -
    - - {value} - - - - {expanded && -
      - {options.map((option, i) => { - return ( -
    • - - {option} - -
    • - ); - })} -
    } -
    - ); - } -} diff --git a/common/components/ui/SimpleDropdown.tsx b/common/components/ui/SimpleDropdown.tsx new file mode 100644 index 00000000..74cc81d6 --- /dev/null +++ b/common/components/ui/SimpleDropdown.tsx @@ -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 { + public render() { + const { options, value, onChange, ariaLabel } = this.props; + + const StringDropdown = Dropdown as new () => Dropdown; + + return ( + + ); + } +} diff --git a/common/components/ui/index.ts b/common/components/ui/index.ts index cb9b8db1..dd6af4cd 100644 --- a/common/components/ui/index.ts +++ b/common/components/ui/index.ts @@ -1,4 +1,6 @@ +export { default as ColorDropdown } from './ColorDropdown'; export { default as Dropdown } from './Dropdown'; +export { default as DropdownShell } from './DropdownShell'; export { default as Identicon } from './Identicon'; export { default as Modal } from './Modal'; export { default as UnlockHeader } from './UnlockHeader'; diff --git a/common/containers/Tabs/SendTransaction/components/UnitDropdown.tsx b/common/containers/Tabs/SendTransaction/components/UnitDropdown.tsx index d8d3f0f5..b4531fe7 100644 --- a/common/containers/Tabs/SendTransaction/components/UnitDropdown.tsx +++ b/common/containers/Tabs/SendTransaction/components/UnitDropdown.tsx @@ -1,5 +1,5 @@ -import SimpleDropDown from 'components/ui/SimpleDropDown'; import React from 'react'; +import SimpleDropdown from 'components/ui/SimpleDropdown'; interface UnitDropdownProps { value: string; @@ -22,7 +22,7 @@ export default class UnitDropdown extends React.Component< return (
    -