Browse Source

Merge pull request #64 from loeck/master

Clean custom scrollbar
master
Loëck Vézien 7 years ago
committed by GitHub
parent
commit
9925b27aac
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      package.json
  2. 39
      src/components/SelectAccount.js
  3. 63
      src/components/SelectAccount/index.js
  4. 49
      src/components/SelectAccount/stories.js
  5. 3
      src/components/SideBar/index.js
  6. 3
      src/components/Wrapper.js
  7. 91
      src/components/base/GrowScroll/index.js
  8. 13
      src/components/base/GrowScroll/stories.js
  9. 1
      src/components/base/Icon/index.js
  10. 1
      src/components/base/Modal/index.js
  11. 160
      src/components/base/Select/index.js
  12. 25
      src/styles/global.js
  13. 3
      static/i18n/en/translation.yml
  14. 3
      static/i18n/fr/translation.yml
  15. 32
      yarn.lock

7
package.json

@ -75,12 +75,14 @@
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"react-router-redux": "5.0.0-alpha.9",
"react-smooth-scrollbar": "^8.0.6",
"redux": "^3.7.2",
"redux-actions": "^2.2.1",
"redux-thunk": "^2.2.0",
"shortid": "^2.2.8",
"smooth-scrollbar": "^8.2.5",
"source-map-support": "^0.5.3",
"styled-components": "^3.1.2",
"styled-components": "^3.1.4",
"styled-system": "^1.1.1"
},
"devDependencies": {
@ -98,8 +100,9 @@
"babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"chance": "^1.0.13",
"concurrently": "^3.5.1",
"dotenv": "^4.0.0",
"dotenv": "^5.0.0",
"electron": "1.7.11",
"electron-builder": "^19.55.2",
"electron-devtools-installer": "^2.2.3",

39
src/components/SelectAccount.js

@ -1,39 +0,0 @@
// @flow
import React from 'react'
import { connect } from 'react-redux'
import type { MapStateToProps } from 'react-redux'
import { getAccounts } from 'reducers/accounts'
import Select from 'components/base/Select'
import type { Account } from 'types/common'
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
accounts: Object.entries(getAccounts(state)).map(([, account]: [string, any]) => account),
})
type Props = {
accounts: Array<Account>,
onChange: () => Account | void,
value: Account | null,
}
const SelectAccount = ({ accounts, value, onChange }: Props) => (
<Select
value={value}
renderSelected={item => item.name}
renderItem={item => (
<div key={item.id}>
{item.name} - {item.data.balance}
</div>
)}
keyProp="id"
items={accounts}
placeholder="Choose an account"
onChange={onChange}
/>
)
export default connect(mapStateToProps)(SelectAccount)

63
src/components/SelectAccount/index.js

@ -0,0 +1,63 @@
// @flow
import React from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import noop from 'lodash/noop'
import type { MapStateToProps } from 'react-redux'
import type { T, Account } from 'types/common'
import { formatBTC } from 'helpers/format'
import { getAccounts } from 'reducers/accounts'
import Select from 'components/base/Select'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
const mapStateToProps: MapStateToProps<*, *, *> = state => ({
accounts: Object.entries(getAccounts(state)).map(([, account]: [string, any]) => account),
})
const renderItem = item => (
<Box horizontal align="center">
<Box grow>
<Text color="night" fontSize={0} fontWeight="bold">
{item.name}
</Text>
</Box>
<Box>
<Text color="mouse" fontSize={0}>
{formatBTC(item.data.balance)}
</Text>
</Box>
</Box>
)
type Props = {
accounts: Array<Account>,
onChange?: () => Account | void,
value?: Account | null,
t: T,
}
export const SelectAccount = ({ accounts, onChange, value, t }: Props) => (
<Select
value={value && accounts.find(a => value && a.id === value.id)}
renderSelected={renderItem}
renderItem={renderItem}
keyProp="id"
items={accounts}
placeholder={t('SelectAccount.placeholder')}
onChange={onChange}
/>
)
SelectAccount.defaultProps = {
onChange: noop,
value: undefined,
}
export default compose(connect(mapStateToProps), translate())(SelectAccount)

49
src/components/SelectAccount/stories.js

@ -0,0 +1,49 @@
// @flow
import React, { PureComponent } from 'react'
import { storiesOf } from '@storybook/react'
import Chance from 'chance'
import { SelectAccount } from 'components/SelectAccount'
const chance = new Chance()
const stories = storiesOf('SelectAccount', module)
const accounts = [...Array(20)].map(() => ({
id: chance.string(),
name: chance.name(),
type: 'BTC',
data: {
address: chance.string(),
balance: chance.floating({ min: 0, max: 20 }),
currentIndex: chance.integer({ min: 0, max: 20 }),
transactions: [],
},
}))
type State = {
value: any,
}
class Wrapper extends PureComponent<any, State> {
state = {
value: '',
}
handleChange = item => this.setState({ value: item })
render() {
const { render } = this.props
const { value } = this.state
return render({ onChange: this.handleChange, value })
}
}
stories.add('basic', () => (
<Wrapper
render={({ onChange, value }) => (
<SelectAccount onChange={onChange} value={value} accounts={accounts} t={k => k} />
)}
/>
))

3
src/components/SideBar/index.js

@ -17,7 +17,8 @@ import { getAccounts } from 'reducers/accounts'
import { formatBTC } from 'helpers/format'
import { rgba } from 'styles/helpers'
import Box, { GrowScroll } from 'components/base/Box'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import Item from './Item'
const CapsSubtitle = styled(Box).attrs({

3
src/components/Wrapper.js

@ -5,7 +5,8 @@ import { Route } from 'react-router'
import { translate } from 'react-i18next'
import * as modals from 'components/modals'
import Box, { GrowScroll } from 'components/base/Box'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import AccountPage from 'components/AccountPage'
import DashboardPage from 'components/DashboardPage'

91
src/components/base/GrowScroll/index.js

@ -0,0 +1,91 @@
// @flow
import React, { PureComponent } from 'react'
import Scrollbar from 'react-smooth-scrollbar'
import noop from 'lodash/noop'
import Box from 'components/base/Box'
type Props = {
maxHeight?: number | string,
children: any,
offsetLimit: Object,
onUpdate: Function,
}
class GrowScroll extends PureComponent<Props> {
static defaultProps = {
onUpdate: noop,
offsetLimit: {
y: {
max: -3,
min: 3,
},
},
}
componentDidMount() {
const { offsetLimit } = this.props
if (this._scrollbar) {
this._scrollbar.addListener(function onScroll({ limit, offset }) {
if (limit.y > 0) {
const maxY = limit.y + offsetLimit.y.max
const minY = offsetLimit.y.min
if (offset.y > maxY) {
this.scrollTo(offset.x, maxY)
}
if (offset.y < minY) {
this.scrollTo(offset.x, minY)
}
}
})
}
this.handleUpdate(this.props)
}
componentWillReceiveProps(nextProps: Props) {
this.handleUpdate(nextProps)
}
handleUpdate = (props: Props) => {
if (this._scrollbar) {
props.onUpdate(this._scrollbar)
}
}
_scrollbar = undefined
render() {
const { onUpdate, children, maxHeight, ...props } = this.props
return (
<Box grow relative>
<Scrollbar
damping={1}
style={{
...(maxHeight
? {
maxHeight,
}
: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
}),
}}
ref={r => r && (this._scrollbar = r.scrollbar)}
>
<Box {...props}>{children}</Box>
</Scrollbar>
</Box>
)
}
}
export default GrowScroll

13
src/components/base/GrowScroll/stories.js

@ -0,0 +1,13 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
const stories = storiesOf('GrowScroll', module)
stories.add('basic', () => (
<Box style={{ height: 400, border: '1px solid black' }}>
<GrowScroll>{[...Array(1000).keys()].map(v => <div key={v}>{v}</div>)}</GrowScroll>
</Box>
))

1
src/components/base/Icon/index.js

@ -8,6 +8,7 @@ import FontAwesomeIcon from '@fortawesome/react-fontawesome'
const Container = styled.span`
${fontSize};
${color};
position: relative;
`
export default ({ name, ...props }: { name: string }) => (

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

@ -85,6 +85,7 @@ const Wrapper = styled(Box).attrs({
const Body = styled(Box).attrs({
bg: p => p.theme.colors.white,
p: 3,
relative: true,
})`
border-radius: 5px;
`

160
src/components/base/Select/index.js

@ -4,13 +4,16 @@ import React, { PureComponent } from 'react'
import Downshift from 'downshift'
import styled from 'styled-components'
import { space } from 'styled-system'
import get from 'lodash/get'
import type { Element } from 'react'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import GrowScroll from 'components/base/GrowScroll'
import Icon from 'components/base/Icon'
import Input from 'components/base/Input'
import Search from 'components/base/Search'
import Text from 'components/base/Text'
import Triangles from './Triangles'
@ -49,6 +52,7 @@ const TriggerBtn = styled(Box).attrs({
`
const Item = styled(Box).attrs({
align: 'center',
p: 2,
})`
background: ${p => (p.highlighted ? p.theme.colors.cream : p.theme.colors.white)};
@ -68,8 +72,6 @@ const Dropdown = styled(Box).attrs({
left: 0;
right: 0;
border: 1px solid ${p => p.theme.colors.mouse};
max-height: 300px;
overflow-y: auto;
border-radius: 3px;
box-shadow: rgba(0, 0, 0, 0.05) 0 2px 2px;
`
@ -89,26 +91,83 @@ const FloatingTriangles = styled(Box).attrs({
padding-right: 1px;
`
const IconSelected = styled(Box).attrs({
bg: 'blue',
color: 'white',
align: 'center',
justify: 'center',
})`
border-radius: 50%;
height: 15px;
font-size: 5px;
width: 15px;
opacity: ${p => (p.selected ? 1 : 0)};
// add top for center icon
> * {
top: 1px;
}
`
class Select extends PureComponent<Props> {
static defaultProps = {
itemToString: (item: Object) => item && item.name,
keyProp: undefined,
}
renderItems = (items: Array<Object>, downshiftProps: Object) => {
_scrollToSelectedItem = true
_useKeyboard = false
renderItems = (items: Array<Object>, selectedItem: any, downshiftProps: Object) => {
const { renderItem, keyProp } = this.props
const { getItemProps, highlightedIndex } = downshiftProps
const selectedItemIndex = items.indexOf(selectedItem)
return (
<Dropdown>
{items.length ? (
items.map((item, i) => (
<ItemWrapper key={keyProp ? item[keyProp] : item.key} {...getItemProps({ item })}>
<Item highlighted={i === highlightedIndex}>
{renderItem ? renderItem(item) : <span>{item.name_highlight || item.name}</span>}
</Item>
</ItemWrapper>
))
<GrowScroll
maxHeight={300}
onUpdate={scrollbar => {
const { contentEl } = scrollbar
const children = get(contentEl, 'children[0].children[0].children', {})
const currentHighlighted = children[highlightedIndex]
const currentSelectedItem = children[selectedItemIndex]
if (this._useKeyboard && currentHighlighted) {
scrollbar.scrollIntoView(currentHighlighted, {
alignToTop: false,
})
} else if (this._scrollToSelectedItem && currentSelectedItem) {
scrollbar.scrollIntoView(currentSelectedItem, {
alignToTop: false,
})
this._scrollToSelectedItem = false
}
}}
>
{items.map((item, i) => (
<ItemWrapper key={keyProp ? item[keyProp] : item.key} {...getItemProps({ item })}>
<Item highlighted={i === highlightedIndex} horizontal flow={10}>
<Box grow>
{renderItem ? (
renderItem(item)
) : (
<span>{item.name_highlight || item.name}</span>
)}
</Box>
<Box>
<IconSelected selected={selectedItem === item}>
<Icon name="check" />
</IconSelected>
</Box>
</Item>
</ItemWrapper>
))}
</GrowScroll>
) : (
<ItemWrapper>
<Item>{'No results'}</Item>
@ -147,42 +206,53 @@ class Select extends PureComponent<Props> {
openMenu,
selectedItem,
...downshiftProps
}) => (
<Container {...getRootProps({ refKey: 'innerRef' })} {...props}>
{searchable ? (
<Box relative>
<Input keepEvent {...getInputProps({ placeholder })} onClick={openMenu} />
<FloatingTriangles>
<Triangles />
</FloatingTriangles>
</Box>
) : (
<TriggerBtn {...getButtonProps()} tabIndex={0} horizontal align="center" flow={2}>
<Box grow>
{selectedItem && renderSelected ? (
renderSelected(selectedItem)
) : (
<Text color="mouse">{placeholder}</Text>
)}
}) => {
if (!isOpen) {
this._scrollToSelectedItem = true
}
return (
<Container
{...getRootProps({ refKey: 'innerRef' })}
{...props}
onKeyDown={() => (this._useKeyboard = true)}
onKeyUp={() => (this._useKeyboard = false)}
>
{searchable ? (
<Box relative>
<Input keepEvent {...getInputProps({ placeholder })} onClick={openMenu} />
<FloatingTriangles>
<Triangles />
</FloatingTriangles>
</Box>
<Triangles />
</TriggerBtn>
)}
{isOpen &&
(searchable ? (
<Search
value={inputValue}
items={items}
fuseOptions={fuseOptions}
highlight={highlight}
renderHighlight={renderHighlight}
render={items => this.renderItems(items, downshiftProps)}
/>
) : (
this.renderItems(items, downshiftProps)
))}
</Container>
)}
<TriggerBtn {...getButtonProps()} tabIndex={0} horizontal align="center" flow={2}>
<Box grow>
{selectedItem && renderSelected ? (
renderSelected(selectedItem)
) : (
<Text color="mouse">{placeholder}</Text>
)}
</Box>
<Triangles />
</TriggerBtn>
)}
{isOpen &&
(searchable ? (
<Search
value={inputValue}
items={items}
fuseOptions={fuseOptions}
highlight={highlight}
renderHighlight={renderHighlight}
render={items => this.renderItems(items, selectedItem, downshiftProps)}
/>
) : (
this.renderItems(items, selectedItem, downshiftProps)
))}
</Container>
)
}}
/>
)
}

25
src/styles/global.js

@ -44,18 +44,23 @@ injectGlobal`
em {
font-style: italic;
}
::-webkit-scrollbar {
background-color: rgba(0, 0, 0, 0);
width: 6px;
.scrollbar-thumb-y {
width: 5px !important;
}
.scrollbar-thumb-x {
height: 5px !important;
}
::-webkit-scrollbar:hover {
background-color: rgba(0, 0, 0, 0.09);
.scrollbar-track {
background: transparent !important;
transition: opacity 0.2s ease-in-out !important;
}
::-webkit-scrollbar-thumb:vertical {
background: rgba(0, 0, 0, 0.5);
.scrollbar-track-y {
right: 2px !important;
width: 5px !important;
}
::-webkit-scrollbar-thumb:vertical:active {
background: rgba(0, 0, 0, 0.61);
.scrollbar-track-x {
bottom: 2px !important;
height: 5px !important;
}
`

3
static/i18n/en/translation.yml

@ -40,3 +40,6 @@ settings:
display:
language: Language
SelectAccount:
placeholder: Select a account

3
static/i18n/fr/translation.yml

@ -40,3 +40,6 @@ settings:
display:
language: Langage
SelectAccount:
placeholder: Sélectionner un compte

32
yarn.lock

@ -2219,6 +2219,10 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
chance@^1.0.13:
version "1.0.13"
resolved "https://registry.yarnpkg.com/chance/-/chance-1.0.13.tgz#666bec2db42b3084456a3e4f4c28a82db5ccb7e6"
chardet@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
@ -2602,7 +2606,7 @@ core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0, core-js@^2.5.3:
core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
@ -3138,6 +3142,10 @@ dotenv@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
dotenv@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.0.tgz#0206eb5b336639bf377618a2a304ff00c6a1fddb"
downshift@^1.26.1:
version "1.26.1"
resolved "https://registry.yarnpkg.com/downshift/-/downshift-1.26.1.tgz#ae45a016f211d02f8000584d0b466142fde2dd6b"
@ -7435,6 +7443,10 @@ react-router@^4.2.0:
prop-types "^15.5.4"
warning "^3.0.0"
react-smooth-scrollbar@^8.0.6:
version "8.0.6"
resolved "https://registry.yarnpkg.com/react-smooth-scrollbar/-/react-smooth-scrollbar-8.0.6.tgz#179072e6a547b3af589ea303c50fd86366275edc"
react-split-pane@^0.1.74:
version "0.1.74"
resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.74.tgz#cf79fc98b51ab0763fdc778749b810a102b036ca"
@ -8176,6 +8188,14 @@ slice-ansi@1.0.0:
dependencies:
readable-stream "~1.0.31"
smooth-scrollbar@^8.2.5:
version "8.2.5"
resolved "https://registry.yarnpkg.com/smooth-scrollbar/-/smooth-scrollbar-8.2.5.tgz#676a2595b1aad97fe0835d2425e403b0d9c70eb3"
dependencies:
core-js "^2.5.1"
lodash-es "^4.17.4"
tslib "^1.7.1"
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -8535,9 +8555,9 @@ style-loader@^0.19.0, style-loader@^0.19.1:
loader-utils "^1.0.2"
schema-utils "^0.3.0"
styled-components@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.1.2.tgz#0769655335eb6800dc5f6691425f6f7fe1801e32"
styled-components@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.1.4.tgz#1bdc1409c9bacafee3510c573d23b73039b0d875"
dependencies:
buffer "^5.0.3"
css-to-react-native "^2.0.3"
@ -8818,6 +8838,10 @@ truncate-utf8-bytes@^1.0.0:
dependencies:
utf8-byte-length "^1.0.1"
tslib@^1.7.1:
version "1.9.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8"
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"

Loading…
Cancel
Save