@eureca/eureca-ui
Version:
UI component library of Eureca's user and admin apps
345 lines (316 loc) • 9.28 kB
JavaScript
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import styled, { css } from 'styled-components';
import _ from 'lodash';
import { Box, Menu, MenuItem, Typography } from '@material-ui/core';
import { motion } from 'framer-motion';
import { FiChevronDown } from 'react-icons/fi';
import { colors } from '../../theme/colors';
import { Checkbox } from '../Checkbox';
import { Flex } from '../Flex';
const variants = {
closed: { height: 0, overflow: 'hidden' },
open: { height: 'auto' },
transition: { duration: 500, ease: 'easeInOut' },
};
const SelectFlex = styled(Flex)`
${({ error }) => css`
border: 1px solid rgba(0, 0, 0, 0.23);
&:hover {
border: 1px solid rgba(0, 0, 0, 0.87);
}
${error &&
css`
border: 1px solid ${colors.error1};
&:hover {
border: 1px solid ${colors.error1};
}
`}
`}
`;
const ItemList = React.forwardRef(({ item, values, onClickItem, onClickAll, subItem }, ref) => {
const [isOpen, setIsOpen] = useState(false);
const indexValues = values.findIndex(element => element.id === item.id);
// 'haveItemSet' checks if the item has subitems as options or is a normal checkbox.
const haveItemSet = item[subItem]?.length > 0;
// 'set' is the subitems array, if exists
const set = haveItemSet && item[subItem];
const hasSome =
haveItemSet &&
_.intersection([...set.flatMap(i => i.id)], values[indexValues]?.subItems).length;
const allChecked = haveItemSet && hasSome === Object.keys(set).length;
function openOrToggleCheckbox(isMulti, e) {
e.stopPropagation();
if (isMulti) {
setIsOpen(!isOpen);
} else {
onClickItem(item.id, '');
}
}
function checkAllOrOne(isMulti, e) {
e.stopPropagation();
if (isMulti) {
onClickAll(
item.id,
set.map(i => i.id)
);
} else {
onClickItem(item.id, '');
}
}
return (
<>
<MenuItem
onClick={e => openOrToggleCheckbox(haveItemSet, e)}
data-testid={`menu-item-${item.id}`}
>
<Flex directionRow alignCenter justifySpaceBetween width={1}>
<Flex directionRow alignCenter>
<Checkbox
name={item.name}
value={item.id}
checked={haveItemSet ? hasSome > 0 : values[indexValues]?.id === item.id}
indeterminate={haveItemSet ? !allChecked && hasSome > 0 : false}
data-testid={`select-checkbox-${item.id}`}
onClick={e => checkAllOrOne(haveItemSet, e)}
/>
<Box>
<Typography>{item.name}</Typography>
</Box>
</Flex>
{haveItemSet && (
<Box ml={2} p={1}>
<FiChevronDown
size={16}
style={{
transition: 'all 0.3s ease-in-out',
transform: isOpen ? 'rotate(-180deg)' : 'rotate(0deg)',
}}
/>
</Box>
)}
</Flex>
</MenuItem>
{haveItemSet && (
<motion.div
variants={variants}
initial="closed"
animate={isOpen ? 'open' : 'closed'}
aria-expanded={isOpen.toString()}
>
{isOpen &&
set.map(subItem => (
<MenuItem
key={subItem.id}
onClick={e => {
e.preventDefault();
onClickItem(item.id, subItem);
}}
>
<Box pl={3}>
<Checkbox
label={subItem.name}
name={subItem.name}
value={subItem.id}
checked={values[indexValues]?.subItems.includes(subItem.id)}
data-testid={`select-checkbox-${subItem.id}`}
/>
</Box>
</MenuItem>
))}
</motion.div>
)}
</>
);
});
function SelectMulti({
items = [],
data = [],
onChange = () => {},
label = 'Select Multi',
subItem = 'subItems',
helperText,
error,
}) {
const [values, setValues] = useState(data ? [...data] : []);
const [content, setContent] = useState([]);
const [anchorEl, setAnchorEl] = useState(null);
const handleClick = event => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
function onClickItem(item, subItem) {
const indexValues = values.findIndex(element => element.id === item);
// Does the item already exists on the selected array?
if (indexValues === -1) {
// If it doesn't exist yet, add it to the array
const updated = values;
values.push({ id: item, subItems: [subItem.id] });
setValues([...updated]);
return;
}
// If the item does exist, check if the subitem exists in it's array
if (!values[indexValues].subItems.includes(subItem.id) && subItem !== ['']) {
const updated = values;
updated[indexValues] = {
id: item,
subItems: [...updated[indexValues].subItems, subItem.id],
};
setValues([...updated]);
return;
}
// If it does exists it means we must remove it from the array of subitems, it must be 'unchecked'
const updated = values;
updated[indexValues] = {
id: item,
subItems: updated[indexValues].subItems.filter(i => i !== subItem.id),
};
if (values[indexValues].subItems.length > 0) {
// If there's any other item on the array we're good
setValues([...updated]);
return;
} else {
// If it was the last one of it's kind we must remove the whole item from the values array
const newArray = [...values];
newArray.splice(indexValues, 1);
setValues(newArray);
return;
}
}
function handleClickAll(item, subItems) {
const indexValues = values.findIndex(element => element.id === item);
const areAllChecked =
_.intersection(values[indexValues]?.subItems, subItems).length === subItems.length;
// Does the item already exists on the selected array?
if (indexValues === -1) {
// If it doesn't exist yet, add it to the array
const updated = values;
values.push({ id: item, subItems: [...subItems] });
setValues([...updated]);
return;
}
// Add all items if they are not all at the values array
if (!areAllChecked) {
const updated = values;
updated[indexValues] = {
id: item,
subItems: [...subItems],
};
setValues([...updated]);
} else {
// Remove item from the array if they were all already present
const updated = [...values];
updated.splice(indexValues, 1);
setValues(updated);
}
}
useEffect(() => {
let sum = 0;
values.forEach(item => {
sum = sum + item.subItems.length;
});
if (sum === 0) {
setContent(label);
return;
}
if (sum === 1) {
setContent(`(${sum}) selecionada`);
return;
}
if (sum > 1 && sum < 10) {
setContent(`(0${sum}) selecionadas`);
return;
}
setContent(`(${sum}) selecionadas`);
}, [values, subItem, label]);
useEffect(() => {
onChange(values);
}, [onChange, values]);
return (
<Box position="relative">
<SelectFlex
px={1.5}
height={56}
borderRadius={4}
cursorPointer
directionRow
alignCenter
justifySpaceBetween
onClick={handleClick}
error={error}
data-testid="select-multi-button"
>
<Typography style={{ color: error ? colors.error1 : colors.gray3 }}>
{content.length > 0 ? content : label}
</Typography>
<Box
width={0}
height={0}
borderLeft="5px solid transparent"
borderRight="5px solid transparent"
borderTop={`5px solid ${error ? colors.error1 : 'rgba(0, 0, 0, 0.54)'}`}
/>
</SelectFlex>
<Box mt="3px" ml="14px">
<Typography
variant="body2"
style={{
color: error ? colors.error1 : colors.gray3,
lineHeight: 1.66,
}}
>
{helperText}
</Typography>
</Box>
<Menu
id="select-permissions"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
>
{items.map(item => (
<ItemList
key={item.id}
item={item}
values={values}
onClickItem={onClickItem}
onClickAll={handleClickAll}
subItem={subItem}
/>
))}
</Menu>
</Box>
);
}
SelectMulti.propTypes = {
items: PropTypes.array.isRequired,
data: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
label: PropTypes.string,
subItem: PropTypes.string,
helperText: PropTypes.string,
error: PropTypes.bool,
};
SelectMulti.defaultProps = {
items: [
{
id: 0,
name: 'Item',
subItem: [
{
id: 10,
name: 'SubItem',
},
],
},
],
data: [],
onChange: () => {},
label: '',
subItem: 'subItems',
helperText: '',
error: false,
};
export { SelectMulti };