Browse Source

Merge pull request #30 from meriadec/master

Search & Select components
master
Loëck Vézien 7 years ago
committed by GitHub
parent
commit
e0ffdbb838
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .eslintrc
  2. 2
      package.json
  3. 20
      src/components/base/Input/index.js
  4. 128
      src/components/base/Search/index.js
  5. 77
      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

2
.eslintrc

@ -18,10 +18,12 @@
"no-shadow": 0, "no-shadow": 0,
"no-underscore-dangle": 0, "no-underscore-dangle": 0,
"no-void": 0, "no-void": 0,
"no-plusplus": 0,
"import/no-extraneous-dependencies": 0, "import/no-extraneous-dependencies": 0,
"react/jsx-curly-brace-presence": 0, "react/jsx-curly-brace-presence": 0,
"react/jsx-filename-extension": 0, "react/jsx-filename-extension": 0,
"react/prefer-stateless-function": 0, "react/prefer-stateless-function": 0,
"react/forbid-prop-types": 0,
}, },
"settings": { "settings": {
"import/resolver": { "import/resolver": {

2
package.json

@ -37,8 +37,10 @@
"@ledgerhq/hw-transport": "^1.1.2-beta.068e2a14", "@ledgerhq/hw-transport": "^1.1.2-beta.068e2a14",
"@ledgerhq/hw-transport-node-hid": "^1.1.2-beta.068e2a14", "@ledgerhq/hw-transport-node-hid": "^1.1.2-beta.068e2a14",
"color": "^2.0.1", "color": "^2.0.1",
"downshift": "^1.25.0",
"electron-store": "^1.3.0", "electron-store": "^1.3.0",
"electron-updater": "^2.18.2", "electron-updater": "^2.18.2",
"fuse.js": "^3.2.0",
"history": "^4.7.2", "history": "^4.7.2",
"i18next": "^10.2.2", "i18next": "^10.2.2",
"i18next-node-fs-backend": "^1.0.0", "i18next-node-fs-backend": "^1.0.0",

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

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

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

@ -0,0 +1,128 @@
// @flow
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,
render: Function,
highlight?: boolean,
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,
}
type State = {
results: Array<Object>,
}
class Search extends PureComponent<Props, State> {
static defaultProps = {
fuseOptions: {},
highlight: false,
filterEmpty: false,
renderHighlight: (chunk: string): * => <b>{chunk}</b>,
}
state = {
results: [],
}
componentWillMount() {
this.initFuse(this.props)
}
componentWillReceiveProps(nextProps: Props) {
if (nextProps.value !== this.props.value) {
if (this._fuse) {
this.formatResults(this._fuse.search(nextProps.value), nextProps)
}
}
if (nextProps.highlight !== this.props.highlight) {
this.initFuse(nextProps)
}
if (nextProps.items !== this.props.items) {
this.initFuse(nextProps)
}
}
_fuse: FuseType<*> | null = null
initFuse(props: Props) {
const { fuseOptions, highlight, items, value } = props
this._fuse = new Fuse(items, {
...fuseOptions,
includeMatches: highlight,
})
this.formatResults(this._fuse.search(value), props)
}
formatResults(results: Array<Object>, props: Props) {
const { highlight, renderHighlight } = props
if (highlight) {
results = results.map(res => {
let { item } = res
const { matches } = res
matches.forEach(match => {
const { key, value, indices } = match
let i = 0
const res = []
indices.forEach(idx => {
const [start, end] = idx
const prefix = value.substring(i, start)
if (prefix.length > 0) {
res.push(prefix)
}
const v = value.substring(start, end + 1)
if (v && renderHighlight) {
res.push(renderHighlight(v, `${key}-${idx.join(',')}`))
}
i = end + 1
})
const suffix = value.substring(indices[indices.length - 1][1] + 1)
if (suffix.length > 0) {
res.push(suffix)
}
const fragment = createElement(Fragment, {
key: item[key],
children: res,
})
item = {
...item,
[`${key}_highlight`]: fragment,
}
})
return item
})
}
this.setState({ results })
}
render() {
const { render, value, items, filterEmpty } = this.props
const { results } = this.state
if (!filterEmpty && value === '') {
return render(items)
}
return render(results)
}
}
export default Search

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

@ -0,0 +1,77 @@
import React from 'react'
import PropTypes from 'prop-types'
import { storiesOf } from '@storybook/react'
import { text, boolean } from '@storybook/addon-knobs'
import Search from 'components/base/Search'
const stories = storiesOf('Search', 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' },
]
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)
return (
<Wrapper>
<Search
value={value}
items={items}
filterEmpty={filterEmpty}
fuseOptions={{
keys: ['name'],
}}
render={items => items.map(item => <div key={item.name}>{item.name}</div>)}
/>
</Wrapper>
)
})
stories.add('highlight matches', () => {
const value = text('value', '')
const filterEmpty = boolean('filterEmpty', false)
return (
<Wrapper>
<Search
value={value}
items={items}
filterEmpty={filterEmpty}
highlight
fuseOptions={{
keys: ['name'],
}}
renderHighlight={(text, key) => (
<b key={key} style={{ textDecoration: 'underline', color: 'red' }}>
{text}
</b>
)}
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" version "4.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" 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: duplexer3@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"

Loading…
Cancel
Save