Created
October 23, 2018 06:39
-
-
Save olecksamdr/b0d85d51981c6fb4cda44c803ffb6d53 to your computer and use it in GitHub Desktop.
Select component
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 { 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; |
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 { 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); |
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 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; |
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 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