diff --git a/package.json b/package.json index f27c85f6..ff1b4d03 100644 --- a/package.json +++ b/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", diff --git a/src/components/SelectAccount.js b/src/components/SelectAccount.js deleted file mode 100644 index 08957cb5..00000000 --- a/src/components/SelectAccount.js +++ /dev/null @@ -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, - onChange: () => Account | void, - value: Account | null, -} - -const SelectAccount = ({ accounts, value, onChange }: Props) => ( - 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) diff --git a/src/components/SelectAccount/stories.js b/src/components/SelectAccount/stories.js new file mode 100644 index 00000000..fe379933 --- /dev/null +++ b/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 { + 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', () => ( + ( + k} /> + )} + /> +)) diff --git a/src/components/SideBar/index.js b/src/components/SideBar/index.js index a93fbc62..da5d8922 100644 --- a/src/components/SideBar/index.js +++ b/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({ diff --git a/src/components/Wrapper.js b/src/components/Wrapper.js index 0054b869..efd6bf01 100644 --- a/src/components/Wrapper.js +++ b/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' diff --git a/src/components/base/GrowScroll/index.js b/src/components/base/GrowScroll/index.js new file mode 100644 index 00000000..6992eb3e --- /dev/null +++ b/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 { + 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 ( + + r && (this._scrollbar = r.scrollbar)} + > + {children} + + + ) + } +} + +export default GrowScroll diff --git a/src/components/base/GrowScroll/stories.js b/src/components/base/GrowScroll/stories.js new file mode 100644 index 00000000..d4d9dad2 --- /dev/null +++ b/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', () => ( + + {[...Array(1000).keys()].map(v =>
{v}
)}
+
+)) diff --git a/src/components/base/Icon/index.js b/src/components/base/Icon/index.js index 3735c4ce..2ef9d956 100644 --- a/src/components/base/Icon/index.js +++ b/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 }) => ( diff --git a/src/components/base/Modal/index.js b/src/components/base/Modal/index.js index 02c5ff32..06b31f4c 100644 --- a/src/components/base/Modal/index.js +++ b/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; ` diff --git a/src/components/base/Select/index.js b/src/components/base/Select/index.js index 50bf06a6..1942db1b 100644 --- a/src/components/base/Select/index.js +++ b/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 { static defaultProps = { itemToString: (item: Object) => item && item.name, keyProp: undefined, } - renderItems = (items: Array, downshiftProps: Object) => { + _scrollToSelectedItem = true + _useKeyboard = false + + renderItems = (items: Array, selectedItem: any, downshiftProps: Object) => { const { renderItem, keyProp } = this.props const { getItemProps, highlightedIndex } = downshiftProps + const selectedItemIndex = items.indexOf(selectedItem) + return ( {items.length ? ( - items.map((item, i) => ( - - - {renderItem ? renderItem(item) : {item.name_highlight || item.name}} - - - )) + { + 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) => ( + + + + {renderItem ? ( + renderItem(item) + ) : ( + {item.name_highlight || item.name} + )} + + + + + + + + + ))} + ) : ( {'No results'} @@ -147,42 +206,53 @@ class Select extends PureComponent { openMenu, selectedItem, ...downshiftProps - }) => ( - - {searchable ? ( - - - - - - - ) : ( - - - {selectedItem && renderSelected ? ( - renderSelected(selectedItem) - ) : ( - {placeholder} - )} + }) => { + if (!isOpen) { + this._scrollToSelectedItem = true + } + + return ( + (this._useKeyboard = true)} + onKeyUp={() => (this._useKeyboard = false)} + > + {searchable ? ( + + + + + - - - )} - {isOpen && - (searchable ? ( - this.renderItems(items, downshiftProps)} - /> ) : ( - this.renderItems(items, downshiftProps) - ))} - - )} + + + {selectedItem && renderSelected ? ( + renderSelected(selectedItem) + ) : ( + {placeholder} + )} + + + + )} + {isOpen && + (searchable ? ( + this.renderItems(items, selectedItem, downshiftProps)} + /> + ) : ( + this.renderItems(items, selectedItem, downshiftProps) + ))} + + ) + }} /> ) } diff --git a/src/styles/global.js b/src/styles/global.js index 1144a3b4..929a570b 100644 --- a/src/styles/global.js +++ b/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; } ` diff --git a/static/i18n/en/translation.yml b/static/i18n/en/translation.yml index 6beace79..8d8eda6e 100644 --- a/static/i18n/en/translation.yml +++ b/static/i18n/en/translation.yml @@ -40,3 +40,6 @@ settings: display: language: Language + +SelectAccount: + placeholder: Select a account diff --git a/static/i18n/fr/translation.yml b/static/i18n/fr/translation.yml index b46f4439..5a4039ff 100644 --- a/static/i18n/fr/translation.yml +++ b/static/i18n/fr/translation.yml @@ -40,3 +40,6 @@ settings: display: language: Langage + +SelectAccount: + placeholder: Sélectionner un compte diff --git a/yarn.lock b/yarn.lock index 615fc4a2..8bdda5fd 100644 --- a/yarn.lock +++ b/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"