import { GeneratedPropTypes } from '../types';
import { Icon } from './Icon';
import { Text } from './Typography';
import { colors } from '../theme/colors';
import { ease, generateProps, mq, transitions } from 'styled-gen';
import { flyoutMenuShadow, fonts, inputWrapperStyle } from '../theme';
import { position } from 'polished';
import { useClickOutside } from '../hooks/useClickOutside';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled, { css } from 'styled-components';
type Value = string | number;
type OptionType = {
label?: string;
value: string;
};
export type SelectProps = {
clearLabel?: string | Function;
disabled?: boolean;
hint?: string;
isClearable?: boolean;
isMultiple?: boolean;
onBlur?: Function;
onChange?: Function;
onFocus?: Function;
options?: OptionType[];
optionsSearchPlaceholder?: string;
renderLabel: Function;
renderOption?: Function;
value?: Value | Value[];
withError?: boolean;
withOptionsSearch?: boolean;
} & GeneratedPropTypes;
// #region ====== style ===
const SelectedOptionContent = styled.div`
margin-right: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
& > * {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
const InputWrapper = styled.a<{ disabled?: boolean; isFocused?: boolean; withError?: boolean } & GeneratedPropTypes>`
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')} !important;
align-items: center;
display: flex;
justify-content: space-between;
${inputWrapperStyle};
`;
const Option = styled.a<{ isActive?: boolean }>`
display: flex;
padding: 0.625rem 0.875rem;
background-color: ${colors.n01};
justify-content: space-between;
align-items: center;
${({ isActive }) =>
isActive
? css`
background-color: ${colors.p25};
`
: css`
&:hover {
background-color: ${colors.p100};
}
`}
`;
const OptionsContent = styled.div`
overflow-y: auto;
max-height: 15rem;
${mq.tablet(css`
max-height: 17.25rem;
`)}
`;
const OptionsWrapper = styled.div<{ isActive?: boolean; withOptionsSearch?: boolean }>`
${transitions('all', 250, ease.inOutCubic)};
background-color: ${colors.n01};
opacity: 0;
overflow: hidden;
padding-top: ${({ withOptionsSearch }) => (withOptionsSearch ? '2.75rem' : 0)};
position: absolute;
transform: translateY(1.5rem);
visibility: hidden;
width: 100%;
z-index: 1;
border-radius: 0.5rem;
top: 100%;
${({ isActive }) =>
isActive &&
css`
opacity: 1;
visibility: visible;
transform: translateY(0.5rem);
`}
${flyoutMenuShadow};
`;
const SearchInput = styled.div`
${position('absolute', 0, 0, null, 0)};
background-color: ${colors.n01};
display: flex;
padding: 0.625rem 0.875rem;
input {
color: ${colors.g900};
font-family: ${fonts.families.sans};
font-size: 1rem;
width: 100%;
::placeholder,
::-webkit-input-placeholder {
color: ${colors.g500};
}
:-ms-input-placeholder {
color: ${colors.g500};
}
}
`;
const Wrapper = styled.div`
position: relative;
${generateProps};
`;
// #endregion === style ===
export const Select = React.forwardRef((props: SelectProps, ref) => {
const {
clearLabel,
disabled,
hint,
isClearable,
isMultiple,
onBlur,
onChange,
onFocus,
options,
optionsSearchPlaceholder,
renderLabel,
renderOption,
value,
withError,
withOptionsSearch,
...wrapperProps
} = props;
const [filteredOptions, setFilteredOptions] = useState(options || []);
const [filterTerm, setFilterTerm] = useState('');
const [isActive, setIsActive] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const optionsListRef = useRef();
const checkIfActive = (option: OptionType) =>
!isMultiple || !Array.isArray(value) ? value === option?.value : (value as Value[]).includes(option?.value);
const getLabelValue = () => {
return !isMultiple
? options?.find(option => option?.value === value)
: options?.filter(option => (value as Value[]).includes(option?.value));
};
const handleBlur = useCallback(
(event?: any) => {
setIsFocused(false);
if (typeof onBlur === 'function') {
return onBlur(event);
}
},
[onBlur]
);
const handleChange = (option?: OptionType) => {
setFilterTerm('');
if (typeof onChange !== 'function') {
return;
}
const { value: optionValue } = option || {};
if (typeof option === 'undefined') {
return onChange(isMultiple ? [] : undefined);
}
if (!isMultiple) {
setIsActive(false);
return onChange(optionValue);
}
const newValueArray = (value as Value[]).includes(optionValue as any)
? ((value || []) as Value[]).filter(pickedValue => pickedValue !== optionValue)
: [...(value as Value[]), optionValue];
return onChange(newValueArray);
};
const handleFocus = useCallback(
(event?: any) => {
setIsFocused(true);
if (typeof onFocus === 'function') {
return onFocus(event);
}
},
[onFocus]
);
const handleClickOutside = () => {
if (!isActive) {
return;
}
return setIsActive(false);
};
const hasOptionSelected = () =>
(typeof value !== 'undefined' && value !== null && value !== '') || (Array.isArray(value) && value?.length);
useClickOutside(optionsListRef?.current, handleClickOutside);
useEffect(() => {
setFilteredOptions(options || []);
}, [options]);
useEffect(() => {
if (isActive) {
handleFocus();
}
if (!isActive) {
handleBlur();
}
}, [isActive]);
useEffect(() => {
if (filterTerm) {
const filteredOptions = options?.filter(
({ label, value }) =>
label?.toLowerCase()?.includes(filterTerm.toLowerCase()) ||
value.toString().includes(filterTerm.toLowerCase())
);
setFilteredOptions(filteredOptions || []);
} else {
setFilteredOptions(options || []);
}
}, [filterTerm]);
return (
<>
!disabled && setIsActive(!isActive)}
withError={withError}
>
{renderLabel({ isFocused, selected: getLabelValue() })}
{withOptionsSearch && (
setFilterTerm(event?.target?.value)}
placeholder={optionsSearchPlaceholder || ''}
type="text"
value={filterTerm || ''}
/>
)}
{isClearable && !!clearLabel && (
)}
{filteredOptions.map((option: OptionType, index: any) => (
))}
{!!hint && (
{hint}
)}
>
);
});
Select.displayName = 'Select';