iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
1
自我挑戰組

30 天來點 Design System系列 第 25

Day 25 建立 下拉選單

  • 分享至 

  • xImage
  •  

Select

import React, { useState, Fragment, useRef } from "react";
import isEmpty from "lodash/isEmpty";
import styled from "styled-jss";
import propTypes from "prop-types";
import Pop from "../Pop";
import Icon from "../Icon";
import Card from "../Card";
import CardBody from "../CardBody";
import FormGroup from "../FormGroup";
import FromLabel from "../FromLabel";
import Typography from "../Typography";
import ErrorMessage from "../ErrorMessage";

const getBorderStyle = ({ theme, errorMessage, open }) => {
  if (!isEmpty(errorMessage)) {
    return theme.colors.danger;
  }

  if (open) {
    return theme.colors.primary;
  }

  console.log("TCL: getBorderStyle -> theme.colors.grey1", theme.colors.grey1);
  return theme.colors.grey1;
};

const StyledSelect = styled("div")({
  flex: 1,
  display: "flex",
  cursor: "pointer",
  position: 'relative',
  justifyContent: "space-between",
  borderRadius: ({ theme }) => theme.radius,
  fontSize: ({ theme }) => theme.typography.content.fontSize,
  border: ({ theme, errorMessage, open }) =>
    `1px solid ${getBorderStyle({ open, theme, errorMessage })}`,
  color: ({ theme, disabled }) =>
    disabled ? theme.colors.grey2 : theme.colors.black,
  backgroundColor: ({ theme, disabled }) =>
    disabled ? theme.colors.grey0 : theme.colors.white,
});

const StyledTypography = styled(Typography)({
  position: 'absolute',
  padding: ({ theme }) => `${theme.getSpacing(1)}px ${theme.getSpacing(2)}px`,
})

const Options = styled("div")({
  cursor: "pointer",
  padding: ({ theme }) => theme.getSpacing(2),
  "&:hover": {
    backgroundColor: ({ theme }) => theme.colors.primary,
    color: ({ theme }) => theme.colors.white,
  },
});

const OptionBox = styled("div")({
  overflow: "hidden",
  boxShadow: "0px 0px 5px rgba(0,0,0,0.17)",
  borderRadius: ({ theme }) => theme.radius,
  backgroundColor: ({ theme }) => theme.colors.white,
  border: ({ theme }) => `1px solid ${theme.colors.grey1}`,
});

const StyledInput = styled('input')({
  opacity: 0,
  cursor: "pointer",
  padding: ({ theme }) => `${theme.getSpacing(1)}px ${theme.getSpacing(2)}px`,
})

const StyledIcon = styled(Icon)({
  right: 0,
  position: 'absolute',
  padding: ({ theme }) => `${theme.getSpacing(1)}px`,
})

const handleClick = (open, setOpen) => () => {
  setOpen(!open);
};

const handleSelectItem = ({ setValue, setSelectedItem, setOpen, onChange }) => (
  item
) => (e) => {
  setOpen(false);
  setSelectedItem(item);
  onChange(e);
};

const handleOnBlur = (setOpen) => () => {
  setOpen(false);
};

const Select = ({
  items,
  label,
  required,
  onChange,
  placeholder,
  errorMessage,
  ...props
}) => {
  const menuEle = useRef(null);
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState(null);
  const [selectedItem, setSelectedItem] = useState({});

  const onSelectClick = handleClick(open, setOpen);
  const onSelectItem = handleSelectItem({
    setSelectedItem,
    setOpen,
    onChange,
    setValue,
  });
  const onBlurClick = handleOnBlur(setOpen);
  return (
    <Fragment>
      <FormGroup>
        <FromLabel required={required}>{label}</FromLabel>
        <div ref={menuEle} onClick={onSelectClick}>
          <StyledSelect open={open} errorMessage={errorMessage}>
            <StyledTypography>{selectedItem.label || placeholder}</StyledTypography>
            <StyledIcon icon='fa-angle-down' size={12} />
            <StyledInput
              type='text'
              value={value}
              onChange={onChange}
              onBlur={onBlurClick}
            />
          </StyledSelect>
        </div>
        <Pop open={open} anchorEl={menuEle}>
          <OptionBox>
            {items.map((item, index) => {
              const key = `select_item_${item.label}_${item.vale}_${index}`;
              return (
                <Options key={key} onClick={onSelectItem(item)}>
                  {item.label}
                </Options>
              );
            })}
          </OptionBox>
        </Pop>
      </FormGroup>
      <ErrorMessage errorMessage={errorMessage}>{errorMessage}</ErrorMessage>
    </Fragment>
  );
};

Select.propTypes = {
  label: propTypes.string,
  disabled: propTypes.bool,
  onChange: propTypes.func,
  placeholder: propTypes.string,
  items: propTypes.arrayOf(
    propTypes.shape({
      label: propTypes.string,
      value: propTypes.oneOfType([propTypes.number, propTypes.string]),
    })
  ),
};

Select.defaultProps = {
  label: "",
  items: [],
  placeholder: '請選擇...',
  disabled: false,
  onChange: () => false,
};

export default Select;

Usage

import React, { Fragment, useState } from "react";
import theme from "../../lib/theme";
import Select from "../../lib/Select";
import FormControl from "../../lib/FormControl";
import ThemeProvider from "../../lib/ThemeProvider";

const SelectItems = [
  {
    label: "Item 1",
    value: 1,
  },
  {
    label: "Item 2",
    value: 2,
  },
  {
    label: "Item 3",
    value: 3,
  },
];

const handleOnChange = (values, setValue) => ({ target: { value } }) => {
  const newValues = values.includes(value)
    ? values.filter((selectedValue) => selectedValue !== value)
    : [value, ...values];

    setValue(newValues);
};

const Provider = (props) => {
  return <ThemeProvider theme={theme}>{props.children}</ThemeProvider>;
};

const TemplateForm = (args) => {
  const [values, setValues] = useState([]);

  const onChange = handleOnChange(values, setValues);

  return (
    <Provider>
      <Fragment>
        <FormControl>
          <Select {...args} values={values} onChange={onChange} />
        </FormControl>
        <FormControl>
          <Select {...args} name='disabledTest' values={values} onChange={onChange} disabled/>
        </FormControl>
      </Fragment>
    </Provider>
  );
};


const Template = (args) => {
  const [values, setValues] = useState([]);

  const onChange = handleOnChange(values, setValues);

  return (
    <Provider>
      <Fragment>
        <FormControl>
          <Select {...args} values={values} onChange={onChange} />
        </FormControl>
      </Fragment>
    </Provider>
  );
};

export const Default = TemplateForm.bind({});
Default.args = {
  name: "username",
  items: SelectItems,
};

export const LabelSelect = Template.bind({});
LabelSelect.args = {
  name: "labelItems",
  items: SelectItems,
  label: "label",
  required: true,
};

// 你的頁面標題
export default {
  component: Select,
  title: "Form/Select",
};

結果如下:


上一篇
Day 24 建立 Radio & Checkbox
下一篇
Day 26 建立 導覽相關組件
系列文
30 天來點 Design System30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言