diff --git a/src/components/CounterValue/stories.js b/src/components/CounterValue/stories.js index edf10f81..ef479cbf 100644 --- a/src/components/CounterValue/stories.js +++ b/src/components/CounterValue/stories.js @@ -12,5 +12,5 @@ const stories = storiesOf('Components', module) const currency = getCryptoCurrencyById('bitcoin') stories.add('CounterValue', () => ( - + )) diff --git a/src/components/MainSideBar/AddAccountButton.js b/src/components/MainSideBar/AddAccountButton.js index 4e7054a1..7e74210a 100644 --- a/src/components/MainSideBar/AddAccountButton.js +++ b/src/components/MainSideBar/AddAccountButton.js @@ -19,7 +19,6 @@ const PlusWrapper = styled(Tabbable).attrs({ color: ${p => p.theme.colors.dark}; } - border: 1px solid transparent; &:focus { outline: none; border-color: ${p => rgba(p.theme.colors.wallet, 0.3)}; diff --git a/src/components/SettingsPage/SettingsSection.js b/src/components/SettingsPage/SettingsSection.js index 7542e920..f9343fc8 100644 --- a/src/components/SettingsPage/SettingsSection.js +++ b/src/components/SettingsPage/SettingsSection.js @@ -56,7 +56,7 @@ export function SettingsSectionHeader({ renderRight?: any, }) { return ( - + {icon} @@ -100,7 +100,7 @@ export function SettingsSectionRow({ onClick?: ?Function, }) { return ( - + {title} diff --git a/src/components/SettingsPage/sections/About.js b/src/components/SettingsPage/sections/About.js index 60e3610e..c2e2bf50 100644 --- a/src/components/SettingsPage/sections/About.js +++ b/src/components/SettingsPage/sections/About.js @@ -1,4 +1,5 @@ // @flow +/* eslint-disable react/no-multi-comp */ import React, { PureComponent } from 'react' import { shell } from 'electron' @@ -9,6 +10,7 @@ import type { T } from 'types/common' import IconHelp from 'icons/Help' import IconExternalLink from 'icons/ExternalLink' import Button from 'components/base/Button' +import { Tabbable } from 'components/base/Box' import { openModal } from 'reducers/modals' import { MODAL_RELEASES_NOTES } from 'config/constants' @@ -29,8 +31,29 @@ const mapDispatchToProps = { openModal, } +const ITEMS = [ + { + key: 'faq', + title: t => t('app:settings.about.faq'), + desc: t => t('app:settings.about.faqDesc'), + url: 'https://support.ledgerwallet.com/hc/en-us', + }, + { + key: 'contact', + title: t => t('app:settings.about.contactUs'), + desc: t => t('app:settings.about.contactUsDesc'), + url: 'https://support.ledgerwallet.com/hc/en-us/requests/new', + }, + { + key: 'terms', + title: t => t('app:settings.about.terms'), + desc: t => t('app:settings.about.termsDesc'), + url: 'https://www.ledgerwallet.com/terms', + }, +] + class SectionAbout extends PureComponent { - handleOpenLink = (url: string) => () => shell.openExternal(url) + handleOpenLink = (url: string) => shell.openExternal(url) render() { const { t, openModal } = this.props @@ -54,33 +77,41 @@ class SectionAbout extends PureComponent { {t('app:settings.about.releaseNotesBtn')} - - - - - - - - - + {ITEMS.map(item => ( + + ))} ) } } +class AboutRowItem extends PureComponent<{ + onClick: string => void, + url: string, + title: string, + desc: string, + url: string, +}> { + render() { + const { onClick, title, desc, url } = this.props + const boundOnClick = () => onClick(url) + return ( + + + + + + ) + } +} + export default connect( null, mapDispatchToProps, diff --git a/src/components/StickyBackToTop.js b/src/components/StickyBackToTop.js index 13dc2f6c..5d7ce8d0 100644 --- a/src/components/StickyBackToTop.js +++ b/src/components/StickyBackToTop.js @@ -50,6 +50,7 @@ class StickyBackToTop extends PureComponent { } componentDidMount() { + if (!this.context.getScrollbar) return this.context.getScrollbar(scrollbar => { const listener = () => { const { scrollTop } = scrollbar diff --git a/src/components/TopBar/ItemContainer.js b/src/components/TopBar/ItemContainer.js index f0b693d0..3d954467 100644 --- a/src/components/TopBar/ItemContainer.js +++ b/src/components/TopBar/ItemContainer.js @@ -15,7 +15,6 @@ export default styled(Tabbable).attrs({ borderRadius: 1, })` height: 40px; - border: 1px dashed transparent; &:hover { color: ${p => (p.isDisabled ? '' : p.theme.colors.dark)}; @@ -25,8 +24,4 @@ export default styled(Tabbable).attrs({ &:active { background: ${p => (p.isDisabled ? '' : rgba(p.theme.colors.fog, 0.3))}; } - - &:focus { - outline: none; - } ` diff --git a/src/components/base/Box/Tabbable.js b/src/components/base/Box/Tabbable.js index 1fcee73d..d77562e3 100644 --- a/src/components/base/Box/Tabbable.js +++ b/src/components/base/Box/Tabbable.js @@ -1,52 +1,61 @@ // @flow -import React, { PureComponent } from 'react' +import React, { Component } from 'react' +import styled from 'styled-components' + +import { isGlobalTabEnabled } from 'renderer/init' +import { rgba } from 'styles/helpers' import Box from './index' -// Github like focus style: -// - focus states are not visible by default -// - first time user hit tab, enable global tab to see focus states -const __IS_GLOBAL_TAB_ENABLED__ = false +const KEY_ENTER = 13 -export default class Tabbable extends PureComponent< - any, - { - isFocused: boolean, - }, -> { - state = { - isFocused: false, - } +export const focusedShadowStyle = ` + 0 0 0 1px ${rgba('#0a84ff', 0.5)} inset, + 0 0 0 1px ${rgba('#0a84ff', 0.3)}, + 0 0 0 4px rgba(10, 132, 255, 0.1) +` - componentDidMount() { - window.addEventListener('keydown', this.handleKeydown) +const Raw = styled(Box)` + &:focus { + outline: none; + box-shadow: ${p => (p.isFocused && !p.unstyled ? focusedShadowStyle : 'none')}; } +` - componentWillUnmount() { - window.removeEventListener('keydown', this.handleKeydown) +export default class Tabbable extends Component< + { disabled?: boolean, unstyled?: boolean, onClick?: any => void }, + { isFocused: boolean }, +> { + state = { + isFocused: false, } handleFocus = () => { - if (!__IS_GLOBAL_TAB_ENABLED__) return - this.setState({ isFocused: true }) + if (isGlobalTabEnabled()) { + this.setState({ isFocused: true }) + } } handleBlur = () => this.setState({ isFocused: false }) - handleKeydown = (e: SyntheticKeyboardEvent) => { - if ((e.which === 13 || e.which === 32) && this.state.isFocused && this.props.onClick) { - this.props.onClick(e) - } + handleKeyPress = (e: SyntheticKeyboardEvent<*>) => { + const { isFocused } = this.state + const { onClick } = this.props + const canPress = e.which === KEY_ENTER && isGlobalTabEnabled() && isFocused + if (canPress && onClick) onClick(e) } render() { const { disabled } = this.props + const { isFocused } = this.state return ( - ) diff --git a/src/components/base/Box/index.js b/src/components/base/Box/index.js index fd7a6619..fe5a6b32 100644 --- a/src/components/base/Box/index.js +++ b/src/components/base/Box/index.js @@ -1,5 +1,7 @@ // @flow -export default from './Box' +import Box from './Box' + export { default as Tabbable } from './Tabbable' export { default as Card } from './Card' +export default Box diff --git a/src/components/base/Button/index.js b/src/components/base/Button/index.js index cabd6366..b66fc1f1 100644 --- a/src/components/base/Button/index.js +++ b/src/components/base/Button/index.js @@ -5,12 +5,21 @@ import styled from 'styled-components' import { space, fontSize, fontWeight, color } from 'styled-system' import noop from 'lodash/noop' -import { darken, lighten } from 'styles/helpers' +import { darken, lighten, rgba } from 'styles/helpers' import fontFamily from 'styles/styled/fontFamily' +import { focusedShadowStyle } from 'components/base/Box/Tabbable' import Spinner from 'components/base/Spinner' const buttonStyles = { + default: { + default: noop, + active: noop, + hover: noop, + focus: () => ` + box-shadow: ${focusedShadowStyle}; + `, + }, primary: { default: p => ` background: ${p.disabled ? `${p.theme.colors.lightFog} !important` : p.theme.colors.wallet}; @@ -22,6 +31,12 @@ const buttonStyles = { active: p => ` background: ${darken(p.theme.colors.wallet, 0.1)}; `, + focus: p => ` + box-shadow: + 0 0 0 1px ${darken(p.theme.colors.wallet, 0.3)} inset, + 0 0 0 1px ${rgba(p.theme.colors.wallet, 0.5)}, + 0 0 0 4px ${rgba(p.theme.colors.wallet, 0.3)}; + `, }, danger: { default: p => ` @@ -34,6 +49,12 @@ const buttonStyles = { active: p => ` background: ${darken(p.theme.colors.alertRed, 0.1)}; `, + focus: p => ` + box-shadow: + 0 0 0 1px ${darken(p.theme.colors.alertRed, 0.3)} inset, + 0 0 0 1px ${rgba(p.theme.colors.alertRed, 0.5)}, + 0 0 0 4px ${rgba(p.theme.colors.alertRed, 0.3)}; + `, }, outline: { default: p => ` @@ -86,6 +107,10 @@ const buttonStyles = { function getStyles(props, state) { let output = `` + const defaultStyle = buttonStyles.default[state] + if (defaultStyle) { + output += defaultStyle(props) || '' + } for (const s in buttonStyles) { if (buttonStyles.hasOwnProperty(s) && props[s] === true) { const style = buttonStyles[s][state] @@ -125,6 +150,9 @@ const Base = styled.button.attrs({ &:active { ${p => getStyles(p, 'active')}; } + &:focus { + ${p => getStyles(p, 'focus')}; + } ` type Props = { diff --git a/src/components/base/Modal/ModalTitle.js b/src/components/base/Modal/ModalTitle.js index e89678a5..c4f4c7ed 100644 --- a/src/components/base/Modal/ModalTitle.js +++ b/src/components/base/Modal/ModalTitle.js @@ -20,6 +20,7 @@ const Container = styled(Box).attrs({ })`` const Back = styled(Box).attrs({ + unstyled: true, horizontal: true, align: 'center', color: 'grey', @@ -39,6 +40,13 @@ const Back = styled(Box).attrs({ &:active { color: ${p => p.theme.colors.dark}; } + + span { + border-bottom: 1px dashed transparent; + } + &:focus span { + border-bottom-color: inherit; + } ` function ModalTitle({ @@ -56,7 +64,7 @@ function ModalTitle({ {onBack && ( - {t('app:common.back')} + {t('app:common.back')} )} {children} diff --git a/src/components/base/Modal/index.js b/src/components/base/Modal/index.js index 67f8d24d..d005b33e 100644 --- a/src/components/base/Modal/index.js +++ b/src/components/base/Modal/index.js @@ -16,7 +16,7 @@ import { radii } from 'styles/theme' import { closeModal, isModalOpened, getModalData } from 'reducers/modals' -import Box, { Tabbable } from 'components/base/Box' +import Box from 'components/base/Box' import GrowScroll from 'components/base/GrowScroll' import Defer from 'components/base/Defer' @@ -75,7 +75,7 @@ const Backdrop = styled(Box).attrs({ position: fixed; ` -const Wrapper = styled(Tabbable).attrs({ +const Wrapper = styled(Box).attrs({ bg: 'transparent', flow: 4, style: p => ({ @@ -107,10 +107,11 @@ class Pure extends Component { type Props = { data?: any, isOpened: boolean, - onClose: Function, + onClose?: Function, onHide?: Function, preventBackdropClick: boolean, render: Function, + refocusWhenChange?: string, } export class Modal extends Component { @@ -133,17 +134,14 @@ export class Modal extends Component { componentDidUpdate(prevProps: Props) { const didOpened = this.props.isOpened && !prevProps.isOpened const didClose = !this.props.isOpened && prevProps.isOpened + const shouldFocus = didOpened || this.props.refocusWhenChange !== prevProps.refocusWhenChange if (didOpened) { // Store a reference to the last active element, to restore it after // modal close this._lastFocusedElement = document.activeElement - - // Forced to use findDOMNode here, because innerRef is giving a proxied component - const domWrapper = findDOMNode(this._wrapper) // eslint-disable-line react/no-find-dom-node - - if (domWrapper instanceof HTMLDivElement) { - domWrapper.focus() - } + } + if (shouldFocus) { + this.focusWrapper() } if (didClose) { @@ -156,6 +154,15 @@ export class Modal extends Component { _wrapper = null _lastFocusedElement = null + focusWrapper = () => { + // Forced to use findDOMNode here, because innerRef is giving a proxied component + const domWrapper = findDOMNode(this._wrapper) // eslint-disable-line react/no-find-dom-node + + if (domWrapper instanceof HTMLDivElement) { + domWrapper.focus() + } + } + render() { const { preventBackdropClick, isOpened, onHide, render, data, onClose } = this.props @@ -175,6 +182,7 @@ export class Modal extends Component { (this._wrapper = n)} diff --git a/src/components/base/SideBar/SideBarListItem.js b/src/components/base/SideBar/SideBarListItem.js index 96c1e738..cc5e508b 100644 --- a/src/components/base/SideBar/SideBarListItem.js +++ b/src/components/base/SideBar/SideBarListItem.js @@ -4,7 +4,6 @@ import React, { PureComponent } from 'react' import styled from 'styled-components' import Box, { Tabbable } from 'components/base/Box' -import { rgba } from 'styles/helpers' export type Props = { label: string | (Props => React$Element), @@ -77,12 +76,6 @@ const Container = styled(Tabbable).attrs({ color: ${p => !p.disabled && p.theme.colors.dark}; } - border: 1px solid transparent; - &:focus { - outline: none; - border-color: ${p => rgba(p.theme.colors.wallet, 0.3)}; - } - ${p => { const iconActiveColor = p.theme.colors[p.iconActiveColor] || p.iconActiveColor const color = p.isActive ? iconActiveColor : p.theme.colors.grey diff --git a/src/components/layout/Default.js b/src/components/layout/Default.js index e87ac31e..486690fb 100644 --- a/src/components/layout/Default.js +++ b/src/components/layout/Default.js @@ -26,6 +26,7 @@ import TopBar from 'components/TopBar' const Container = styled(GrowScroll).attrs({ p: 6, })` + outline: none; padding-top: ${p => p.theme.sizes.topBarHeight + p.theme.space[7]}px; ` @@ -40,7 +41,12 @@ class Default extends Component { componentDidUpdate(prevProps) { if (this.props.location !== prevProps.location) { - if (this._scrollContainer) { + const canScroll = + this._scrollContainer && + this._scrollContainer._scrollbar && + this._scrollContainer._scrollbar.scrollTo + if (canScroll) { + // $FlowFixMe already checked this._scrollContainer this._scrollContainer._scrollbar.scrollTo(0, 0) } } @@ -70,7 +76,7 @@ class Default extends Component { - (this._scrollContainer = n)}> + (this._scrollContainer = n)} tabIndex={-1}> diff --git a/src/components/modals/ImportAccounts/AccountRow.js b/src/components/modals/ImportAccounts/AccountRow.js index e571e5bb..32303cd9 100644 --- a/src/components/modals/ImportAccounts/AccountRow.js +++ b/src/components/modals/ImportAccounts/AccountRow.js @@ -6,7 +6,7 @@ import type { Account } from '@ledgerhq/live-common/lib/types' import { darken } from 'styles/helpers' -import Box from 'components/base/Box' +import Box, { Tabbable } from 'components/base/Box' import Radio from 'components/base/Radio' import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon' import FormattedVal from 'components/base/FormattedVal' @@ -116,7 +116,7 @@ export default class AccountRow extends PureComponent { } } -const AccountRowContainer = styled(Box).attrs({ +const AccountRowContainer = styled(Tabbable).attrs({ horizontal: true, align: 'center', bg: 'lightGrey', diff --git a/src/components/modals/ImportAccounts/index.js b/src/components/modals/ImportAccounts/index.js index b80fee45..ddac622a 100644 --- a/src/components/modals/ImportAccounts/index.js +++ b/src/components/modals/ImportAccounts/index.js @@ -196,7 +196,7 @@ class ImportAccounts extends PureComponent { return ( this.setState({ ...INITIAL_STATE })} render={({ onClose }) => ( diff --git a/src/logger.js b/src/logger.js index eb1a2a80..0dd9df43 100644 --- a/src/logger.js +++ b/src/logger.js @@ -49,6 +49,7 @@ const makeSerializableLog = (o: mixed) => { const logClicks = !__DEV__ || process.env.DEBUG_CLICK_ELEMENT const logRedux = !__DEV__ || process.env.DEBUG_ACTION +const logTabkey = __DEV__ || process.env.DEBUG_TAB_KEY export default { // tracks the user interactions (click, input focus/blur, what else?) @@ -77,6 +78,17 @@ export default { addLog('action', `⚛️ ${action.type}`, action) }, + // tracks keyboard events + onTabKey: activeElement => { + const { classList, tagName } = activeElement + const displayEl = `${tagName.toLowerCase()}${classList.length ? ` ${classList[0]}` : ''}` + const msg = `⇓ - active element ${displayEl}` + if (logTabkey) { + console.log(msg) + } + addLog('keydown', msg) + }, + // General functions in case the hooks don't apply log: (...args: any) => { diff --git a/src/renderer/init.js b/src/renderer/init.js index 6ef76a10..2dc4bd99 100644 --- a/src/renderer/init.js +++ b/src/renderer/init.js @@ -28,6 +28,15 @@ import 'styles/global' const rootNode = document.getElementById('app') +// Github like focus style: +// - focus states are not visible by default +// - first time user hit tab, enable global tab to see focus states +let IS_GLOBAL_TAB_ENABLED = false +const TAB_KEY = 9 + +export const isGlobalTabEnabled = () => IS_GLOBAL_TAB_ENABLED +export const enableGlobalTab = () => (IS_GLOBAL_TAB_ENABLED = true) + async function init() { if (process.env.LEDGER_RESET_ALL) { await hardReset() @@ -85,6 +94,23 @@ async function init() { } } }) + + window.addEventListener('keydown', (e: SyntheticKeyboardEvent) => { + if (e.which === TAB_KEY) { + if (!isGlobalTabEnabled()) enableGlobalTab() + logger.onTabKey(document.activeElement) + } + }) + + window.addEventListener('click', ({ target }) => { + const { dataset } = target + if (dataset) { + const { role, roledata } = dataset + if (role) { + logger.onClickElement(role, roledata) + } + } + }) } } diff --git a/src/styles/reset.js b/src/styles/reset.js index fff1b4a9..cf45923f 100644 --- a/src/styles/reset.js +++ b/src/styles/reset.js @@ -7,6 +7,7 @@ module.exports = `* { color: inherit; user-select: none; min-width: 0; + outline: none; /* it will surely make problem in the future... to be inspected. */ /* ;_; */ diff --git a/src/styles/theme.js b/src/styles/theme.js index 4914185f..15dd9de7 100644 --- a/src/styles/theme.js +++ b/src/styles/theme.js @@ -1,5 +1,7 @@ // @flow +import { rgba } from 'styles/helpers' + export const space = [0, 5, 10, 15, 20, 30, 40, 50, 70] export const fontSizes = [8, 9, 10, 12, 13, 16, 18, 22, 32] export const radii = [0, 4] @@ -93,6 +95,7 @@ export default { topBarHeight: 58, sideBarWidth: 230, }, + focusBoxShadow: `${rgba(colors.wallet, 0.2)} 0 2px 5px`, radii, fontFamilies, fontSizes,