Browse Source

Select component

master
meriadec 7 years ago
parent
commit
5dbd35083a
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 1
      .eslintrc
  2. 1
      package.json
  3. 20
      src/components/base/Input/index.js
  4. 33
      src/components/base/Search/index.js
  5. 11
      src/components/base/Search/stories.js
  6. 107
      src/components/base/Select/index.js
  7. 35
      src/components/base/Select/stories.js
  8. 4
      yarn.lock

1
.eslintrc

@ -23,6 +23,7 @@
"react/jsx-curly-brace-presence": 0,
"react/jsx-filename-extension": 0,
"react/prefer-stateless-function": 0,
"react/forbid-prop-types": 0,
},
"settings": {
"import/resolver": {

1
package.json

@ -37,6 +37,7 @@
"@ledgerhq/hw-transport": "^1.1.2-beta.068e2a14",
"@ledgerhq/hw-transport-node-hid": "^1.1.2-beta.068e2a14",
"color": "^2.0.1",
"downshift": "^1.25.0",
"electron-store": "^1.3.0",
"electron-updater": "^2.18.2",
"fuse.js": "^3.2.0",

20
src/components/base/Input/index.js

@ -3,9 +3,12 @@
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { space } from 'styled-system'
const Base = styled.input`
padding: 10px 15px;
const Base = styled.input.attrs({
p: 2,
})`
${space};
border: 1px solid ${p => p.theme.colors.mouse};
border-radius: 3px;
display: flex;
@ -24,17 +27,22 @@ const Base = styled.input`
`
type Props = {
onChange: Function,
onChange?: Function,
keepEvent?: boolean,
}
export default class Input extends PureComponent<Props> {
class Input extends PureComponent<Props> {
handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
const { onChange } = this.props
const { onChange, keepEvent } = this.props
onChange(e.target.value)
if (onChange) {
onChange(keepEvent ? e : e.target.value)
}
}
render() {
return <Base {...this.props} onChange={this.handleChange} />
}
}
export default Input

33
src/components/base/Search/index.js

@ -4,26 +4,31 @@ import React, { PureComponent, Fragment, createElement } from 'react'
import Fuse from 'fuse.js'
import type { Element } from 'react'
import type FuseType from 'fuse.js'
// eslint false positive detection on unused prop-type
type Props = {
items: Array<Object>, // eslint-disable-line react/no-unused-prop-types
value: String,
value: string,
render: Function,
highlight?: boolean,
renderHighlight?: string => Element, // eslint-disable-line react/no-unused-prop-types
renderHighlight?: (string, string) => Element<*>, // eslint-disable-line react/no-unused-prop-types
fuseOptions?: Object, // eslint-disable-line react/no-unused-prop-types
// if true, it will display no items when value is empty
filterEmpty?: boolean,
}
class Search extends PureComponent<Props> {
type State = {
results: Array<Object>,
}
class Search extends PureComponent<Props, State> {
static defaultProps = {
fuseOptions: {},
highlight: false,
filterEmpty: false,
renderHighlight: chunk => <b>{chunk}</b>,
renderHighlight: (chunk: string): * => <b>{chunk}</b>,
}
state = {
@ -34,9 +39,11 @@ class Search extends PureComponent<Props> {
this.initFuse(this.props)
}
componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps: Props) {
if (nextProps.value !== this.props.value) {
this.formatResults(this._fuse.search(nextProps.value), nextProps)
if (this._fuse) {
this.formatResults(this._fuse.search(nextProps.value), nextProps)
}
}
if (nextProps.highlight !== this.props.highlight) {
this.initFuse(nextProps)
@ -46,7 +53,9 @@ class Search extends PureComponent<Props> {
}
}
initFuse(props) {
_fuse: FuseType<*> | null = null
initFuse(props: Props) {
const { fuseOptions, highlight, items, value } = props
this._fuse = new Fuse(items, {
@ -57,7 +66,7 @@ class Search extends PureComponent<Props> {
this.formatResults(this._fuse.search(value), props)
}
formatResults(results, props) {
formatResults(results: Array<Object>, props: Props) {
const { highlight, renderHighlight } = props
if (highlight) {
results = results.map(res => {
@ -78,7 +87,9 @@ class Search extends PureComponent<Props> {
}
const v = value.substring(start, end + 1)
res.push(renderHighlight(v, `${key}-${idx.join(',')}`))
if (v && renderHighlight) {
res.push(renderHighlight(v, `${key}-${idx.join(',')}`))
}
i = end + 1
})
@ -88,14 +99,14 @@ class Search extends PureComponent<Props> {
res.push(suffix)
}
const element = createElement(Fragment, {
const fragment = createElement(Fragment, {
key: item[key],
children: res,
})
item = {
...item,
[key]: element,
[`${key}_highlight`]: fragment,
}
})
return item

11
src/components/base/Search/stories.js

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import { storiesOf } from '@storybook/react'
import { text, boolean } from '@storybook/addon-knobs'
@ -20,13 +21,17 @@ const items = [
{ key: 'vladimir-kramnik', name: 'Vladimir Kramnik' },
]
const Wrapper = ({ children }: { children: any }) => (
const Wrapper = ({ children }) => (
<div>
<div style={{ opacity: 0.2 }}>{'(Change the search value in knobs)'}</div>
{children}
</div>
)
Wrapper.propTypes = {
children: PropTypes.any.isRequired,
}
stories.add('basic', () => {
const value = text('value', '')
const filterEmpty = boolean('filterEmpty', false)
@ -63,7 +68,9 @@ stories.add('highlight matches', () => {
{text}
</b>
)}
render={items => items.map(item => <div key={item.key}>{item.name}</div>)}
render={items =>
items.map(item => <div key={item.key}>{item.name_highlight || item.name}</div>)
}
/>
</Wrapper>
)

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

@ -0,0 +1,107 @@
// @flow
import React, { PureComponent } from 'react'
import Downshift from 'downshift'
import styled from 'styled-components'
import type { Element } from 'react'
import Box from 'components/base/Box'
import Input from 'components/base/Input'
import Search from 'components/base/Search'
type Props = {
items: Array<Object>,
itemToString: Function,
onChange: Function,
fuseOptions?: Object,
highlight?: boolean,
renderHighlight?: string => Element<*>,
}
const Container = styled(Box).attrs({ relative: true, color: 'steel' })``
const SearchInput = styled(Input)`
border-bottom-left-radius: ${p => (p.isOpen ? 0 : '')};
border-bottom-right-radius: ${p => (p.isOpen ? 0 : '')};
`
const Item = styled(Box).attrs({
p: 2,
})`
background: ${p => (p.highlighted ? p.theme.colors.cream : p.theme.colors.white)};
`
const ItemWrapper = styled(Box)`
& + & {
border-top: 1px solid ${p => p.theme.colors.mouse};
}
`
const Dropdown = styled(Box)`
position: absolute;
top: 100%;
left: 0;
right: 0;
border: 1px solid ${p => p.theme.colors.mouse};
border-top: none;
max-height: 300px;
overflow-y: auto;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
box-shadow: rgba(0, 0, 0, 0.05) 0 2px 2px;
`
class Select extends PureComponent<Props> {
render() {
const { items, itemToString, fuseOptions, highlight, renderHighlight, onChange } = this.props
return (
<Downshift
itemToString={itemToString}
onChange={onChange}
render={({
getInputProps,
getItemProps,
getRootProps,
isOpen,
inputValue,
highlightedIndex,
openMenu,
}) => (
<Container {...getRootProps({ refKey: 'innerRef' })}>
<SearchInput
keepEvent
{...getInputProps({ placeholder: 'Chess?' })}
isOpen={isOpen}
onClick={openMenu}
/>
{isOpen && (
<Search
value={inputValue}
items={items}
fuseOptions={fuseOptions}
highlight={highlight}
renderHighlight={renderHighlight}
render={items =>
items.length ? (
<Dropdown>
{items.map((item, i) => (
<ItemWrapper key={item.key} {...getItemProps({ item })}>
<Item highlighted={i === highlightedIndex}>
<span>{item.name_highlight || item.name}</span>
</Item>
</ItemWrapper>
))}
</Dropdown>
) : null
}
/>
)}
</Container>
)}
/>
)
}
}
export default Select

35
src/components/base/Select/stories.js

@ -0,0 +1,35 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import Select from 'components/base/Select'
import Text from 'components/base/Text'
const stories = storiesOf('Select', module)
const items = [
{ key: 'aleksandr-grichtchouk', name: 'Aleksandr Grichtchouk' },
{ key: 'fabiano-caruana', name: 'Fabiano Caruana' },
{ key: 'garry-kasparov', name: 'Garry Kasparov' },
{ key: 'hikaru-nakamura', name: 'Hikaru Nakamura' },
{ key: 'levon-aronian', name: 'Levon Aronian' },
{ key: 'magnus-carlsen', name: 'Magnus Carlsen' },
{ key: 'maxime-vachier-lagrave', name: 'Maxime Vachier-Lagrave' },
{ key: 'shakhriyar-mamedyarov', name: 'Shakhriyar Mamedyarov' },
{ key: 'veselin-topalov', name: 'Veselin Topalov' },
{ key: 'viswanathan-anand', name: 'Viswanathan Anand' },
{ key: 'vladimir-kramnik', name: 'Vladimir Kramnik' },
]
stories.add('basic', () => (
<Select
items={items}
highlight
fuseOptions={{ keys: ['name'] }}
itemToString={item => (item ? item.name : '')}
renderHighlight={(text, key) => (
<Text key={key} fontWeight="bold">
{text}
</Text>
)}
/>
))

4
yarn.lock

@ -3011,6 +3011,10 @@ dotenv@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
downshift@^1.25.0:
version "1.25.0"
resolved "https://registry.yarnpkg.com/downshift/-/downshift-1.25.0.tgz#7f6e2dda4aa5ddbb2932401bd61e7b741e92c02e"
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"

Loading…
Cancel
Save