Created
December 13, 2017 16:49
-
-
Save Bitaru/558a735e53fbb93aeaae957fff339027 to your computer and use it in GitHub Desktop.
DownshiftAutocomplete
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from 'react'; | |
import { omit, pickBy } from 'ramda'; | |
import { | |
compose, withHandlers, | |
withPropsOnChange, branch, defaultProps, | |
renderNothing, withStateHandlers, | |
withProps, mapProps, renameProp | |
} from 'recompose'; | |
// import createLabel from '../createLabel'; | |
import Downshift from 'downshift'; | |
import Icon from 'components/Icon'; | |
import * as cx from 'classnames'; | |
const styles = require('./styles.css'); | |
const pickItemsByValue = value => | |
pickBy(v => !value || v.toLowerCase().includes(value.toLowerCase())); | |
const getItems = compose( | |
branch( | |
({ multiple }) => multiple, | |
withPropsOnChange(['selectedItems', 'items'], ({ items, selectedItems }) => ({ | |
items: pickBy((_, k) => !selectedItems.includes(k))(items) | |
})) | |
), | |
branch( | |
({ searchable }) => !!searchable, | |
withPropsOnChange(['value', 'items'], ({ items, value }) => ({ | |
items: pickItemsByValue(value)(items) | |
})), | |
withPropsOnChange(['value', 'items'], ({ items, value }) => ({ | |
items: value | |
? pickBy(v => !v.toLowerCase().includes(value.toLowerCase()))(items) | |
: items | |
})), | |
) | |
); | |
const withState = withStateHandlers( | |
({ defaultValue }) => ({ search: '', selected: defaultValue || [] }), | |
{ | |
onSearch: (state, { onChangeSearch }) => search => { | |
if (onChangeSearch) onChangeSearch(search); | |
return { ...state, search }; | |
}, | |
addSelected: (state, { onChange }) => (item) => { | |
const selected = [...state.selected, item]; | |
if (state.selected.includes(item)) return { ...state, search: '' }; | |
onChange(selected); | |
return { search: '', selected }; | |
} | |
} | |
); | |
const Item = ({ | |
text, | |
highlighted, | |
...itemProps | |
}) => ( | |
<button {...itemProps} className={cx(styles.item, highlighted && styles.highlighted)}> | |
{ text } | |
</button> | |
); | |
const Items = compose( | |
branch(({ isOpen }) => !isOpen, renderNothing), | |
getItems, | |
)(({ | |
items, | |
itemProps, | |
highlightedIndex, | |
selectedItem | |
}: any) => !!Object.keys(items).length && ( | |
<div className={styles.items}> | |
{ | |
Object.keys(items).map((item, index) => ( | |
<Item | |
key={item} | |
disabled={selectedItem === item} | |
highlighted={highlightedIndex === index} | |
text={items[item]} | |
{...itemProps({ item })} | |
/> | |
)) | |
} | |
</div> | |
)); | |
const Input = compose( | |
withHandlers({ | |
onFocus: ({ onSearch, onFocus }) => (e) => { | |
if (onSearch) onSearch(''); | |
return onFocus(e); | |
}, | |
onChange: ({ onSearch, onChange }) => (e) => { | |
if (onSearch) onSearch(e.target.value); | |
return onChange(e); | |
}, | |
onKeyDown: ({ onKeyDown, onCreate, ...props }) => (e) => { | |
if (onCreate && e.key.toLowerCase() === 'enter' && !props['aria-activedescendant']) { | |
onCreate(); | |
} | |
return onKeyDown(e); | |
} | |
}), | |
branch( | |
({ multiple, search }) => multiple && search !== undefined, | |
renameProp('search', 'value') | |
), | |
mapProps(omit(['onSearch', 'onCreate'])), | |
)(({ value, items, ...props} :any) => ( | |
<input {...props} className={styles.input} value={items && items[value] || value} /> | |
)); | |
const Button = ({ | |
theme, | |
value, | |
multiple, | |
placeholder, | |
...buttonProps | |
}) => ( | |
<button {...buttonProps} className={cx(styles.button, !value && styles.placeholder)}> | |
{ value || placeholder } | |
</button> | |
); | |
export const Autocomplete = compose( | |
defaultProps({ | |
placeholder: 'Select...' | |
}), | |
withProps(({ allowCreate }) => allowCreate && ({ | |
multiple: true, | |
searchable: true | |
})), | |
withPropsOnChange(['options'], ({ options }) => ({ | |
items: Array.isArray(options) | |
? options.reduce((acc, item) => ({ ...acc, [String(item)]: String(item) }), {}) | |
: options || {} | |
})), | |
branch( | |
({ searchable, multiple, allowCreate }) => searchable || multiple || allowCreate, | |
withState | |
), | |
branch( | |
({ multiple }) => multiple, | |
withHandlers({ | |
onChange: ({ addSelected }) => addSelected, | |
onCreate: ({ addSelected, search }) => () => addSelected(search) | |
}) | |
) | |
)(({ | |
searchable, // allow to search inside input | |
multiple, // allow to select multiple items | |
disabled, | |
allowCreate, | |
autoFocus, | |
hideArrow, | |
onChange, | |
onCreate, | |
onSearch, | |
placeholder, | |
items, | |
search, | |
selected, | |
icon, | |
defaultValue | |
}) => ( | |
<Downshift onChange={onChange} defaultInputValue={String(defaultValue)}> | |
{ | |
({ | |
getInputProps, | |
getItemProps, | |
getButtonProps, | |
isOpen, | |
inputValue, | |
selectedItem, | |
highlightedIndex, | |
openMenu | |
}) => ( | |
<div className={cx(styles.autocomplete, isOpen && styles.focus)}> | |
{ icon } | |
{ | |
searchable && | |
<Input | |
{...getInputProps({ placeholder, disabled, autoFocus, onFocus: openMenu })} | |
onSearch={onSearch} | |
onCreate={allowCreate && onCreate} | |
search={search} | |
multiple={multiple} | |
items={items} | |
/> | |
} | |
{ | |
!searchable && | |
<Button | |
{...getButtonProps({ disabled, placeholder })} | |
value={multiple ? undefined : items[inputValue]} | |
/> | |
} | |
{ | |
!hideArrow && !!Object.keys(items).length && | |
<Icon name='arrow-down' width={24} height={24} className={styles.chevron}/> | |
} | |
<Items | |
isOpen={isOpen} | |
items={items} | |
itemProps={getItemProps} | |
multiple={multiple} | |
selectedItems={multiple && selected} | |
searchable={searchable} | |
selectedItem={selectedItem} | |
highlightedIndex={highlightedIndex} | |
value={search !== undefined ? search : inputValue} | |
/> | |
</div> | |
) | |
} | |
</Downshift> | |
)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import 'variables.css'; | |
.label{ | |
display: block; | |
margin-bottom: 5px; | |
font-weight: normal; | |
} | |
.wrapper{ | |
position: relative; | |
} | |
.icon{ | |
position: absolute; | |
left: 15px; | |
top: 50%; | |
margin-top: -12px; | |
z-index: 1; | |
} | |
.chevron{ | |
position: absolute; | |
top: 50%; | |
margin-top: -12px; | |
right: 15px; | |
z-index: 1; | |
color: $base; | |
transition: color .1s linear; | |
.focus &{ | |
color: $hightlight; | |
} | |
} | |
.size{ | |
&-medium { | |
.input, .button{ | |
height: 50px; | |
padding: 0 15px 0 19px; | |
padding-right: 52px; | |
} | |
} | |
&-small { | |
.input, .button{ | |
height: 40px; | |
padding: 0 15px; | |
padding-right: 52px; | |
} | |
} | |
} | |
.autocomplete{ | |
position: relative; | |
background: $white; | |
} | |
.input, .button{ | |
display: block; | |
position: relative; | |
width: 100%; | |
border-radius: 3px; | |
text-align: left; | |
z-index: 3; | |
background: transparent; | |
border: 1px solid $grey-2; | |
font-size: 14px; | |
color: $base; | |
transition: border-color .1s linear; | |
&:not(:first-child){ | |
padding-left: 52px; | |
} | |
.focus &{ | |
border-color: $hightlight; | |
} | |
&::placeholder, .placeholder &{ | |
font-size: 14px; | |
font-style: italic; | |
font-weight: 500; | |
color: $grey-4; | |
} | |
&:disabled, .disabled &{ | |
background-color: $grey; | |
color: $grey-4; | |
cursor: not-allowed; | |
} | |
.hasError &{ | |
border-color: $warning; | |
} | |
} | |
.items { | |
background-color: $white; | |
max-height: 0; | |
overflow-x: hidden; | |
overflow-y: scroll; | |
padding: 0; | |
position: absolute; | |
transition-duration: .1s; | |
transition-property: max-height; | |
transition-timing-function: ease-in-out; | |
visibility: hidden; | |
left: 0; | |
right: 0; | |
top: 100%; | |
margin-top: -3px; | |
z-index: 5; | |
border-bottom-left-radius: 5px; | |
border-bottom-right-radius: 5px; | |
border-top: 1px solid $hightlight; | |
font-size: 14px; | |
color: $base; | |
.focus &{ | |
max-height: 240px; | |
visibility: visible; | |
-ms-overflow-style:none; | |
border: 1px solid $hightlight; | |
border-top: none; | |
} | |
.hideOptions &{ | |
display: none; | |
} | |
.showAllOptions &{ | |
max-height: 1000px; | |
} | |
} | |
.item { | |
display: block; | |
border: none; | |
outline: none; | |
cursor: pointer; | |
width: 100%; | |
padding: 10px 15px; | |
position: relative; | |
text-align: left; | |
color: $base; | |
font-size: 15px; | |
border-radius: 0; | |
&:after{ | |
content: ' '; | |
position: absolute; | |
left: 5px; | |
right: 5px; | |
bottom: -1px; | |
height: 1px; | |
background-color: $grey; | |
} | |
&:last-child:after{ | |
display: none; | |
} | |
&.highlighted { | |
background-color: $grey; | |
} | |
} | |
.theme-{ | |
&dark{ | |
.autocomplete{ | |
background: $base-dark; | |
} | |
.input, .button{ | |
color: $white; | |
border-color: $base-darker; | |
} | |
.items{ | |
background-color: $base-dark; | |
} | |
.item{ | |
color: $white; | |
border-color: $base-darker; | |
} | |
.focus { | |
.item{ | |
border-color: $base-darker; | |
&:hover{ | |
border-color: $base-darker; | |
} | |
&:after{ | |
background-color: $base-darker; | |
} | |
} | |
.items{ | |
border-color: $base-darker; | |
} | |
.highlighted { | |
background-color: $base-darker; | |
} | |
} | |
} | |
} | |
@media(min-width: 768px){ | |
.items::-webkit-scrollbar { | |
-webkit-appearance: none; | |
width: 7px; | |
} | |
.items::-webkit-scrollbar-thumb { | |
border-radius: 4px; | |
background-color: rgba(0,0,0,.5); | |
-webkit-box-shadow: 0 0 1px rgba(255,255,255,.5); | |
} | |
} | |
.inline{ | |
display: flex; | |
& .label{ | |
color: $base; | |
padding: 0 15px; | |
margin: 0; | |
background: #E9E9EC; | |
display: flex; | |
align-items: center; | |
border-top-left-radius: 3px; | |
border-bottom-left-radius: 3px; | |
} | |
& .input, & .button{ | |
border-top-left-radius: 0; | |
border-bottom-left-radius: 0; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment