Advertisement
mattcarlotta

Clustered Select

May 18th, 2019
1,031
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import React, { createRef, useState, useEffect, useCallback } from "react";
  2. import PropTypes from "prop-types";
  3. import styled, { css } from "styled-components";
  4. import { MdChevronRight } from "react-icons/md";
  5.  
  6. const InlineBlockContainer = styled.div`
  7.   width: ${({ width }) => width || "100%"};
  8.   display: inline-block;
  9. `;
  10.  
  11. const ClickHandler = ({ children, handleChange, width }) => {
  12.   const wrapperRef = createRef();
  13.   const [isVisible, setVisible] = useState(false);
  14.  
  15.   const handleClickOutside = useCallback(
  16.     ({ target }) => {
  17.       if (isVisible && wrapperRef && !wrapperRef.current.contains(target)) {
  18.         setVisible(false);
  19.       }
  20.     },
  21.     [isVisible, wrapperRef]
  22.   );
  23.  
  24.   const handleSelectClick = () => {
  25.     setVisible(visible => !visible);
  26.   };
  27.  
  28.   const handleOptionSelect = e => {
  29.     setVisible(false);
  30.     handleChange(e);
  31.   };
  32.  
  33.   useEffect(
  34.     () => {
  35.       document.addEventListener("mousedown", handleClickOutside);
  36.  
  37.       return () => {
  38.         document.removeEventListener("mousedown", handleClickOutside);
  39.       };
  40.     },
  41.     [handleClickOutside]
  42.   );
  43.  
  44.   return (
  45.     <InlineBlockContainer width={width} ref={wrapperRef}>
  46.       {children({
  47.         isVisible,
  48.         handleSelectClick,
  49.         handleOptionSelect
  50.       })}
  51.     </InlineBlockContainer>
  52.   );
  53. };
  54.  
  55. ClickHandler.propTypes = {
  56.   handleChange: PropTypes.func.isRequired,
  57.   children: PropTypes.func.isRequired,
  58.   width: PropTypes.string
  59. };
  60.  
  61. const focused = ({ isVisible }) =>
  62.   isVisible
  63.     ? `outline: -webkit-focus-ring-color auto 1px;
  64.     box-shadow: 0 0 0 1px #2684ff;
  65.     `
  66.     : null;
  67.  
  68. const SelectionContainer = styled.div`
  69.   cursor: pointer;
  70.   display: inline-block;
  71.   height: 40px;
  72.   width: 100%;
  73.   background-color: white;
  74.   display: flex;
  75.   min-height: 38px;
  76.   box-sizing: border-box;
  77.   border-radius: 4px;
  78.   border: 1px solid #bdbdbd;
  79.   transition: all 100ms ease 0s;
  80.   ${props => focused(props)};
  81. `;
  82.  
  83. const StyledSelectText = ({ className, children, handleSelectClick }) => (
  84.   <div className={className} onClick={handleSelectClick}>
  85.     {children}
  86.   </div>
  87. );
  88.  
  89. StyledSelectText.propTypes = {
  90.   className: PropTypes.string.isRequired,
  91.   children: PropTypes.node.isRequired,
  92.   handleSelectClick: PropTypes.func.isRequired
  93. };
  94.  
  95. const SelectText = styled(StyledSelectText)`
  96.   -webkit-box-align: center;
  97.   align-items: center;
  98.   display: flex;
  99.   flex-wrap: wrap;
  100.   position: relative;
  101.   box-sizing: border-box;
  102.   flex: 1 1 0%;
  103.   overflow: hidden;
  104. `;
  105.  
  106. const DisplayOption = styled.div`
  107.   color: #bbb;
  108.   font-size: 16px;
  109.   font-family: "Arial", sans-serif;
  110.   -webkit-box-align: center;
  111.   align-items: center;
  112.   display: flex;
  113.   flex-wrap: wrap;
  114.   position: relative;
  115.   box-sizing: border-box;
  116.   flex: 1 1 0%;
  117.   padding: 2px 8px;
  118.   overflow: hidden;
  119.   ${({ value }) => (value ? "color: #484848 !important;" : null)};
  120. `;
  121.  
  122. const StyledChevron = ({ className }) => (
  123.   <div className={className}>
  124.     <MdChevronRight />
  125.   </div>
  126. );
  127.  
  128. StyledChevron.propTypes = {
  129.   className: PropTypes.string.isRequired
  130. };
  131.  
  132. const Chevron = styled(StyledChevron)`
  133.   display: flex;
  134.   box-sizing: border-box;
  135.   padding: 10px;
  136.  
  137.   svg {
  138.     vertical-align: middle;
  139.     transform: ${({ isVisible }) =>
  140.       isVisible ? "rotate(90deg)" : "rotate(270deg)"};
  141.     transition: 0.2s ease-in-out;
  142.     transition-property: transform;
  143.   }
  144. `;
  145.  
  146. const Selection = ({
  147.   handleSelectClick,
  148.   isVisible,
  149.   placeholder,
  150.   value,
  151.   width
  152. }) => (
  153.   <SelectionContainer isVisible={isVisible} width={width}>
  154.     <SelectText handleSelectClick={handleSelectClick}>
  155.       <DisplayOption value={value}>
  156.         {!value ? placeholder : value}
  157.       </DisplayOption>
  158.       <ChevronIcon isVisible={isVisible} />
  159.     </SelectText>
  160.   </SelectionContainer>
  161. );
  162.  
  163. Selection.propTypes = {
  164.   handleSelectClick: PropTypes.func.isRequired,
  165.   isVisible: PropTypes.bool.isRequired,
  166.   value: PropTypes.string,
  167.   placeholder: PropTypes.string,
  168.   width: PropTypes.string
  169. };
  170.  
  171. const SelectBox = styled.div`
  172.   position: relative;
  173.   box-sizing: border-box;
  174.   width: 100%;
  175. `;
  176.  
  177. const DropContainer = styled.div`
  178.   position: absolute;
  179.   width: 100%;
  180.   z-index: 1000;
  181.   top: 100%;
  182.   background-color: hsl(0, 0%, 100%);
  183.   border-radius: 4px;
  184.   box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1);
  185.   margin-top: 1px;
  186.   box-sizing: border-box;
  187. `;
  188.  
  189. const OptionsContainer = styled.div`
  190.   top: 100%;
  191.   background-color: hsl(0, 0%, 100%);
  192.   border-radius: 4px;
  193.   box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1);
  194.   position: absolute;
  195.   width: 100%;
  196.   z-index: 1000000;
  197.   box-sizing: border-box;
  198. `;
  199.  
  200. const StyledOption = ({ className, onClick, name, value }) => (
  201.   <div
  202.     className={className}
  203.     data-name={name}
  204.     data-value={value}
  205.     onClick={onClick}
  206.   >
  207.     {value}
  208.   </div>
  209. );
  210.  
  211. StyledOption.propTypes = {
  212.   className: PropTypes.string.isRequired,
  213.   onClick: PropTypes.func.isRequired,
  214.   name: PropTypes.string.isRequired,
  215.   value: PropTypes.string.isRequired
  216. };
  217.  
  218. const Option = styled(StyledOption)`
  219.   cursor: pointer;
  220.   color: #282c34;
  221.   display: block;
  222.   word-break: break-all;
  223.   font-size: 16px;
  224.   padding: 8px 12px;
  225.   width: 100%;
  226.   font-weight: normal;
  227.   -webkit-user-select: none;
  228.   -moz-user-select: none;
  229.   -ms-user-select: none;
  230.   user-select: none;
  231.   -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  232.   box-sizing: border-box;
  233.   border-bottom: 1px dotted pink;
  234.   text-align: left;
  235.   ${({ selected, value }) =>
  236.     selected === value ? `background-color: #dedede;  color: #0f7ae5;` : null};
  237.  
  238.   &:hover {
  239.     color: #0f7ae5 !important;
  240.     ${({ selected, value }) =>
  241.       selected !== value ? "background: #eee !important;" : null};
  242.   }
  243. `;
  244.  
  245. const Options = ({
  246.   handleOptionSelect,
  247.   isVisible,
  248.   name,
  249.   selected,
  250.   selectOptions
  251. }) => {
  252.   const onOptionSelect = ({
  253.     target: {
  254.       dataset: { name, value }
  255.     }
  256.   }) => {
  257.     const e = {
  258.       target: {
  259.         name,
  260.         value
  261.       }
  262.     };
  263.     handleOptionSelect(e);
  264.   };
  265.  
  266.   return (
  267.     isVisible && (
  268.       <DropContainer>
  269.         <OptionsContainer>
  270.           {selectOptions.map((value, key) => (
  271.             <Option
  272.               key={key}
  273.               name={name}
  274.               value={value}
  275.               onClick={onOptionSelect}
  276.               selected={selected}
  277.             />
  278.           ))}
  279.         </OptionsContainer>
  280.       </DropContainer>
  281.     )
  282.   );
  283. };
  284.  
  285. Options.propTypes = {
  286.   handleOptionSelect: PropTypes.func.isRequired,
  287.   isVisible: PropTypes.bool.isRequired,
  288.   name: PropTypes.string.isRequired,
  289.   selected: PropTypes.string,
  290.   selectOptions: PropTypes.arrayOf(PropTypes.string.isRequired)
  291. };
  292.  
  293. const Select = ({ name, selectOptions, value, ...props }) => (
  294.   <ClickHandler selectOptions={selectOptions} {...props}>
  295.     {handlers => (
  296.       <SelectContainer>
  297.         <SelectBox>
  298.           <Selection {...handlers} {...props} value={value} />
  299.           <Options
  300.             {...handlers}
  301.             name={name}
  302.             selectOptions={selectOptions}
  303.             selected={value}
  304.           />
  305.         </SelectBox>
  306.       </SelectContainer>
  307.     )}
  308.   </ClickHandler>
  309. );
  310.  
  311. Select.propTypes = {
  312.   name: PropTypes.string.isRequired,
  313.   placeholder: PropTypes.string,
  314.   selectOptions: PropTypes.arrayOf(PropTypes.string.isRequired),
  315.   value: PropTypes.string
  316. };
  317.  
  318. export default Select;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement