Gaëtan Renaudeau
7 years ago
committed by
GitHub
19 changed files with 971 additions and 476 deletions
@ -0,0 +1,343 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import Downshift from 'downshift' |
||||
|
import styled from 'styled-components' |
||||
|
import { space } from 'styled-system' |
||||
|
|
||||
|
import Box from 'components/base/Box' |
||||
|
import GrowScroll from 'components/base/GrowScroll' |
||||
|
import Input from 'components/base/Input' |
||||
|
import Search from 'components/base/Search' |
||||
|
import Text from 'components/base/Text' |
||||
|
|
||||
|
import IconCheck from 'icons/Check' |
||||
|
|
||||
|
type Props = { |
||||
|
bg?: string, |
||||
|
flatLeft?: boolean, |
||||
|
flatRight?: boolean, |
||||
|
fakeFocusRight?: boolean, |
||||
|
fuseOptions?: Object, |
||||
|
highlight?: boolean, |
||||
|
items: Array<any>, |
||||
|
itemToString?: Function, |
||||
|
keyProp?: string, |
||||
|
maxHeight?: number, |
||||
|
onChange?: Function, |
||||
|
placeholder?: string, |
||||
|
renderHighlight?: string => React$Node, |
||||
|
renderItem?: (*) => React$Node, |
||||
|
renderSelected?: any => React$Node, |
||||
|
searchable?: boolean, |
||||
|
value?: *, |
||||
|
disabled: boolean, |
||||
|
small?: boolean, |
||||
|
} |
||||
|
|
||||
|
const Container = styled(Box).attrs({ relative: true, color: 'graphite' })`` |
||||
|
|
||||
|
const TriggerBtn = styled(Box).attrs({ |
||||
|
alignItems: 'center', |
||||
|
ff: p => (p.small ? 'Open Sans' : 'Open Sans|SemiBold'), |
||||
|
flow: 2, |
||||
|
fontSize: p => (p.small ? 3 : 4), |
||||
|
horizontal: true, |
||||
|
px: 3, |
||||
|
})` |
||||
|
${space}; |
||||
|
height: ${p => (p.small ? '34' : '40')}px; |
||||
|
background: ${p => (p.disabled ? p.theme.colors.lightGrey : p.bg || p.theme.colors.white)}; |
||||
|
border-bottom-left-radius: ${p => (p.flatLeft ? 0 : p.theme.radii[1])}px; |
||||
|
border-bottom-right-radius: ${p => (p.flatRight ? 0 : p.theme.radii[1])}px; |
||||
|
border-top-left-radius: ${p => (p.flatLeft ? 0 : p.theme.radii[1])}px; |
||||
|
border-top-right-radius: ${p => (p.flatRight ? 0 : p.theme.radii[1])}px; |
||||
|
border: 1px solid ${p => p.theme.colors.fog}; |
||||
|
color: ${p => p.theme.colors.graphite}; |
||||
|
cursor: ${p => (p.disabled ? 'cursor' : 'pointer')}; |
||||
|
display: flex; |
||||
|
width: 100%; |
||||
|
|
||||
|
&:focus { |
||||
|
outline: none; |
||||
|
${p => |
||||
|
p.disabled |
||||
|
? '' |
||||
|
: ` |
||||
|
border-color: ${p.theme.colors.wallet}; |
||||
|
box-shadow: rgba(0, 0, 0, 0.05) 0 2px 2px;`};
|
||||
|
} |
||||
|
|
||||
|
${p => { |
||||
|
const c = p.theme.colors.wallet |
||||
|
return p.fakeFocusRight |
||||
|
? ` |
||||
|
border-top: 1px solid ${c}; |
||||
|
border-right: 1px solid ${c}; |
||||
|
border-bottom: 1px solid ${c}; |
||||
|
` |
||||
|
: '' |
||||
|
}}; |
||||
|
` |
||||
|
|
||||
|
const Item = styled(Box).attrs({ |
||||
|
alignItems: 'center', |
||||
|
fontSize: 4, |
||||
|
ff: p => `Open Sans|${p.selected ? 'SemiBold' : 'Regular'}`, |
||||
|
px: 3, |
||||
|
py: 2, |
||||
|
color: 'dark', |
||||
|
})` |
||||
|
background: ${p => (p.highlighted ? p.theme.colors.lightGrey : p.theme.colors.white)}; |
||||
|
|
||||
|
${p => |
||||
|
p.first && |
||||
|
` |
||||
|
border-top-left-radius: ${p.theme.radii[1]}px; |
||||
|
border-top-right-radius: ${p.theme.radii[1]}px; |
||||
|
`} ${p => |
||||
|
p.last && |
||||
|
` |
||||
|
border-bottom-left-radius: ${p.theme.radii[1]}px; |
||||
|
border-bottom-right-radius: ${p.theme.radii[1]}px; |
||||
|
`};
|
||||
|
` |
||||
|
|
||||
|
const Dropdown = styled(Box).attrs({ |
||||
|
mt: 1, |
||||
|
})` |
||||
|
border-radius: ${p => p.theme.radii[1]}px; |
||||
|
border: 1px solid ${p => p.theme.colors.fog}; |
||||
|
box-shadow: rgba(0, 0, 0, 0.05) 0 2px 2px; |
||||
|
left: 0; |
||||
|
position: absolute; |
||||
|
right: 0; |
||||
|
top: 100%; |
||||
|
z-index: 1; |
||||
|
` |
||||
|
|
||||
|
const IconSelected = styled(Box).attrs({ |
||||
|
color: 'wallet', |
||||
|
alignItems: 'center', |
||||
|
justifyContent: 'center', |
||||
|
})` |
||||
|
height: 12px; |
||||
|
width: 12px; |
||||
|
opacity: ${p => (p.selected ? 1 : 0)}; |
||||
|
` |
||||
|
|
||||
|
const AngleDown = props => ( |
||||
|
<Box color="grey" alignItems="center" justifyContent="center" {...props}> |
||||
|
<svg viewBox="0 0 16 16" width="16" height="16"> |
||||
|
<path |
||||
|
fill="currentColor" |
||||
|
d="M7.70785815 10.86875l-5.08670521-4.5875c-.16153725-.146875-.16153725-.384375 0-.53125l.68051867-.61875c.16153726-.146875.42274645-.146875.58428371 0L8 8.834375l4.1140447-3.703125c.1615372-.146875.4227464-.146875.5842837 0l.6805187.61875c.1615372.146875.1615372.384375 0 .53125l-5.08670525 4.5875c-.16153726.146875-.42274644.146875-.5842837 0z" |
||||
|
/> |
||||
|
</svg> |
||||
|
</Box> |
||||
|
) |
||||
|
|
||||
|
const renderSelectedItem = ({ selectedItem, renderSelected, placeholder }: any) => |
||||
|
selectedItem && renderSelected ? ( |
||||
|
renderSelected(selectedItem) |
||||
|
) : ( |
||||
|
<Text color="fog">{placeholder}</Text> |
||||
|
) |
||||
|
|
||||
|
class LegacySelect extends PureComponent<Props> { |
||||
|
static defaultProps = { |
||||
|
bg: undefined, |
||||
|
disabled: false, |
||||
|
small: false, |
||||
|
fakeFocusRight: false, |
||||
|
flatLeft: false, |
||||
|
flatRight: false, |
||||
|
itemToString: (item: Object) => item && item.name, |
||||
|
keyProp: undefined, |
||||
|
maxHeight: 300, |
||||
|
} |
||||
|
|
||||
|
_scrollToSelectedItem = true |
||||
|
_oldHighlightedIndex = 0 |
||||
|
_useKeyboard = false |
||||
|
_children = {} |
||||
|
|
||||
|
renderItems = (items: Array<Object>, selectedItem: any, downshiftProps: Object) => { |
||||
|
const { renderItem, maxHeight, keyProp } = this.props |
||||
|
const { getItemProps, highlightedIndex } = downshiftProps |
||||
|
|
||||
|
const selectedItemIndex = items.indexOf(selectedItem) |
||||
|
|
||||
|
return ( |
||||
|
<Dropdown> |
||||
|
{items.length ? ( |
||||
|
<GrowScroll |
||||
|
maxHeight={maxHeight} |
||||
|
onUpdate={scrollbar => { |
||||
|
const currentHighlighted = this._children[highlightedIndex] |
||||
|
const currentSelectedItem = this._children[selectedItemIndex] |
||||
|
|
||||
|
if (this._useKeyboard && currentHighlighted) { |
||||
|
scrollbar.scrollIntoView(currentHighlighted, { |
||||
|
alignToTop: highlightedIndex < this._oldHighlightedIndex, |
||||
|
offsetTop: -1, |
||||
|
onlyScrollIfNeeded: true, |
||||
|
}) |
||||
|
} else if (this._scrollToSelectedItem && currentSelectedItem) { |
||||
|
window.requestAnimationFrame(() => |
||||
|
scrollbar.scrollIntoView(currentSelectedItem, { |
||||
|
offsetTop: -1, |
||||
|
}), |
||||
|
) |
||||
|
|
||||
|
this._scrollToSelectedItem = false |
||||
|
} |
||||
|
|
||||
|
this._oldHighlightedIndex = highlightedIndex |
||||
|
}} |
||||
|
> |
||||
|
{items.map((item, i) => ( |
||||
|
<Box |
||||
|
key={keyProp ? item[keyProp] : item.key} |
||||
|
innerRef={n => (this._children[i] = n)} |
||||
|
{...getItemProps({ item })} |
||||
|
> |
||||
|
<Item |
||||
|
first={i === 0} |
||||
|
last={i === items.length - 1} |
||||
|
highlighted={i === highlightedIndex} |
||||
|
selected={selectedItem === item} |
||||
|
horizontal |
||||
|
flow={3} |
||||
|
> |
||||
|
<Box grow> |
||||
|
{renderItem ? ( |
||||
|
renderItem(item) |
||||
|
) : ( |
||||
|
<span>{item.name_highlight || item.name}</span> |
||||
|
)} |
||||
|
</Box> |
||||
|
<Box> |
||||
|
<IconSelected selected={selectedItem === item}> |
||||
|
<IconCheck size={12} /> |
||||
|
</IconSelected> |
||||
|
</Box> |
||||
|
</Item> |
||||
|
</Box> |
||||
|
))} |
||||
|
</GrowScroll> |
||||
|
) : ( |
||||
|
<Box> |
||||
|
<Item>{'No results'}</Item> |
||||
|
</Box> |
||||
|
)} |
||||
|
</Dropdown> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
render() { |
||||
|
const { |
||||
|
disabled, |
||||
|
fakeFocusRight, |
||||
|
flatLeft, |
||||
|
flatRight, |
||||
|
fuseOptions, |
||||
|
highlight, |
||||
|
items, |
||||
|
itemToString, |
||||
|
onChange, |
||||
|
placeholder, |
||||
|
renderHighlight, |
||||
|
renderSelected, |
||||
|
searchable, |
||||
|
value, |
||||
|
small, |
||||
|
...props |
||||
|
} = this.props |
||||
|
|
||||
|
return ( |
||||
|
<Downshift |
||||
|
selectedItem={value} |
||||
|
itemToString={itemToString} |
||||
|
onChange={onChange} |
||||
|
render={({ |
||||
|
getInputProps, |
||||
|
getToggleButtonProps, |
||||
|
getRootProps, |
||||
|
isOpen, |
||||
|
inputValue, |
||||
|
openMenu, |
||||
|
selectedItem, |
||||
|
...downshiftProps |
||||
|
}) => { |
||||
|
if (!isOpen) { |
||||
|
this._scrollToSelectedItem = true |
||||
|
} |
||||
|
|
||||
|
if (disabled) { |
||||
|
return ( |
||||
|
<Container {...getRootProps({ refKey: 'innerRef' })}> |
||||
|
<TriggerBtn disabled bg={props.bg} tabIndex={0} small={small}> |
||||
|
{renderSelectedItem({ selectedItem, renderSelected, placeholder })} |
||||
|
</TriggerBtn> |
||||
|
</Container> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Container |
||||
|
{...getRootProps({ refKey: 'innerRef' })} |
||||
|
{...props} |
||||
|
horizontal |
||||
|
onKeyDown={() => (this._useKeyboard = true)} |
||||
|
onKeyUp={() => (this._useKeyboard = false)} |
||||
|
> |
||||
|
{searchable ? ( |
||||
|
<Box grow> |
||||
|
<Input |
||||
|
small |
||||
|
keepEvent |
||||
|
onClick={openMenu} |
||||
|
renderRight={<AngleDown mr={2} />} |
||||
|
{...getInputProps({ placeholder })} |
||||
|
/> |
||||
|
</Box> |
||||
|
) : ( |
||||
|
<TriggerBtn |
||||
|
{...getToggleButtonProps()} |
||||
|
bg={props.bg} |
||||
|
fakeFocusRight={fakeFocusRight} |
||||
|
flatLeft={flatLeft} |
||||
|
flatRight={flatRight} |
||||
|
tabIndex={0} |
||||
|
small={small} |
||||
|
> |
||||
|
<Box grow> |
||||
|
{renderSelectedItem({ selectedItem, renderSelected, placeholder })} |
||||
|
</Box> |
||||
|
<AngleDown mr={-1} /> |
||||
|
</TriggerBtn> |
||||
|
)} |
||||
|
<div hidden={!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) |
||||
|
)} |
||||
|
</div> |
||||
|
</Container> |
||||
|
) |
||||
|
}} |
||||
|
/> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default LegacySelect |
@ -0,0 +1,117 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import { storiesOf } from '@storybook/react' |
||||
|
import { boolean } from '@storybook/addon-knobs' |
||||
|
|
||||
|
import Box from 'components/base/Box' |
||||
|
import LegacySelect from 'components/base/LegacySelect' |
||||
|
import Text from 'components/base/Text' |
||||
|
|
||||
|
const stories = storiesOf('Components/base/LegacySelect', module) |
||||
|
|
||||
|
const itemsChessPlayers = [ |
||||
|
{ 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' }, |
||||
|
] |
||||
|
|
||||
|
type State = { |
||||
|
item: Object | null, |
||||
|
} |
||||
|
|
||||
|
class Wrapper extends PureComponent<any, State> { |
||||
|
state = { |
||||
|
item: null, |
||||
|
} |
||||
|
|
||||
|
handleChange = item => this.setState({ item }) |
||||
|
|
||||
|
render() { |
||||
|
const { children } = this.props |
||||
|
const { item } = this.state |
||||
|
return ( |
||||
|
<div> |
||||
|
{children(this.handleChange)} |
||||
|
{item && ( |
||||
|
<Box mt={2}> |
||||
|
<pre> |
||||
|
{'You selected:'} |
||||
|
{JSON.stringify(item)} |
||||
|
</pre> |
||||
|
</Box> |
||||
|
)} |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
stories.add('basic', () => ( |
||||
|
<Wrapper> |
||||
|
{onChange => ( |
||||
|
<LegacySelect |
||||
|
disabled={boolean('disabled', false)} |
||||
|
placeholder="Choose a chess player..." |
||||
|
items={itemsChessPlayers} |
||||
|
renderSelected={item => item.name} |
||||
|
onChange={onChange} |
||||
|
/> |
||||
|
)} |
||||
|
</Wrapper> |
||||
|
)) |
||||
|
|
||||
|
stories.add('searchable', () => ( |
||||
|
<LegacySelect |
||||
|
placeholder="Choose a chess player..." |
||||
|
items={itemsChessPlayers} |
||||
|
searchable |
||||
|
highlight |
||||
|
fuseOptions={{ keys: ['name'] }} |
||||
|
itemToString={item => (item ? item.name : '')} |
||||
|
renderHighlight={(text, key) => ( |
||||
|
<Text key={key} fontWeight="bold"> |
||||
|
{text} |
||||
|
</Text> |
||||
|
)} |
||||
|
/> |
||||
|
)) |
||||
|
|
||||
|
const itemsColors = [ |
||||
|
{ key: 'absolute zero', name: 'Absolute Zero', color: '#0048BA' }, |
||||
|
{ key: 'acid green', name: 'Acid Green', color: '#B0BF1A' }, |
||||
|
{ key: 'aero', name: 'Aero', color: '#7CB9E8' }, |
||||
|
{ key: 'aero blue', name: 'Aero Blue', color: '#C9FFE5' }, |
||||
|
{ key: 'african violet', name: 'African Violet', color: '#B284BE' }, |
||||
|
{ key: 'air force blue (usaf)', name: 'Air Force Blue (USAF)', color: '#00308F' }, |
||||
|
{ key: 'air superiority blue', name: 'Air Superiority Blue', color: '#72A0C1' }, |
||||
|
] |
||||
|
|
||||
|
stories.add('custom render', () => ( |
||||
|
<LegacySelect |
||||
|
placeholder="Choose a color..." |
||||
|
items={itemsColors} |
||||
|
highlight |
||||
|
searchable |
||||
|
fuseOptions={{ keys: ['name', 'color'] }} |
||||
|
itemToString={item => (item ? item.name : '')} |
||||
|
renderHighlight={(text, key) => ( |
||||
|
<Text key={key} fontWeight="bold"> |
||||
|
{text} |
||||
|
</Text> |
||||
|
)} |
||||
|
renderItem={item => ( |
||||
|
<Box horizontal flow={2}> |
||||
|
<Box bg={item.color} style={{ width: 20, height: 20 }} /> |
||||
|
<span>{item.name_highlight || item.name}</span> |
||||
|
</Box> |
||||
|
)} |
||||
|
/> |
||||
|
)) |
@ -0,0 +1,71 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React from 'react' |
||||
|
import styled from 'styled-components' |
||||
|
import { components } from 'react-select' |
||||
|
|
||||
|
import type { OptionProps } from 'react-select/lib/types' |
||||
|
|
||||
|
import Box from 'components/base/Box' |
||||
|
import IconCheck from 'icons/Check' |
||||
|
import IconAngleDown from 'icons/AngleDown' |
||||
|
import IconCross from 'icons/Cross' |
||||
|
|
||||
|
import type { Option } from './index' |
||||
|
|
||||
|
export default ({ |
||||
|
renderOption, |
||||
|
renderValue, |
||||
|
}: { |
||||
|
renderOption: Option => Node, |
||||
|
renderValue: Option => Node, |
||||
|
}) => ({ |
||||
|
...STYLES_OVERRIDE, |
||||
|
Option: (props: OptionProps) => { |
||||
|
const { data, isSelected } = props |
||||
|
return ( |
||||
|
<components.Option {...props}> |
||||
|
<Box horizontal pr={4} relative> |
||||
|
<Box grow>{renderOption ? renderOption(props) : data.label}</Box> |
||||
|
{isSelected && ( |
||||
|
<CheckContainer color="wallet"> |
||||
|
<IconCheck size={12} color="red" /> |
||||
|
</CheckContainer> |
||||
|
)} |
||||
|
</Box> |
||||
|
</components.Option> |
||||
|
) |
||||
|
}, |
||||
|
SingleValue: (props: OptionProps) => { |
||||
|
const { data } = props |
||||
|
return ( |
||||
|
<components.SingleValue {...props}> |
||||
|
{renderValue ? renderValue(props) : data.label} |
||||
|
</components.SingleValue> |
||||
|
) |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
const STYLES_OVERRIDE = { |
||||
|
DropdownIndicator: (props: OptionProps) => ( |
||||
|
<components.DropdownIndicator {...props}> |
||||
|
<IconAngleDown size={20} /> |
||||
|
</components.DropdownIndicator> |
||||
|
), |
||||
|
ClearIndicator: (props: OptionProps) => ( |
||||
|
<components.ClearIndicator {...props}> |
||||
|
<IconCross size={16} /> |
||||
|
</components.ClearIndicator> |
||||
|
), |
||||
|
} |
||||
|
|
||||
|
const CheckContainer = styled(Box).attrs({ |
||||
|
align: 'center', |
||||
|
justify: 'center', |
||||
|
})` |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
width: 10px; |
||||
|
` |
@ -0,0 +1,65 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import { colors } from 'styles/theme' |
||||
|
import { ff } from 'styles/helpers' |
||||
|
|
||||
|
export default ({ width, minWidth }: { width: number, minWidth: number }) => ({ |
||||
|
control: (styles: Object, { isFocused }: Object) => ({ |
||||
|
...styles, |
||||
|
width, |
||||
|
minWidth, |
||||
|
...ff('Open Sans|SemiBold'), |
||||
|
height: 40, |
||||
|
backgroundColor: 'white', |
||||
|
cursor: 'pointer', |
||||
|
...(isFocused |
||||
|
? { |
||||
|
borderColor: colors.wallet, |
||||
|
boxShadow: 'rgba(0, 0, 0, 0.05) 0 2px 2px', |
||||
|
} |
||||
|
: {}), |
||||
|
}), |
||||
|
valueContainer: (styles: Object) => ({ |
||||
|
...styles, |
||||
|
paddingLeft: 15, |
||||
|
color: colors.graphite, |
||||
|
}), |
||||
|
indicatorSeparator: (styles: Object) => ({ |
||||
|
...styles, |
||||
|
background: 'none', |
||||
|
}), |
||||
|
option: (styles: Object, { isFocused, isSelected }: Object) => ({ |
||||
|
...styles, |
||||
|
...ff('Open Sans|Regular'), |
||||
|
color: colors.dark, |
||||
|
padding: '10px 15px 10px 15px', |
||||
|
...(isFocused |
||||
|
? { |
||||
|
background: colors.lightGrey, |
||||
|
color: colors.dark, |
||||
|
} |
||||
|
: {}), |
||||
|
...(isSelected |
||||
|
? { |
||||
|
background: 'unset !important', |
||||
|
...ff('Open Sans|SemiBold'), |
||||
|
} |
||||
|
: { |
||||
|
cursor: 'pointer', |
||||
|
}), |
||||
|
}), |
||||
|
menu: (styles: Object) => ({ |
||||
|
...styles, |
||||
|
border: `1px solid ${colors.fog}`, |
||||
|
boxShadow: 'rgba(0, 0, 0, 0.05) 0 2px 2px', |
||||
|
}), |
||||
|
menuList: (styles: Object) => ({ |
||||
|
...styles, |
||||
|
background: 'white', |
||||
|
borderRadius: 3, |
||||
|
}), |
||||
|
container: (styles: Object) => ({ |
||||
|
...styles, |
||||
|
fontSize: 13, |
||||
|
}), |
||||
|
}) |
@ -1,343 +1,83 @@ |
|||||
// @flow
|
// @flow
|
||||
|
|
||||
import React, { PureComponent } from 'react' |
import React, { Component } from 'react' |
||||
import Downshift from 'downshift' |
import ReactSelect from 'react-select' |
||||
import styled from 'styled-components' |
import { translate } from 'react-i18next' |
||||
import { space } from 'styled-system' |
|
||||
|
|
||||
import Box from 'components/base/Box' |
import createStyles from './createStyles' |
||||
import GrowScroll from 'components/base/GrowScroll' |
import createRenderers from './createRenderers' |
||||
import Input from 'components/base/Input' |
|
||||
import Search from 'components/base/Search' |
|
||||
import Text from 'components/base/Text' |
|
||||
|
|
||||
import IconCheck from 'icons/Check' |
|
||||
|
|
||||
type Props = { |
type Props = { |
||||
bg?: string, |
// required
|
||||
flatLeft?: boolean, |
value: ?Option, |
||||
flatRight?: boolean, |
options: Option[], |
||||
fakeFocusRight?: boolean, |
onChange: Option => void, |
||||
fuseOptions?: Object, |
|
||||
highlight?: boolean, |
|
||||
items: Array<any>, |
|
||||
itemToString?: Function, |
|
||||
keyProp?: string, |
|
||||
maxHeight?: number, |
|
||||
onChange?: Function, |
|
||||
placeholder?: string, |
|
||||
renderHighlight?: string => React$Node, |
|
||||
renderItem?: (*) => React$Node, |
|
||||
renderSelected?: any => React$Node, |
|
||||
searchable?: boolean, |
|
||||
value?: *, |
|
||||
disabled: boolean, |
|
||||
small?: boolean, |
|
||||
} |
|
||||
|
|
||||
const Container = styled(Box).attrs({ relative: true, color: 'graphite' })`` |
|
||||
|
|
||||
const TriggerBtn = styled(Box).attrs({ |
|
||||
alignItems: 'center', |
|
||||
ff: p => (p.small ? 'Open Sans' : 'Open Sans|SemiBold'), |
|
||||
flow: 2, |
|
||||
fontSize: p => (p.small ? 3 : 4), |
|
||||
horizontal: true, |
|
||||
px: 3, |
|
||||
})` |
|
||||
${space}; |
|
||||
height: ${p => (p.small ? '34' : '40')}px; |
|
||||
background: ${p => (p.disabled ? p.theme.colors.lightGrey : p.bg || p.theme.colors.white)}; |
|
||||
border-bottom-left-radius: ${p => (p.flatLeft ? 0 : p.theme.radii[1])}px; |
|
||||
border-bottom-right-radius: ${p => (p.flatRight ? 0 : p.theme.radii[1])}px; |
|
||||
border-top-left-radius: ${p => (p.flatLeft ? 0 : p.theme.radii[1])}px; |
|
||||
border-top-right-radius: ${p => (p.flatRight ? 0 : p.theme.radii[1])}px; |
|
||||
border: 1px solid ${p => p.theme.colors.fog}; |
|
||||
color: ${p => p.theme.colors.graphite}; |
|
||||
cursor: ${p => (p.disabled ? 'cursor' : 'pointer')}; |
|
||||
display: flex; |
|
||||
width: 100%; |
|
||||
|
|
||||
&:focus { |
|
||||
outline: none; |
|
||||
${p => |
|
||||
p.disabled |
|
||||
? '' |
|
||||
: ` |
|
||||
border-color: ${p.theme.colors.wallet}; |
|
||||
box-shadow: rgba(0, 0, 0, 0.05) 0 2px 2px;`};
|
|
||||
} |
|
||||
|
|
||||
${p => { |
|
||||
const c = p.theme.colors.wallet |
|
||||
return p.fakeFocusRight |
|
||||
? ` |
|
||||
border-top: 1px solid ${c}; |
|
||||
border-right: 1px solid ${c}; |
|
||||
border-bottom: 1px solid ${c}; |
|
||||
` |
|
||||
: '' |
|
||||
}}; |
|
||||
` |
|
||||
|
|
||||
const Item = styled(Box).attrs({ |
// custom renders
|
||||
alignItems: 'center', |
renderOption: Option => Node, |
||||
fontSize: 4, |
renderValue: Option => Node, |
||||
ff: p => `Open Sans|${p.selected ? 'SemiBold' : 'Regular'}`, |
|
||||
px: 3, |
|
||||
py: 2, |
|
||||
color: 'dark', |
|
||||
})` |
|
||||
background: ${p => (p.highlighted ? p.theme.colors.lightGrey : p.theme.colors.white)}; |
|
||||
|
|
||||
${p => |
// optional
|
||||
p.first && |
placeholder?: string, |
||||
` |
isClearable?: boolean, |
||||
border-top-left-radius: ${p.theme.radii[1]}px; |
isDisabled?: boolean, |
||||
border-top-right-radius: ${p.theme.radii[1]}px; |
isLoading?: boolean, |
||||
`} ${p => |
isSearchable?: boolean, |
||||
p.last && |
width: number, |
||||
` |
minWidth: number, |
||||
border-bottom-left-radius: ${p.theme.radii[1]}px; |
} |
||||
border-bottom-right-radius: ${p.theme.radii[1]}px; |
|
||||
`};
|
|
||||
` |
|
||||
|
|
||||
const Dropdown = styled(Box).attrs({ |
|
||||
mt: 1, |
|
||||
})` |
|
||||
border-radius: ${p => p.theme.radii[1]}px; |
|
||||
border: 1px solid ${p => p.theme.colors.fog}; |
|
||||
box-shadow: rgba(0, 0, 0, 0.05) 0 2px 2px; |
|
||||
left: 0; |
|
||||
position: absolute; |
|
||||
right: 0; |
|
||||
top: 100%; |
|
||||
z-index: 1; |
|
||||
` |
|
||||
|
|
||||
const IconSelected = styled(Box).attrs({ |
|
||||
color: 'wallet', |
|
||||
alignItems: 'center', |
|
||||
justifyContent: 'center', |
|
||||
})` |
|
||||
height: 12px; |
|
||||
width: 12px; |
|
||||
opacity: ${p => (p.selected ? 1 : 0)}; |
|
||||
` |
|
||||
|
|
||||
const AngleDown = props => ( |
|
||||
<Box color="grey" alignItems="center" justifyContent="center" {...props}> |
|
||||
<svg viewBox="0 0 16 16" width="16" height="16"> |
|
||||
<path |
|
||||
fill="currentColor" |
|
||||
d="M7.70785815 10.86875l-5.08670521-4.5875c-.16153725-.146875-.16153725-.384375 0-.53125l.68051867-.61875c.16153726-.146875.42274645-.146875.58428371 0L8 8.834375l4.1140447-3.703125c.1615372-.146875.4227464-.146875.5842837 0l.6805187.61875c.1615372.146875.1615372.384375 0 .53125l-5.08670525 4.5875c-.16153726.146875-.42274644.146875-.5842837 0z" |
|
||||
/> |
|
||||
</svg> |
|
||||
</Box> |
|
||||
) |
|
||||
|
|
||||
const renderSelectedItem = ({ selectedItem, renderSelected, placeholder }: any) => |
|
||||
selectedItem && renderSelected ? ( |
|
||||
renderSelected(selectedItem) |
|
||||
) : ( |
|
||||
<Text color="fog">{placeholder}</Text> |
|
||||
) |
|
||||
|
|
||||
class Select extends PureComponent<Props> { |
|
||||
static defaultProps = { |
|
||||
bg: undefined, |
|
||||
disabled: false, |
|
||||
small: false, |
|
||||
fakeFocusRight: false, |
|
||||
flatLeft: false, |
|
||||
flatRight: false, |
|
||||
itemToString: (item: Object) => item && item.name, |
|
||||
keyProp: undefined, |
|
||||
maxHeight: 300, |
|
||||
} |
|
||||
|
|
||||
_scrollToSelectedItem = true |
|
||||
_oldHighlightedIndex = 0 |
|
||||
_useKeyboard = false |
|
||||
_children = {} |
|
||||
|
|
||||
renderItems = (items: Array<Object>, selectedItem: any, downshiftProps: Object) => { |
|
||||
const { renderItem, maxHeight, keyProp } = this.props |
|
||||
const { getItemProps, highlightedIndex } = downshiftProps |
|
||||
|
|
||||
const selectedItemIndex = items.indexOf(selectedItem) |
|
||||
|
|
||||
return ( |
|
||||
<Dropdown> |
|
||||
{items.length ? ( |
|
||||
<GrowScroll |
|
||||
maxHeight={maxHeight} |
|
||||
onUpdate={scrollbar => { |
|
||||
const currentHighlighted = this._children[highlightedIndex] |
|
||||
const currentSelectedItem = this._children[selectedItemIndex] |
|
||||
|
|
||||
if (this._useKeyboard && currentHighlighted) { |
|
||||
scrollbar.scrollIntoView(currentHighlighted, { |
|
||||
alignToTop: highlightedIndex < this._oldHighlightedIndex, |
|
||||
offsetTop: -1, |
|
||||
onlyScrollIfNeeded: true, |
|
||||
}) |
|
||||
} else if (this._scrollToSelectedItem && currentSelectedItem) { |
|
||||
window.requestAnimationFrame(() => |
|
||||
scrollbar.scrollIntoView(currentSelectedItem, { |
|
||||
offsetTop: -1, |
|
||||
}), |
|
||||
) |
|
||||
|
|
||||
this._scrollToSelectedItem = false |
export type Option = { |
||||
} |
value: 'string', |
||||
|
label: 'string', |
||||
|
data: any, |
||||
|
} |
||||
|
|
||||
this._oldHighlightedIndex = highlightedIndex |
class Select extends Component<Props> { |
||||
}} |
handleChange = (value, { action }) => { |
||||
> |
const { onChange } = this.props |
||||
{items.map((item, i) => ( |
if (action === 'select-option') { |
||||
<Box |
onChange(value) |
||||
key={keyProp ? item[keyProp] : item.key} |
} |
||||
innerRef={n => (this._children[i] = n)} |
|
||||
{...getItemProps({ item })} |
|
||||
> |
|
||||
<Item |
|
||||
first={i === 0} |
|
||||
last={i === items.length - 1} |
|
||||
highlighted={i === highlightedIndex} |
|
||||
selected={selectedItem === item} |
|
||||
horizontal |
|
||||
flow={3} |
|
||||
> |
|
||||
<Box grow> |
|
||||
{renderItem ? ( |
|
||||
renderItem(item) |
|
||||
) : ( |
|
||||
<span>{item.name_highlight || item.name}</span> |
|
||||
)} |
|
||||
</Box> |
|
||||
<Box> |
|
||||
<IconSelected selected={selectedItem === item}> |
|
||||
<IconCheck size={12} /> |
|
||||
</IconSelected> |
|
||||
</Box> |
|
||||
</Item> |
|
||||
</Box> |
|
||||
))} |
|
||||
</GrowScroll> |
|
||||
) : ( |
|
||||
<Box> |
|
||||
<Item>{'No results'}</Item> |
|
||||
</Box> |
|
||||
)} |
|
||||
</Dropdown> |
|
||||
) |
|
||||
} |
} |
||||
|
|
||||
render() { |
render() { |
||||
const { |
const { |
||||
disabled, |
|
||||
fakeFocusRight, |
|
||||
flatLeft, |
|
||||
flatRight, |
|
||||
fuseOptions, |
|
||||
highlight, |
|
||||
items, |
|
||||
itemToString, |
|
||||
onChange, |
|
||||
placeholder, |
|
||||
renderHighlight, |
|
||||
renderSelected, |
|
||||
searchable, |
|
||||
value, |
value, |
||||
small, |
isClearable, |
||||
|
isSearchable, |
||||
|
isDisabled, |
||||
|
isLoading, |
||||
|
placeholder, |
||||
|
options, |
||||
|
renderOption, |
||||
|
renderValue, |
||||
|
width, |
||||
|
minWidth, |
||||
...props |
...props |
||||
} = this.props |
} = this.props |
||||
|
|
||||
return ( |
return ( |
||||
<Downshift |
<ReactSelect |
||||
selectedItem={value} |
value={value} |
||||
itemToString={itemToString} |
maxMenuHeight={300} |
||||
onChange={onChange} |
classNamePrefix="select" |
||||
render={({ |
options={options} |
||||
getInputProps, |
components={createRenderers({ renderOption, renderValue })} |
||||
getToggleButtonProps, |
styles={createStyles({ width, minWidth })} |
||||
getRootProps, |
placeholder={placeholder} |
||||
isOpen, |
isDisabled={isDisabled} |
||||
inputValue, |
isLoading={isLoading} |
||||
openMenu, |
isClearable={isClearable} |
||||
selectedItem, |
isSearchable={isSearchable} |
||||
...downshiftProps |
blurInputOnSelect={false} |
||||
}) => { |
onChange={this.handleChange} |
||||
if (!isOpen) { |
backspaceRemovesValue |
||||
this._scrollToSelectedItem = true |
menuShouldBlockScroll |
||||
} |
{...props} |
||||
|
|
||||
if (disabled) { |
|
||||
return ( |
|
||||
<Container {...getRootProps({ refKey: 'innerRef' })}> |
|
||||
<TriggerBtn disabled bg={props.bg} tabIndex={0} small={small}> |
|
||||
{renderSelectedItem({ selectedItem, renderSelected, placeholder })} |
|
||||
</TriggerBtn> |
|
||||
</Container> |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<Container |
|
||||
{...getRootProps({ refKey: 'innerRef' })} |
|
||||
{...props} |
|
||||
horizontal |
|
||||
onKeyDown={() => (this._useKeyboard = true)} |
|
||||
onKeyUp={() => (this._useKeyboard = false)} |
|
||||
> |
|
||||
{searchable ? ( |
|
||||
<Box grow> |
|
||||
<Input |
|
||||
small |
|
||||
keepEvent |
|
||||
onClick={openMenu} |
|
||||
renderRight={<AngleDown mr={2} />} |
|
||||
{...getInputProps({ placeholder })} |
|
||||
/> |
|
||||
</Box> |
|
||||
) : ( |
|
||||
<TriggerBtn |
|
||||
{...getToggleButtonProps()} |
|
||||
bg={props.bg} |
|
||||
fakeFocusRight={fakeFocusRight} |
|
||||
flatLeft={flatLeft} |
|
||||
flatRight={flatRight} |
|
||||
tabIndex={0} |
|
||||
small={small} |
|
||||
> |
|
||||
<Box grow> |
|
||||
{renderSelectedItem({ selectedItem, renderSelected, placeholder })} |
|
||||
</Box> |
|
||||
<AngleDown mr={-1} /> |
|
||||
</TriggerBtn> |
|
||||
)} |
|
||||
<div hidden={!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) |
|
||||
)} |
|
||||
</div> |
|
||||
</Container> |
|
||||
) |
|
||||
}} |
|
||||
/> |
/> |
||||
) |
) |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
export default Select |
export default translate()(Select) |
||||
|
@ -0,0 +1,62 @@ |
|||||
|
import { colors } from 'styles/theme' |
||||
|
import { ff } from 'styles/helpers' |
||||
|
|
||||
|
export default { |
||||
|
control: (styles, { isFocused }) => ({ |
||||
|
...styles, |
||||
|
...ff('Open Sans|SemiBold'), |
||||
|
height: 40, |
||||
|
backgroundColor: 'white', |
||||
|
cursor: 'pointer', |
||||
|
...(isFocused |
||||
|
? { |
||||
|
borderColor: colors.wallet, |
||||
|
boxShadow: 'rgba(0, 0, 0, 0.05) 0 2px 2px', |
||||
|
} |
||||
|
: {}), |
||||
|
}), |
||||
|
valueContainer: styles => ({ |
||||
|
...styles, |
||||
|
paddingLeft: 15, |
||||
|
color: colors.graphite, |
||||
|
}), |
||||
|
indicatorSeparator: styles => ({ |
||||
|
...styles, |
||||
|
background: 'none', |
||||
|
}), |
||||
|
option: (styles, { isFocused, isSelected }) => ({ |
||||
|
...styles, |
||||
|
...ff('Open Sans|Regular'), |
||||
|
color: colors.dark, |
||||
|
padding: '10px 15px 10px 15px', |
||||
|
...(isFocused |
||||
|
? { |
||||
|
background: colors.lightGrey, |
||||
|
color: colors.dark, |
||||
|
} |
||||
|
: {}), |
||||
|
...(isSelected |
||||
|
? { |
||||
|
background: 'unset !important', |
||||
|
...ff('Open Sans|SemiBold'), |
||||
|
} |
||||
|
: { |
||||
|
cursor: 'pointer', |
||||
|
}), |
||||
|
}), |
||||
|
menu: styles => ({ |
||||
|
...styles, |
||||
|
border: `1px solid ${colors.fog}`, |
||||
|
boxShadow: 'rgba(0, 0, 0, 0.05) 0 2px 2px', |
||||
|
}), |
||||
|
menuList: styles => ({ |
||||
|
...styles, |
||||
|
background: 'white', |
||||
|
borderRadius: 3, |
||||
|
overflow: 'hidden', |
||||
|
}), |
||||
|
container: styles => ({ |
||||
|
...styles, |
||||
|
fontSize: 13, |
||||
|
}), |
||||
|
} |
Loading…
Reference in new issue