Skip to content

Instantly share code, notes, and snippets.

@Bitaru
Created December 13, 2017 16:49
Show Gist options
  • Save Bitaru/558a735e53fbb93aeaae957fff339027 to your computer and use it in GitHub Desktop.
Save Bitaru/558a735e53fbb93aeaae957fff339027 to your computer and use it in GitHub Desktop.
DownshiftAutocomplete
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>
));
@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