diff options
author | Fabio Berger <me@fabioberger.com> | 2018-01-28 17:29:15 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-01-28 17:29:15 +0800 |
commit | dd9f5adc2e771f3602461ae708d44536f146b902 (patch) | |
tree | f5d11f3bc288a9711af4ff3c5091f78a8ccc8eb4 /packages/website/ts/components | |
parent | 748d805a32ad7a1ee5f5a212d383b95460d3d828 (diff) | |
download | dexon-0x-contracts-dd9f5adc2e771f3602461ae708d44536f146b902.tar.gz dexon-0x-contracts-dd9f5adc2e771f3602461ae708d44536f146b902.tar.zst dexon-0x-contracts-dd9f5adc2e771f3602461ae708d44536f146b902.zip |
Initial Ledger support implementation
Diffstat (limited to 'packages/website/ts/components')
-rw-r--r-- | packages/website/ts/components/dialogs/ledger_config_dialog.tsx | 46 | ||||
-rw-r--r-- | packages/website/ts/components/dropdowns/network_drop_down.tsx | 40 | ||||
-rw-r--r-- | packages/website/ts/components/portal.tsx | 35 | ||||
-rw-r--r-- | packages/website/ts/components/top_bar/provider_display.tsx | 149 | ||||
-rw-r--r-- | packages/website/ts/components/top_bar/provider_picker.tsx | 88 | ||||
-rw-r--r-- | packages/website/ts/components/top_bar/top_bar.tsx (renamed from packages/website/ts/components/top_bar.tsx) | 52 | ||||
-rw-r--r-- | packages/website/ts/components/top_bar/top_bar_menu_item.tsx (renamed from packages/website/ts/components/top_bar_menu_item.tsx) | 0 | ||||
-rw-r--r-- | packages/website/ts/components/ui/drop_down.tsx (renamed from packages/website/ts/components/ui/drop_down_menu_item.tsx) | 54 |
8 files changed, 419 insertions, 45 deletions
diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx index 60db93c52..aff3f67b1 100644 --- a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx +++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx @@ -7,8 +7,10 @@ import TextField from 'material-ui/TextField'; import * as React from 'react'; import ReactTooltip = require('react-tooltip'); import { Blockchain } from 'ts/blockchain'; +import { NetworkDropDown } from 'ts/components/dropdowns/network_drop_down'; import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button'; import { Dispatcher } from 'ts/redux/dispatcher'; +import { ProviderType } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; @@ -27,27 +29,30 @@ interface LedgerConfigDialogProps { dispatcher: Dispatcher; blockchain: Blockchain; networkId: number; + providerType: ProviderType; } interface LedgerConfigDialogState { - didConnectFail: boolean; + connectionErrMsg: string; stepIndex: LedgerSteps; userAddresses: string[]; addressBalances: BigNumber[]; derivationPath: string; derivationErrMsg: string; + preferredNetworkId: number; } export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> { constructor(props: LedgerConfigDialogProps) { super(props); this.state = { - didConnectFail: false, + connectionErrMsg: '', stepIndex: LedgerSteps.CONNECT, userAddresses: [], addressBalances: [], derivationPath: configs.DEFAULT_DERIVATION_PATH, derivationErrMsg: '', + preferredNetworkId: props.networkId, }; } public render() { @@ -77,7 +82,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, return ( <div> <div className="h4 pt3">Follow these instructions before proceeding:</div> - <ol> + <ol className="mb0"> <li className="pb1">Connect your Ledger Nano S & Open the Ethereum application</li> <li className="pb1">Verify that Browser Support is enabled in Settings</li> <li className="pb1"> @@ -86,7 +91,15 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, Firmware >1.2 </a> </li> + <li>Choose your desired network:</li> </ol> + <div className="pb2"> + <NetworkDropDown + updateSelectedNetwork={this._onSelectedNetworkUpdated.bind(this)} + selectedNetworkId={this.state.preferredNetworkId} + avialableNetworkIds={[1, 42]} + /> + </div> <div className="center pb3"> <LifeCycleRaisedButton isPrimary={true} @@ -95,9 +108,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, labelComplete="Connected!" onClickAsyncFn={this._onConnectLedgerClickAsync.bind(this, true)} /> - {this.state.didConnectFail && ( + {!_.isEmpty(this.state.connectionErrMsg) && ( <div className="pt2 left-align" style={{ color: colors.red200 }}> - Failed to connect. Follow the instructions and try again. + {this.state.connectionErrMsg} </div> )} </div> @@ -172,7 +185,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, } private _onClose() { this.setState({ - didConnectFail: false, + connectionErrMsg: '', }); const isOpen = false; this.props.toggleDialogFn(isOpen); @@ -184,6 +197,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, const selectAddressBalance = this.state.addressBalances[selectedRowIndex]; this.props.dispatcher.updateUserAddress(selectedAddress); this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress); + this.props.blockchain.fetchTokenInformationAsync(); // fire and forget this.props.dispatcher.updateUserEtherBalance(selectAddressBalance); this.setState({ stepIndex: LedgerSteps.CONNECT, @@ -219,7 +233,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, } catch (err) { utils.consoleLog(`Ledger error: ${JSON.stringify(err)}`); this.setState({ - didConnectFail: true, + connectionErrMsg: 'Failed to connect. Follow the instructions and try again.', }); return false; } @@ -241,6 +255,19 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, }); } private async _onConnectLedgerClickAsync() { + const isU2FSupported = await utils.isU2FSupportedAsync(); + if (!isU2FSupported) { + utils.consoleLog(`U2F not supported in this browser`); + this.setState({ + connectionErrMsg: 'U2F not supported by this browser. Try using Chrome.', + }); + return false; + } + + if (this.props.providerType !== ProviderType.Ledger) { + await this.props.blockchain.updateProviderToLedgerAsync(this.state.preferredNetworkId); + } + const didSucceed = await this._fetchAddressesAndBalancesAsync(); if (didSucceed) { this.setState({ @@ -258,4 +285,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, } return userAddresses; } + private _onSelectedNetworkUpdated(e: any, index: number, networkId: number) { + this.setState({ + preferredNetworkId: networkId, + }); + } } diff --git a/packages/website/ts/components/dropdowns/network_drop_down.tsx b/packages/website/ts/components/dropdowns/network_drop_down.tsx new file mode 100644 index 000000000..28ec28ed5 --- /dev/null +++ b/packages/website/ts/components/dropdowns/network_drop_down.tsx @@ -0,0 +1,40 @@ +import * as _ from 'lodash'; +import DropDownMenu from 'material-ui/DropDownMenu'; +import MenuItem from 'material-ui/MenuItem'; +import * as React from 'react'; +import { constants } from 'ts/utils/constants'; + +interface NetworkDropDownProps { + updateSelectedNetwork: (e: any, index: number, value: number) => void; + selectedNetworkId: number; + avialableNetworkIds: number[]; +} + +interface NetworkDropDownState {} + +export class NetworkDropDown extends React.Component<NetworkDropDownProps, NetworkDropDownState> { + public render() { + return ( + <div className="mx-auto" style={{ width: 120 }}> + <DropDownMenu value={this.props.selectedNetworkId} onChange={this.props.updateSelectedNetwork}> + {this._renderDropDownItems()} + </DropDownMenu> + </div> + ); + } + private _renderDropDownItems() { + const items = _.map(this.props.avialableNetworkIds, networkId => { + const networkName = constants.NETWORK_NAME_BY_ID[networkId]; + const primaryText = ( + <div className="flex"> + <div className="pr1" style={{ width: 14, paddingTop: 2 }}> + <img src={`/images/network_icons/${networkName.toLowerCase()}.png`} style={{ width: 14 }} /> + </div> + <div>{networkName}</div> + </div> + ); + return <MenuItem key={networkId} value={networkId} primaryText={primaryText} />; + }); + return items; + } +} diff --git a/packages/website/ts/components/portal.tsx b/packages/website/ts/components/portal.tsx index e2e28e8b6..5975569c4 100644 --- a/packages/website/ts/components/portal.tsx +++ b/packages/website/ts/components/portal.tsx @@ -6,6 +6,7 @@ import * as DocumentTitle from 'react-document-title'; import { Route, Switch } from 'react-router-dom'; import { Blockchain } from 'ts/blockchain'; import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog'; +import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog'; import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog'; import { WrappedEthSectionNoticeDialog } from 'ts/components/dialogs/wrapped_eth_section_notice_dialog'; import { EthWrappers } from 'ts/components/eth_wrappers'; @@ -13,19 +14,21 @@ import { FillOrder } from 'ts/components/fill_order'; import { Footer } from 'ts/components/footer'; import { PortalMenu } from 'ts/components/portal_menu'; import { TokenBalances } from 'ts/components/token_balances'; -import { TopBar } from 'ts/components/top_bar'; +import { TopBar } from 'ts/components/top_bar/top_bar'; import { TradeHistory } from 'ts/components/trade_history/trade_history'; import { FlashMessage } from 'ts/components/ui/flash_message'; import { Loading } from 'ts/components/ui/loading'; import { GenerateOrderForm } from 'ts/containers/generate_order_form'; import { localStorage } from 'ts/local_storage/local_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; +import { State } from 'ts/redux/reducer'; import { orderSchema } from 'ts/schemas/order_schema'; import { SchemaValidator } from 'ts/schemas/validator'; import { BlockchainErrs, HashData, Order, + ProviderType, ScreenWidths, Token, TokenByAddress, @@ -46,9 +49,11 @@ export interface PortalAllProps { blockchainIsLoaded: boolean; dispatcher: Dispatcher; hashData: HashData; + injectedProviderName: string; networkId: number; nodeVersion: string; orderFillAmount: BigNumber; + providerType: ProviderType; screenWidth: ScreenWidths; tokenByAddress: TokenByAddress; tokenStateByAddress: TokenStateByAddress; @@ -67,6 +72,7 @@ interface PortalAllState { prevPathname: string; isDisclaimerDialogOpen: boolean; isWethNoticeDialogOpen: boolean; + isLedgerDialogOpen: boolean; } export class Portal extends React.Component<PortalAllProps, PortalAllState> { @@ -96,6 +102,7 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { prevPathname: this.props.location.pathname, isDisclaimerDialogOpen: !hasAcceptedDisclaimer, isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances, + isLedgerDialogOpen: false, }; } public componentDidMount() { @@ -127,8 +134,9 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { this._blockchain.userAddressUpdatedFireAndForgetAsync(nextProps.userAddress); if (!_.isEmpty(nextProps.userAddress) && nextProps.blockchainIsLoaded) { const tokens = _.values(nextProps.tokenByAddress); + const trackedTokens = _.filter(tokens, t => t.isTracked); // tslint:disable-next-line:no-floating-promises - this._updateBalanceAndAllowanceWithLoadingScreenAsync(tokens); + this._updateBalanceAndAllowanceWithLoadingScreenAsync(trackedTokens); } this.setState({ prevUserAddress: nextProps.userAddress, @@ -167,8 +175,14 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { <DocumentTitle title="0x Portal DApp" /> <TopBar userAddress={this.props.userAddress} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + onToggleLedgerDialog={this.onToggleLedgerDialog.bind(this)} + dispatcher={this.props.dispatcher} + providerType={this.props.providerType} blockchainIsLoaded={this.props.blockchainIsLoaded} location={this.props.location} + blockchain={this._blockchain} /> <div id="portal" className="mx-auto max-width-4" style={{ width: '100%' }}> <Paper className="mb3 mt2"> @@ -239,11 +253,26 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)} /> <FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} /> + {this.props.blockchainIsLoaded && ( + <LedgerConfigDialog + providerType={this.props.providerType} + networkId={this.props.networkId} + blockchain={this._blockchain} + dispatcher={this.props.dispatcher} + toggleDialogFn={this.onToggleLedgerDialog.bind(this)} + isOpen={this.state.isLedgerDialogOpen} + /> + )} </div> - <Footer /> + <Footer />; </div> ); } + public onToggleLedgerDialog() { + this.setState({ + isLedgerDialogOpen: !this.state.isLedgerDialogOpen, + }); + } private _renderEthWrapper() { return ( <EthWrappers diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx new file mode 100644 index 000000000..c7b6a4743 --- /dev/null +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -0,0 +1,149 @@ +import * as _ from 'lodash'; +import Menu from 'material-ui/Menu'; +import MenuItem from 'material-ui/MenuItem'; +import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; +import RaisedButton from 'material-ui/RaisedButton'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { Blockchain } from 'ts/blockchain'; +import { ProviderPicker } from 'ts/components/top_bar/provider_picker'; +import { DropDown } from 'ts/components/ui/drop_down'; +import { Identicon } from 'ts/components/ui/identicon'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { ProviderType } from 'ts/types'; +import { colors } from 'ts/utils/colors'; +import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; + +const IDENTICON_DIAMETER = 32; + +interface ProviderDisplayProps { + dispatcher: Dispatcher; + userAddress: string; + networkId: number; + injectedProviderName: string; + providerType: ProviderType; + onToggleLedgerDialog: () => void; + blockchain: Blockchain; +} + +interface ProviderDisplayState {} + +export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> { + public render() { + const isAddressAvailable = !_.isEmpty(this.props.userAddress); + const isExternallyInjectedProvider = ProviderType.Injected && this.props.injectedProviderName !== '0x Public'; + const displayAddress = isAddressAvailable + ? utils.getAddressBeginAndEnd(this.props.userAddress) + : isExternallyInjectedProvider ? 'Account locked' : '0x0000...0000'; + // If the "injected" provider is our fallback public node, then we want to + // show the "connect a wallet" message instead of the providerName + const injectedProviderName = isExternallyInjectedProvider + ? this.props.injectedProviderName + : 'Connect a wallet'; + const providerTitle = + this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S'; + const hoverActiveNode = ( + <div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}> + <div> + <Identicon address={this.props.userAddress} diameter={IDENTICON_DIAMETER} /> + </div> + <div style={{ marginLeft: 12, paddingTop: 1 }}> + <div style={{ fontSize: 12, color: '#FF7F00' }}>{providerTitle}</div> + <div style={{ fontSize: 14 }}>{displayAddress}</div> + </div> + <div style={{ borderLeft: '1px solid #E0E0E0', marginLeft: 17, paddingTop: 1 }} className="px2"> + <i style={{ fontSize: 30, color: '#E0E0E0' }} className="zmdi zmdi zmdi-chevron-down" /> + </div> + </div> + ); + const hasInjectedProvider = + this.props.injectedProviderName !== '0x Public' && this.props.providerType === ProviderType.Injected; + const hasLedgerProvider = this.props.providerType === ProviderType.Ledger; + const horizontalPosition = hasInjectedProvider || hasLedgerProvider ? 'left' : 'middle'; + return ( + <div style={{ width: 'fit-content', height: 48, float: 'right' }}> + <DropDown + hoverActiveNode={hoverActiveNode} + popoverContent={this.renderPopoverContent(hasInjectedProvider, hasLedgerProvider)} + anchorOrigin={{ horizontal: horizontalPosition, vertical: 'bottom' }} + targetOrigin={{ horizontal: horizontalPosition, vertical: 'top' }} + zDepth={1} + /> + </div> + ); + } + public renderPopoverContent(hasInjectedProvider: boolean, hasLedgerProvider: boolean) { + if (hasInjectedProvider || hasLedgerProvider) { + return ( + <ProviderPicker + dispatcher={this.props.dispatcher} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this.props.onToggleLedgerDialog} + blockchain={this.props.blockchain} + /> + ); + } else { + // Nothing to connect to, show install/info popover + return ( + <div className="px2" style={{ maxWidth: 420 }}> + <div className="center h4 py2" style={{ color: colors.grey700 }}> + Choose a wallet: + </div> + <div className="flex pb3"> + <div className="center px2"> + <div style={{ color: colors.darkGrey }}>Install a browser wallet</div> + <div className="py2"> + <img src="/images/metamask_or_parity.png" width="135" /> + </div> + <div> + Use{' '} + <a + href={constants.URL_METAMASK_CHROME_STORE} + target="_blank" + style={{ color: colors.lightBlueA700 }} + > + Metamask + </a>{' '} + or{' '} + <a + href={constants.URL_PARITY_CHROME_STORE} + target="_blank" + style={{ color: colors.lightBlueA700 }} + > + Parity Signer + </a> + </div> + </div> + <div> + <div + className="pl1 ml1" + style={{ borderLeft: `1px solid ${colors.grey300}`, height: 65 }} + /> + <div className="py1">or</div> + <div + className="pl1 ml1" + style={{ borderLeft: `1px solid ${colors.grey300}`, height: 68 }} + /> + </div> + <div className="px2 center"> + <div style={{ color: colors.darkGrey }}>Connect to a ledger hardware wallet</div> + <div style={{ paddingTop: 21, paddingBottom: 29 }}> + <img src="/images/ledger_icon.png" style={{ width: 80 }} /> + </div> + <div> + <RaisedButton + style={{ width: '100%' }} + label="Use Ledger" + onClick={this.props.onToggleLedgerDialog} + /> + </div> + </div> + </div> + </div> + ); + } + } +} diff --git a/packages/website/ts/components/top_bar/provider_picker.tsx b/packages/website/ts/components/top_bar/provider_picker.tsx new file mode 100644 index 000000000..ca98d8d05 --- /dev/null +++ b/packages/website/ts/components/top_bar/provider_picker.tsx @@ -0,0 +1,88 @@ +import * as _ from 'lodash'; +import Menu from 'material-ui/Menu'; +import MenuItem from 'material-ui/MenuItem'; +import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { Blockchain } from 'ts/blockchain'; +import { DropDown } from 'ts/components/ui/drop_down'; +import { Identicon } from 'ts/components/ui/identicon'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { ProviderType } from 'ts/types'; +import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; + +const IDENTICON_DIAMETER = 32; +const SELECTED_BG_COLOR = '#F7F7F7'; + +interface ProviderPickerProps { + networkId: number; + injectedProviderName: string; + providerType: ProviderType; + onToggleLedgerDialog: () => void; + dispatcher: Dispatcher; + blockchain: Blockchain; +} + +interface ProviderPickerState {} + +export class ProviderPicker extends React.Component<ProviderPickerProps, ProviderPickerState> { + public render() { + const isLedgerSelected = this.props.providerType === ProviderType.Ledger; + const menuStyle = { + padding: 10, + paddingTop: 15, + paddingBottom: 15, + }; + const injectedLabel = ( + <div className="flex"> + <div>{this.props.injectedProviderName}</div> + {this._renderNetwork()} + </div> + ); + // Show dropdown with two options + return ( + <div style={{ width: 225, overflow: 'hidden' }}> + <RadioButtonGroup + name="provider" + defaultSelected={this.props.providerType} + onChange={this._onProviderRadioChanged.bind(this)} + > + <RadioButton + style={{ ...menuStyle, backgroundColor: !isLedgerSelected && SELECTED_BG_COLOR }} + value={ProviderType.Injected} + label={injectedLabel} + /> + <RadioButton + style={{ ...menuStyle, backgroundColor: isLedgerSelected && SELECTED_BG_COLOR }} + value={ProviderType.Ledger} + label="Ledger Nano S" + /> + </RadioButtonGroup> + </div> + ); + } + private _renderNetwork() { + const networkName = constants.NETWORK_NAME_BY_ID[this.props.networkId]; + return ( + <div className="flex"> + <div className="pr1 relative" style={{ width: 14, paddingLeft: 14 }}> + <img + src={`/images/network_icons/${networkName.toLowerCase()}.png`} + className="absolute" + style={{ top: 4, width: 14 }} + /> + </div> + <div style={{ color: '#BBBBBB' }}>{networkName}</div> + </div> + ); + } + private _onProviderRadioChanged(e: any, value: string) { + if (value === ProviderType.Ledger) { + this.props.onToggleLedgerDialog(); + } else { + // Fire and forget + this.props.blockchain.updateProviderToInjectedAsync(); + } + } +} diff --git a/packages/website/ts/components/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 11d3e7cc2..652da5435 100644 --- a/packages/website/ts/components/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -1,21 +1,32 @@ import * as _ from 'lodash'; import Drawer from 'material-ui/Drawer'; +import Menu from 'material-ui/Menu'; import MenuItem from 'material-ui/MenuItem'; import * as React from 'react'; import { Link } from 'react-router-dom'; import ReactTooltip = require('react-tooltip'); +import { Blockchain } from 'ts/blockchain'; import { PortalMenu } from 'ts/components/portal_menu'; -import { TopBarMenuItem } from 'ts/components/top_bar_menu_item'; -import { DropDownMenuItem } from 'ts/components/ui/drop_down_menu_item'; +import { ProviderDisplay } from 'ts/components/top_bar/provider_display'; +import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item'; +import { DropDown } from 'ts/components/ui/drop_down'; import { Identicon } from 'ts/components/ui/identicon'; import { DocsInfo } from 'ts/pages/documentation/docs_info'; import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu'; -import { DocsMenu, MenuSubsectionsBySection, Styles, WebsitePaths } from 'ts/types'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { DocsMenu, MenuSubsectionsBySection, ProviderType, Styles, TypeDocNode, WebsitePaths } from 'ts/types'; import { colors } from 'ts/utils/colors'; +import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; interface TopBarProps { userAddress?: string; + networkId?: number; + injectedProviderName?: string; + providerType?: ProviderType; + onToggleLedgerDialog?: () => void; + blockchain?: Blockchain; + dispatcher?: Dispatcher; blockchainIsLoaded: boolean; location: Location; docsVersion?: string; @@ -125,6 +136,15 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { cursor: 'pointer', paddingTop: 16, }; + const hoverActiveNode = ( + <div className="flex relative" style={{ color: menuIconStyle.color }}> + <div style={{ paddingRight: 10 }}>Developers</div> + <div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}> + <i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} /> + </div> + </div> + ); + const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>; return ( <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1"> <div className={parentClassNames}> @@ -138,11 +158,12 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { {!this._isViewingPortal() && ( <div className={menuClasses}> <div className="flex justify-between"> - <DropDownMenuItem - title="Developers" - subMenuItems={developerSectionMenuItems} + <DropDown + hoverActiveNode={hoverActiveNode} + popoverContent={popoverContent} + anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }} + targetOrigin={{ horizontal: 'middle', vertical: 'top' }} style={styles.menuItem} - isNightVersion={isNightVersion} /> <TopBarMenuItem title="Wiki" @@ -167,10 +188,19 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> </div> )} - {this.props.blockchainIsLoaded && - !_.isEmpty(this.props.userAddress) && ( - <div className="col col-5 sm-hide xs-hide">{this._renderUser()}</div> - )} + {this.props.blockchainIsLoaded && ( + <div className="col col-5"> + <ProviderDisplay + dispatcher={this.props.dispatcher} + userAddress={this.props.userAddress} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this.props.onToggleLedgerDialog} + blockchain={this.props.blockchain} + /> + </div> + )} <div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}> <div style={menuIconStyle}> <i className="zmdi zmdi-menu" onClick={this._onMenuButtonClick.bind(this)} /> diff --git a/packages/website/ts/components/top_bar_menu_item.tsx b/packages/website/ts/components/top_bar/top_bar_menu_item.tsx index 96ee86142..96ee86142 100644 --- a/packages/website/ts/components/top_bar_menu_item.tsx +++ b/packages/website/ts/components/top_bar/top_bar_menu_item.tsx diff --git a/packages/website/ts/components/ui/drop_down_menu_item.tsx b/packages/website/ts/components/ui/drop_down.tsx index a578fb4f9..31a67f0d7 100644 --- a/packages/website/ts/components/ui/drop_down_menu_item.tsx +++ b/packages/website/ts/components/ui/drop_down.tsx @@ -1,7 +1,8 @@ import * as _ from 'lodash'; import Menu from 'material-ui/Menu'; -import Popover from 'material-ui/Popover'; +import Popover, { PopoverAnimationVertical } from 'material-ui/Popover'; import * as React from 'react'; +import { MaterialUIPosition, Styles, WebsitePaths } from 'ts/types'; import { colors } from 'ts/utils/colors'; const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300; @@ -9,28 +10,28 @@ const DEFAULT_STYLE = { fontSize: 14, }; -interface DropDownMenuItemProps { - title: string; - subMenuItems: React.ReactNode[]; +interface DropDownProps { + hoverActiveNode: React.ReactNode; + popoverContent: React.ReactNode; + anchorOrigin: MaterialUIPosition; + targetOrigin: MaterialUIPosition; style?: React.CSSProperties; - menuItemStyle?: React.CSSProperties; - isNightVersion?: boolean; + zDepth?: number; } -interface DropDownMenuItemState { +interface DropDownState { isDropDownOpen: boolean; anchorEl?: HTMLInputElement; } -export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, DropDownMenuItemState> { - public static defaultProps: Partial<DropDownMenuItemProps> = { +export class DropDown extends React.Component<DropDownProps, DropDownState> { + public static defaultProps: Partial<DropDownProps> = { style: DEFAULT_STYLE, - menuItemStyle: DEFAULT_STYLE, - isNightVersion: false, + zDepth: 1, }; private _isHovering: boolean; private _popoverCloseCheckIntervalId: number; - constructor(props: DropDownMenuItemProps) { + constructor(props: DropDownProps) { super(props); this.state = { isDropDownOpen: false, @@ -44,30 +45,35 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro public componentWillUnmount() { window.clearInterval(this._popoverCloseCheckIntervalId); } + public componentWillReceiveProps(nextProps: DropDownProps) { + // HACK: If the popoverContent is updated to a different dimension and the users + // mouse is no longer above it, the dropdown can enter an inconsistent state where + // it believes the user is still hovering over it. In order to remedy this, we + // call hoverOff whenever the dropdown receives updated props. This is a hack + // because it will effectively close the dropdown on any prop update, barring + // dropdowns from having dynamic content. + this._onHoverOff(); + } public render() { - const colorStyle = this.props.isNightVersion ? 'white' : this.props.style.color; return ( <div - style={{ ...this.props.style, color: colorStyle }} + style={{ ...this.props.style, width: 'fit-content', height: '100%' }} onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)} > - <div className="flex relative"> - <div style={{ paddingRight: 10 }}>{this.props.title}</div> - <div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}> - <i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} /> - </div> - </div> + {this.props.hoverActiveNode} <Popover open={this.state.isDropDownOpen} anchorEl={this.state.anchorEl} - anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }} - targetOrigin={{ horizontal: 'middle', vertical: 'top' }} + anchorOrigin={this.props.anchorOrigin} + targetOrigin={this.props.targetOrigin} onRequestClose={this._closePopover.bind(this)} useLayerForClickAway={false} + animation={PopoverAnimationVertical} + zDepth={this.props.zDepth} > <div onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}> - <Menu style={{ color: colors.grey }}>{this.props.subMenuItems}</Menu> + {this.props.popoverContent} </div> </Popover> </div> @@ -87,7 +93,7 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro anchorEl: event.currentTarget, }); } - private _onHoverOff(event: React.FormEvent<HTMLInputElement>) { + private _onHoverOff() { this._isHovering = false; } private _checkIfShouldClosePopover() { |