Skip to content

Instantly share code, notes, and snippets.

@olecksamdr
Created October 23, 2018 06:39
Show Gist options
  • Save olecksamdr/b0d85d51981c6fb4cda44c803ffb6d53 to your computer and use it in GitHub Desktop.
Save olecksamdr/b0d85d51981c6fb4cda44c803ffb6d53 to your computer and use it in GitHub Desktop.
Select component
import { func } from 'prop-types';
import React, { Component } from 'react';
import { Options as StyledOptions } from 'components/global/Select/style';
class Options extends Component {
constructor(props) {
super(props);
this.optionsRef = React.createRef();
}
handleScroll = ({ target }) => {
if (target.scrollTop + target.clientHeight >= target.scrollHeight - 56) {
this.props.onLoadMore();
}
};
componentDidMount() {
this.optionsRef.current.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
this.optionsRef.current.removeEventListener('scroll', this.handleScroll);
}
render () {
return <StyledOptions innerRef={this.optionsRef} {...this.props} />
}
}
Options.propTypes = {
onLoadMore: func.isRequired,
};
export default Options;
import { contains, identity, pipe, prop } from 'ramda';
import { arrayOf, func, shape, string } from 'prop-types';
import {
compose,
mapProps,
withState,
setPropTypes,
defaultProps,
withHandlers,
} from 'recompose';
import withClickOutside from 'utils/hocs/withClickOutside';
import { isNotEmpty } from 'utils/general';
import Select from './SelectComp';
const labelContains = value => pipe(prop('label'), contains(value));
const filterBy = (value, items) =>
isNotEmpty(value) ? items.filter(labelContains(value)) : items;
export const selectEnhancer = compose(
setPropTypes({
onLoadMore: func,
onSelect: func,
onChange: func,
options: arrayOf(shape({
label: string.isRequired,
value: string.isRequired
})),
}),
defaultProps({
onChange: identity,
onSelect: identity,
}),
withState('isOpen', 'setIsOpen', false),
withState('value', 'setValue', ''),
withState('filter', 'setFilter', ''),
withClickOutside({
onClickOutside: ({ setIsOpen }) => setIsOpen(false),
clickHandlerName: 'onSelectBodyClick',
}),
withHandlers({
onFocus: ({ setIsOpen }) => () => setIsOpen(true),
onChange: ({ setValue, setFilter, onChange }) => ({ target }) => {
const { value } = target;
setValue(value);
setFilter(value);
onChange(value);
},
select: ({ setValue, setIsOpen, setFilter, onSelect }) => (value) => {
setValue(value);
onSelect(value);
setFilter('');
setIsOpen(false);
},
}),
mapProps(({ options, filter, ...props }) => ({
...props,
options: filterBy(filter, options)
})),
);
export default selectEnhancer(Select);
import React from 'react';
import styled from 'styled-components';
import { func, string, bool, shape, arrayOf } from 'prop-types';
import { ErrorMessage } from 'components/global/ErrorMessage';
import { SpinnerSvg } from 'components/global/Spinner';
import { Translation } from 'shame/translations';
import InfiniteOptions from './InfiniteOptions';
import {
Arrow,
Option,
Options,
Wrapper,
HeaderWrapper,
InputWithArrow,
} from './style';
const SpinnerWrapper = styled.div`
position: absolute;
top: calc(50% - 10px);
right: 46px;
display: flex;
align-items: center;
justify-content: center;
`;
const SelectInput = ({ isOpen, loading, ...props }) => (
<HeaderWrapper>
<InputWithArrow
{...{ isOpen }}
{...props}
type="text"
autoComplete="off"
/>
{loading && (
<SpinnerWrapper>
<SpinnerSvg size={20} />
</SpinnerWrapper>
)}
<Arrow {...{ isOpen }} />
</HeaderWrapper>
);
const Message = ({ loading }) => (
<Option>
<Translation path={`select.${loading ? 'loading' : 'noData'}`}/>
</Option>
);
const SelectComp = ({
name,
value,
select,
isOpen,
invalid,
options,
loading,
onFocus,
onChange,
onLoadMore,
placeholder,
registerRef,
onSelectBodyClick,
}) => {
const OptionsComponent = onLoadMore ? InfiniteOptions : Options;
return (
<Wrapper
{...{ isOpen }}
innerRef={registerRef}
onClick={onSelectBodyClick}
>
<SelectInput
{...{
name,
value,
isOpen,
onFocus,
loading,
invalid,
onChange,
placeholder,
}} />
{isOpen && (
<OptionsComponent {...{ onLoadMore }}>
{options.length ?
options.map(({ label, value, disabled }) => (
<Option
key={label}
onClick={() => !disabled && select(value)}
{...{ disabled }}
>
{label}
</Option>
))
:
<Message {...{ loading }} />
}
</OptionsComponent>
)}
<ErrorMessage {...{ name }} />
</Wrapper>
);
};
SelectComp.propTypes = {
name: string,
value: string.isRequired,
select: func.isRequired,
isOpen: bool.isRequired,
invalid: bool,
options: arrayOf(shape({
label: string.isRequired,
value: string.isRequired
})),
loading: bool,
onLoadMore: func,
onFocus: func.isRequired,
onChange: func.isRequired,
registerRef: func.isRequired,
placeholder: string.isRequired,
onSelectBodyClick: func.isRequired,
};
export default SelectComp;
import styled, { css } from 'styled-components';
import { getThemeColor } from 'utils/theme';
import { StyledInput } from 'components/global/Input';
const ARROW_WIDTH = 14;
const ARROW_HEIGHT = 8;
const shadow = css`
box-shadow: 0 3px 4px rgba(10, 31, 68, 0.1),
0 0 1px rgba(10, 31, 68, 0.08);
`;
const focusStyles = css`
${shadow};
outline: none;
border-color: transparent;
`;
export const Input = StyledInput.extend`
:focus {
${focusStyles};
}
${({ isOpen }) => isOpen && css`
${focusStyles};
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
`};
transition: border-color .1s, box-shadow .1s;
`;
export const InputWithArrow = Input.extend`
padding-right: ${32 + ARROW_WIDTH}px;
`;
export const Arrow = styled.span`
display:inline-block;
position: relative;
width: ${ARROW_WIDTH}px;
height: ${ARROW_HEIGHT}px;
cursor: pointer;
&::before,
&::after {
content: '';
background: ${getThemeColor(['ink'])};
display: block;
position: absolute;
top: 0;
bottom: 0;
height: 2px;
width: calc(50% + 1px);
margin: auto;
transition: transform .3s;
}
&::before {
left: 0;
transform: rotate(${({ isOpen }) => isOpen ? -40 : 40}deg);
border-radius: .5rem 0 0 .5rem;
}
&::after {
right: 0;
transform: rotate(${({ isOpen }) => isOpen ? 40 : -40}deg);
border-radius: 0 .5rem .5rem 0;
}
`;
export const HeaderWrapper = styled.div`
position: relative;
${Arrow} {
position: absolute;
top: calc(50% - 0.25rem);
right: 16px;
}
`;
const OPTION_HEIGHT = 56;
export const Wrapper = styled.div`
position: relative;
`;
export const Options = styled.div`
${shadow};
width: 100%;
min-height: ${OPTION_HEIGHT}px;
max-height: ${({ maxItems = 4 }) => maxItems * OPTION_HEIGHT}px;
overflow: auto;
margin-top: -1px;
padding: 0;
position: absolute;
background-color: ${getThemeColor(['white'])};
border-top: 1px solid ${getThemeColor(['cloud'])};
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
z-index: 1;
`;
export const Option = styled.div`
padding: 17px 17px 17px 16px;
position: relative;
font-size: 0.875rem;
line-height: 1.57;
color: ${getThemeColor(['ink'])};
background-color: ${getThemeColor(['white'])};
text-align: left;
cursor: pointer;
${({ disabled }) => disabled && css`
color: ${getThemeColor(['cloudDark'])};
cursor: not-allowed;
`};
:hover {
background-color: ${getThemeColor(['cloudLighter'])};
}
&:not(:last-child)::after {
height: 1px;
width: calc(100% - 16px);
content: '';
display: block;
position: absolute;
bottom: 0;
background-color: ${getThemeColor(['cloud'])};
}
`;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment