Repository: zeit-ui/react (ts)

Issue:
Select component support multiple values
# Feature request 🚀

Multiple selections on `Select`.


### Expected:
  - Overlayer does not close  after select
  - Multiple lines of display text
  - Click the tag to remove the value
  - Divider and group

### Examples:

```jsx
<Select multiple>
  <Select.Option label>America Languages</Select.Option>
  <Select.Option value="1">en-us</Select.Option>
  <Select.Option divider />
  <Select.Option label>Asian Languages</Select.Option>
  <Select.Option value="2">zh-cn</Select.Option>
  <Select.Option value="3">ja-jp</Select.Option>
</Select>
```

### References

1. Material UI Select:

<img width="643" alt="material-select" src="https://user-images.githubusercontent.com/11304944/82559136-a942f900-9ba1-11ea-82ba-eb0d429fa82d.png">

2. The `Select` component of `ZEIT UI` will still not be `input`, this is for better display of custom elements:

<img width="222" alt="zeit-ui-select" src="https://user-images.githubusercontent.com/11304944/82559365-1a82ac00-9ba2-11ea-9409-a949d66d4a44.png">



### Others:

This feature is still planned, welcome to leave any thoughts.



Pull request:
feat(select): handle multiple selections
## PR Checklist

- [x] Fix linting errors
- [x] Label has been added


## Change information

resolved #222 


Patch:
diff --git a/components/select/select-context.ts b/components/select/select-context.ts
index 5ba965f..e67c132 100644
--- a/components/select/select-context.ts
+++ b/components/select/select-context.ts
@@ -2,7 +2,7 @@ import React, { MutableRefObject } from 'react'
 import { NormalSizes } from '../utils/prop-types'
 
 export interface SelectConfig {
-  value?: string
+  value?: string | string[]
   updateValue?: Function
   visible?: boolean
   updateVisible?: Function
diff --git a/components/select/select-multiple-value.tsx b/components/select/select-multiple-value.tsx
new file mode 100644
index 0000000..b74aa99
--- /dev/null
+++ b/components/select/select-multiple-value.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import useTheme from '../styles/use-theme'
+import Grid from '../grid'
+
+interface Props {
+  disabled: boolean
+  size: string
+}
+
+const SelectMultipleValue: React.FC<React.PropsWithChildren<Props>> = ({
+  disabled,
+  size,
+  children,
+}) => {
+  const theme = useTheme()
+
+  return (
+    <Grid>
+      <div className="item">{children}</div>
+      <style jsx>{`
+        .item {
+          display: inline-flex;
+          height: calc(${size} * 2);
+          align-items: center;
+          line-height: 1;
+          padding: 0 0.5rem;
+          font-size: ${size};
+          border-radius: ${theme.layout.radius};
+          background-color: ${theme.palette.accents_2};
+          color: ${disabled ? theme.palette.accents_4 : theme.palette.accents_6};
+        }
+
+        .item > :global(div),
+        .item > :global(div:hover) {
+          border-radius: 0;
+          background-color: transparent;
+          padding: 0;
+          margin: 0;
+          color: inherit;
+        }
+      `}</style>
+    </Grid>
+  )
+}
+
+export default SelectMultipleValue
diff --git a/components/select/select-option.tsx b/components/select/select-option.tsx
index a385601..60be23c 100644
--- a/components/select/select-option.tsx
+++ b/components/select/select-option.tsx
@@ -5,14 +5,18 @@ import { useSelectContext } from './select-context'
 import useWarning from '../utils/use-warning'
 
 interface Props {
-  value: string
+  value?: string
   disabled?: boolean
   className?: string
+  divider?: boolean
+  label?: boolean
   preventAllEvents?: boolean
 }
 
 const defaultProps = {
   disabled: false,
+  divider: false,
+  label: false,
   className: '',
   preventAllEvents: false,
 }
@@ -25,23 +29,37 @@ const SelectOption: React.FC<React.PropsWithChildren<SelectOptionProps>> = ({
   className,
   children,
   disabled,
+  divider,
+  label,
   preventAllEvents,
   ...props
 }) => {
   const theme = useTheme()
   const { updateValue, value, disableAll } = useSelectContext()
   const isDisabled = useMemo(() => disabled || disableAll, [disabled, disableAll])
-  if (identValue === undefined) {
+  const isLabel = useMemo(() => label || divider, [label, divider])
+  if (!isLabel && identValue === undefined) {
     useWarning('The props "value" is required.', 'Select Option')
   }
 
-  const selected = useMemo(() => (value ? identValue === value : false), [identValue, value])
+  const selected = useMemo(() => {
+    if (!value) return false
+    if (typeof value === 'string') {
+      return identValue === value
+    }
+    return value.includes(`${identValue}`)
+  }, [identValue, value])
 
   const bgColor = useMemo(() => {
     if (isDisabled) return theme.palette.accents_1
-    return selected ? theme.palette.accents_1 : theme.palette.background
+    return selected ? theme.palette.accents_2 : theme.palette.background
   }, [selected, isDisabled, theme.palette])
 
+  const hoverBgColor = useMemo(() => {
+    if (isDisabled || isLabel || selected) return bgColor
+    return theme.palette.accents_1
+  }, [selected, isDisabled, theme.palette, isLabel, bgColor])
+
   const color = useMemo(() => {
     if (isDisabled) return theme.palette.accents_4
     return selected ? theme.palette.foreground : theme.palette.accents_5
@@ -52,13 +70,16 @@ const SelectOption: React.FC<React.PropsWithChildren<SelectOptionProps>> = ({
     event.stopPropagation()
     event.nativeEvent.stopImmediatePropagation()
     event.preventDefault()
-    if (isDisabled) return
+    if (isDisabled || isLabel) return
     updateValue && updateValue(identValue)
   }
 
   return (
     <>
-      <div className={`option ${className}`} onClick={clickHandler} {...props}>
+      <div
+        className={`option ${divider ? 'divider' : ''} ${label ? 'label' : ''} ${className}`}
+        onClick={clickHandler}
+        {...props}>
         {children}
       </div>
 
@@ -80,18 +101,27 @@ const SelectOption: React.FC<React.PropsWithChildren<SelectOptionProps>> = ({
           transition: background 0.2s ease 0s, border-color 0.2s ease 0s;
         }
 
-        .option:first-of-type {
-          border-top-left-radius: ${theme.layout.radius};
-          border-top-right-radius: ${theme.layout.radius};
+        .option:hover {
+          background-color: ${hoverBgColor};
+          color: ${theme.palette.accents_7};
         }
 
-        .option:last-of-type {
-          border-bottom-left-radius: ${theme.layout.radius};
-          border-bottom-right-radius: ${theme.layout.radius};
+        .divider {
+          line-height: 0;
+          height: 0;
+          padding: 0;
+          overflow: hidden;
+          border-top: 1px solid ${theme.palette.border};
+          margin: 0.5rem 0;
+          width: 100%;
         }
 
-        .option:hover {
-          background-color: ${theme.palette.accents_1};
+        .label {
+          font-size: 0.875rem;
+          color: ${theme.palette.accents_7};
+          border-bottom: 1px solid ${theme.palette.border};
+          text-transform: capitalize;
+          cursor: default;
         }
       `}</style>
     </>
diff --git a/components/select/select.tsx b/components/select/select.tsx
index 1f51adb..7fd8103 100644
--- a/components/select/select.tsx
+++ b/components/select/select.tsx
@@ -1,24 +1,29 @@
 import React, { useEffect, useMemo, useRef, useState } from 'react'
 import { NormalSizes } from '../utils/prop-types'
+import useTheme from '../styles/use-theme'
 import useClickAway from '../utils/use-click-away'
-import { pickChildByProps, pickChildrenFirst } from '../utils/collections'
+import useCurrentState from '../utils/use-current-state'
+import { pickChildByProps } from '../utils/collections'
 import SelectIcon from './select-icon'
 import SelectOption from './select-option'
-import useTheme from '../styles/use-theme'
 import SelectDropdown from './select-dropdown'
+import SelectMultipleValue from './select-multiple-value'
+import Grid from '../grid'
 import { SelectContext, SelectConfig } from './select-context'
 import { getSizes } from './styles'
 
 interface Props {
   disabled?: boolean
   size?: NormalSizes
-  value?: string
-  initialValue?: string
+  value?: string | string[]
+  initialValue?: string | string[]
   placeholder?: React.ReactNode | string
   icon?: React.ComponentType
-  onChange?: (value: string) => void
+  onChange?: (value: string | string[]) => void
   pure?: boolean
+  multiple?: boolean
   className?: string
+  width?: string
   dropdownClassName?: string
   dropdownStyle?: object
 }
@@ -28,6 +33,8 @@ const defaultProps = {
   size: 'medium' as NormalSizes,
   icon: SelectIcon as React.ComponentType,
   pure: false,
+  multiple: false,
+  width: 'initial',
   className: '',
 }
 
@@ -42,9 +49,11 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
   value: customValue,
   icon: Icon,
   onChange,
-  className,
   pure,
+  multiple,
   placeholder,
+  width,
+  className,
   dropdownClassName,
   dropdownStyle,
   ...props
@@ -52,14 +61,28 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
   const theme = useTheme()
   const ref = useRef<HTMLDivElement>(null)
   const [visible, setVisible] = useState<boolean>(false)
-  const [value, setValue] = useState<string | undefined>(init)
+  const [value, setValue, valueRef] = useCurrentState<string | string[] | undefined>(() => {
+    if (!multiple) return init
+    if (Array.isArray(init)) return init
+    return typeof init === 'undefined' ? [] : [init]
+  })
+  const isEmpty = useMemo(() => {
+    if (!Array.isArray(value)) return !value
+    return value.length === 0
+  }, [value])
   const sizes = useMemo(() => getSizes(theme, size), [theme, size])
 
   const updateVisible = (next: boolean) => setVisible(next)
   const updateValue = (next: string) => {
-    setValue(next)
-    onChange && onChange(next)
-    setVisible(false)
+    setValue(last => {
+      if (!Array.isArray(last)) return next
+      if (!last.includes(next)) return [...last, next]
+      return last.filter(item => item !== next)
+    })
+    onChange && onChange(valueRef.current as string | string[])
+    if (!multiple) {
+      setVisible(false)
+    }
   }
 
   const initialValue: SelectConfig = useMemo(
@@ -72,7 +95,7 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
       ref,
       disableAll: disabled,
     }),
-    [visible, size, disabled, ref],
+    [visible, size, disabled, ref, value, multiple],
   )
 
   const clickHandler = (event: React.MouseEvent<HTMLDivElement>) => {
@@ -84,7 +107,6 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
   }
 
   useClickAway(ref, () => setVisible(false))
-
   useEffect(() => {
     if (customValue === undefined) return
     setValue(customValue)
@@ -92,16 +114,28 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
 
   const selectedChild = useMemo(() => {
     const [, optionChildren] = pickChildByProps(children, 'value', value)
-    const child = pickChildrenFirst(optionChildren)
-    if (!React.isValidElement(child)) return optionChildren
-    return React.cloneElement(child, { preventAllEvents: true })
-  }, [value, children])
+    return React.Children.map(optionChildren, child => {
+      if (!React.isValidElement(child)) return null
+      const el = React.cloneElement(child, { preventAllEvents: true })
+      if (!multiple) return el
+      return (
+        <SelectMultipleValue size={sizes.fontSize} disabled={disabled}>
+          {el}
+        </SelectMultipleValue>
+      )
+    })
+  }, [value, children, multiple])
 
   return (
     <SelectContext.Provider value={initialValue}>
-      <div className={`select ${className}`} ref={ref} onClick={clickHandler} {...props}>
-        {!value && <span className="value placeholder">{placeholder}</span>}
-        {value && <span className="value">{selectedChild}</span>}
+      <div
+        className={`select ${multiple ? 'multiple' : ''} ${className}`}
+        ref={ref}
+        onClick={clickHandler}
+        {...props}>
+        {isEmpty && <span className="value placeholder">{placeholder}</span>}
+        {value && !multiple && <span className="value">{selectedChild}</span>}
+        {value && multiple && <Grid.Container gap={0.5}>{selectedChild}</Grid.Container>}
         <SelectDropdown
           visible={visible}
           className={dropdownClassName}
@@ -122,7 +156,7 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
             position: relative;
             cursor: ${disabled ? 'not-allowed' : 'pointer'};
             max-width: 80vw;
-            width: initial;
+            width: ${width};
             overflow: hidden;
             transition: border 0.2s ease 0s, color 0.2s ease-out 0s, box-shadow 0.2s ease 0s;
             border: 1px solid ${theme.palette.border};
@@ -133,6 +167,13 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
             background-color: ${disabled ? theme.palette.accents_1 : theme.palette.background};
           }
 
+          .multiple {
+            height: auto;
+            min-height: ${sizes.height};
+            padding: ${theme.layout.gapQuarter} calc(${sizes.fontSize} * 2)
+              ${theme.layout.gapQuarter} ${theme.layout.gapHalf};
+          }
+
           .select:hover {
             border-color: ${disabled ? theme.palette.border : theme.palette.foreground};
           }
diff --git a/components/shared/dropdown.tsx b/components/shared/dropdown.tsx
index d400e31..b8442cb 100644
--- a/components/shared/dropdown.tsx
+++ b/components/shared/dropdown.tsx
@@ -4,6 +4,7 @@ import usePortal from '../utils/use-portal'
 import useResize from '../utils/use-resize'
 import CSSTransition from './css-transition'
 import useClickAnyWhere from '../utils/use-click-anywhere'
+import useDOMObserver from '../utils/use-dom-observer'
 
 interface Props {
   parent?: MutableRefObject<HTMLElement | null> | undefined
@@ -53,6 +54,9 @@ const Dropdown: React.FC<React.PropsWithChildren<Props>> = React.memo(
       if (!shouldUpdatePosition) return
       updateRect()
     })
+    useDOMObserver(parent, () => {
+      updateRect()
+    })
     useEffect(() => {
       if (!parent || !parent.current) return
       parent.current.addEventListener('mouseenter', updateRect)
diff --git a/components/use-current-state/use-current-state.ts b/components/use-current-state/use-current-state.ts
index d0a60af..0d8dce6 100644
--- a/components/use-current-state/use-current-state.ts
+++ b/components/use-current-state/use-current-state.ts
@@ -2,8 +2,10 @@ import { Dispatch, MutableRefObject, SetStateAction, useEffect, useRef, useState
 
 export type CurrentStateType<S> = [S, Dispatch<SetStateAction<S>>, MutableRefObject<S>]
 
-const useCurrentState = <S>(initialState: S): CurrentStateType<S> => {
-  const [state, setState] = useState<S>(initialState as S)
+const useCurrentState = <S>(initialState: S | (() => S)): CurrentStateType<S> => {
+  const [state, setState] = useState<S>(() => {
+    return typeof initialState === 'function' ? (initialState as () => S)() : initialState
+  })
   const ref = useRef<S>(initialState as S)
 
   useEffect(() => {
diff --git a/components/utils/collections.ts b/components/utils/collections.ts
index d5ae19f..a6cb23e 100644
--- a/components/utils/collections.ts
+++ b/components/utils/collections.ts
@@ -38,9 +38,17 @@ export const pickChildByProps = (
   value: any,
 ): [ReactNode | undefined, ReactNode | undefined] => {
   let target: ReactNode[] = []
+  const isArray = Array.isArray(value)
   const withoutPropChildren = React.Children.map(children, item => {
     if (!React.isValidElement(item)) return null
     if (!item.props) return item
+    if (isArray) {
+      if (value.includes(item.props[key])) {
+        target.push(item)
+        return null
+      }
+      return item
+    }
     if (item.props[key] === value) {
       target.push(item)
       return null
diff --git a/components/utils/use-dom-observer.ts b/components/utils/use-dom-observer.ts
new file mode 100644
index 0000000..258fb2a
--- /dev/null
+++ b/components/utils/use-dom-observer.ts
@@ -0,0 +1,25 @@
+import { MutableRefObject, useEffect } from 'react'
+
+const useDOMObserver = (
+  ref: MutableRefObject<HTMLElement | null> | undefined,
+  callback: MutationCallback = () => {},
+) => {
+  const config = { attributes: false, childList: true, subtree: true }
+
+  useEffect(() => {
+    if (!ref || !ref.current) return
+    let unmount = false
+    const done: MutationCallback = (...params) => {
+      if (unmount) return
+      callback(...params)
+    }
+    const observer = new MutationObserver(done)
+    observer.observe(ref.current, config)
+    return () => {
+      unmount = true
+      observer.disconnect()
+    }
+  }, [ref])
+}
+
+export default useDOMObserver
diff --git a/pages/en-us/components/select.mdx b/pages/en-us/components/select.mdx
index 904bc7c..3d13615 100644
--- a/pages/en-us/components/select.mdx
+++ b/pages/en-us/components/select.mdx
@@ -58,6 +58,52 @@ Display a dropdown list of items.
 </Select>
 `} />
 
+<Playground
+  title="Labels"
+  desc="show data as a group."
+  scope={{ Select }}
+  code={`
+<Select placeholder="Frameworks">
+  <Select.Option label>JavaScript</Select.Option>
+  <Select.Option value="react">React</Select.Option>
+  <Select.Option value="angular">Angular</Select.Option>
+  <Select.Option label>Ruby</Select.Option>
+  <Select.Option value="rails">Rails</Select.Option>
+  <Select.Option value="sinatra">Sinatra</Select.Option>
+</Select>
+`} />
+
+<Playground
+  title="Divider"
+  scope={{ Select }}
+  code={`
+<Select placeholder="Frameworks">
+  <Select.Option value="react">React</Select.Option>
+  <Select.Option value="angular">Angular</Select.Option>
+  <Select.Option divider />
+  <Select.Option value="rails">Rails</Select.Option>
+  <Select.Option value="sinatra">Sinatra</Select.Option>
+</Select>
+`} />
+
+<Playground
+  title="Multiple"
+  desc="`Select` supports multiple values to be selected."
+  scope={{ Select }}
+  code={`
+<Select placeholder="Frameworks" multiple width="200px" initialValue={['1', '3', '4', '6']}>
+  <Select.Option value="1">React</Select.Option>
+  <Select.Option value="2">Angular</Select.Option>
+  <Select.Option value="3">Vue</Select.Option>
+  <Select.Option divider />
+  <Select.Option value="4">Rails</Select.Option>
+  <Select.Option value="5">Sinatra</Select.Option>
+  <Select.Option divider />
+  <Select.Option value="6">Express</Select.Option>
+  <Select.Option value="7">Koa</Select.Option>
+</Select>
+`} />
+
 <Playground
   title="Compose"
   desc="Use with other components."
@@ -69,20 +115,21 @@ Display a dropdown list of items.
 </Select>
 `} />
 
-
 <Attributes edit="/pages/en-us/components/select.mdx">
 <Attributes.Title>Select.Props</Attributes.Title>
 
 | Attribute | Description | Type | Accepted values | Default
 | ---------- | ---------- | ---- |  -------------- | ------ |
-| **value** | selected value | `string` | - | - |
-| **initialValue** | initial value | `string` | - | - |
+| **value** | selected value | `string`, `string[]` | - | - |
+| **initialValue** | initial value | `string`, `string[]` | - | - |
 | **placeholder** | placeholder string | `string` | - | - |
-| **size** | select component size | `NormalSizes` | `'mini', 'small', 'medium', 'large'` | `medium` |
+| **width** | css width value of select | `string` | - | `initial` |
+| **size** | select component size | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
 | **icon** | icon component | `ComponentType` | - | `SVG Component` |
 | **pure** | remove icon component | `boolean` | - | `false` |
+| **multiple** | support multiple selection | `boolean` | - | `false` |
 | **disabled** | disable current radio | `boolean` | - | `false` |
-| **onChange** | selected value | `(val: string) => void` | - | - |
+| **onChange** | selected value | <Code>(val: string &#124; string[]) => void </Code> | - | - |
 | **dropdownClassName** | className of dropdown menu | `string` | - | - |
 | **dropdownStyle** | style of dropdown menu | `object` | - | - |
 | ... | native props | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
@@ -95,6 +142,12 @@ Display a dropdown list of items.
 | **disabled** | disable current option | `boolean` | - | `false` |
 | ... | native props | `HTMLAttributes` | `'name', 'id', 'className', ...` | - |
 
+<Attributes.Title>NormalSizes</Attributes.Title>
+
+```ts
+type NormalSizes = 'mini' | 'small' | 'medium' | 'large'
+```
+
 </Attributes>
 
 export default ({ children }) => <Layout meta={meta}>{children}</Layout>
diff --git a/pages/en-us/components/use-current-state.mdx b/pages/en-us/components/use-current-state.mdx
index e234533..100ee76 100644
--- a/pages/en-us/components/use-current-state.mdx
+++ b/pages/en-us/components/use-current-state.mdx
@@ -46,7 +46,7 @@ type CurrentStateType<S> = [
 ]
 
 const useCurrentState<S> = (
-  initialState: S,
+  initialState: S | () => S,
 ) => CurrentStateType<S>
 ```
 
diff --git a/pages/zh-cn/components/select.mdx b/pages/zh-cn/components/select.mdx
index c09ec77..c9ce40d 100644
--- a/pages/zh-cn/components/select.mdx
+++ b/pages/zh-cn/components/select.mdx
@@ -57,6 +57,53 @@ export const meta = {
 </Select>
 `} />
 
+<Playground
+  title="标签"
+  desc="用标签将选项分组。"
+  scope={{ Select }}
+  code={`
+<Select placeholder="选择框架">
+  <Select.Option label>JavaScript</Select.Option>
+  <Select.Option value="react">React</Select.Option>
+  <Select.Option value="angular">Angular</Select.Option>
+  <Select.Option label>Ruby</Select.Option>
+  <Select.Option value="rails">Rails</Select.Option>
+  <Select.Option value="sinatra">Sinatra</Select.Option>
+</Select>
+`} />
+
+<Playground
+  title="分割线"
+  desc="在选项中加入修饰用的线条。"
+  scope={{ Select }}
+  code={`
+<Select placeholder="选择框架">
+  <Select.Option value="react">React</Select.Option>
+  <Select.Option value="angular">Angular</Select.Option>
+  <Select.Option divider />
+  <Select.Option value="rails">Rails</Select.Option>
+  <Select.Option value="sinatra">Sinatra</Select.Option>
+</Select>
+`} />
+
+<Playground
+  title="多选"
+  desc="Select 组件支持同时选中多个值。"
+  scope={{ Select }}
+  code={`
+<Select placeholder="选择框架" multiple width="200px" initialValue={['1', '3', '4', '6']}>
+  <Select.Option value="1">React</Select.Option>
+  <Select.Option value="2">Angular</Select.Option>
+  <Select.Option value="3">Vue</Select.Option>
+  <Select.Option divider />
+  <Select.Option value="4">Rails</Select.Option>
+  <Select.Option value="5">Sinatra</Select.Option>
+  <Select.Option divider />
+  <Select.Option value="6">Express</Select.Option>
+  <Select.Option value="7">Koa</Select.Option>
+</Select>
+`} />
+
 <Playground
   title="组合"
   desc="与其他组件组合使用。"
@@ -74,14 +121,16 @@ export const meta = {
 
 | 属性 | 描述 | 类型 | 推荐值 | 默认
 | ---------- | ---------- | ---- |  -------------- | ------ |
-| **value** | 手动设置选择器的值 | `string` | - | - |
-| **initialValue** | 选择器初始值 | `string` | - | - |
+| **value** | 手动设置选择器的值 | `string`, `string[]` | - | - |
+| **initialValue** | 选择器初始值 | `string`, `string[]` | - | - |
 | **placeholder** | 占位文本内容 | `string` | - | - |
-| **size** | 选择器组件大小 | `NormalSizes` | `'mini', 'small', 'medium', 'large'` | `medium` |
+| **width** | 组件的 CSS 宽度值 | `string` | - | `initial` |
+| **size** | 选择器组件大小 | `NormalSizes` | [NormalSizes](#normalsizes) | `medium` |
 | **icon** | 右侧图标组件 | `ComponentType` | - | `SVG Component` |
 | **pure** | 隐藏右侧图标组件 | `boolean` | - | `false` |
+| **multiple** | 是否支持多选 | `boolean` | - | `false` |
 | **disabled** | 禁用所有的交互 | `boolean` | - | `false` |
-| **onChange** | 选项被选中所触发的事件 | `(val: string) => void` | - | - |
+| **onChange** | 选项被选中所触发的事件 | <Code>(val: string &#124; string[]) => void </Code> | - | - |
 | **dropdownClassName** | 下拉框的自定义类名 | `string` | - | - |
 | **dropdownStyle** | 下拉框的自定义样式 | `object` | - | - |
 | ... | 原生属性 | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
@@ -94,6 +143,12 @@ export const meta = {
 | **disabled** | 禁用当前选项 | `boolean` | - | `false` |
 | ... | 原生属性 | `HTMLAttributes` | `'name', 'id', 'className', ...` | - |
 
+<Attributes.Title>NormalSizes</Attributes.Title>
+
+```ts
+type NormalSizes = 'mini' | 'small' | 'medium' | 'large'
+```
+
 </Attributes>
 
 export default ({ children }) => <Layout meta={meta}>{children}</Layout>
diff --git a/pages/zh-cn/components/use-current-state.mdx b/pages/zh-cn/components/use-current-state.mdx
index 559fa16..ccd11bf 100644
--- a/pages/zh-cn/components/use-current-state.mdx
+++ b/pages/zh-cn/components/use-current-state.mdx
@@ -44,7 +44,7 @@ type CurrentStateType<S> = [
 ]
 
 const useCurrentState<S> = (
-  initialState: S,
+  initialState: S | () => S,
 ) => CurrentStateType<S>
 ```
 


Test patch:
diff --git a/components/select/__tests__/__snapshots__/index.test.tsx.snap b/components/select/__tests__/__snapshots__/index.test.tsx.snap
index d26a3e5..0af4cc3 100644
--- a/components/select/__tests__/__snapshots__/index.test.tsx.snap
+++ b/components/select/__tests__/__snapshots__/index.test.tsx.snap
@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Select should render correctly 1`] = `
-"<div><div class=\\"select \\"><span class=\\"value placeholder\\"></span><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
+"<div><div class=\\"select  \\"><span class=\\"value placeholder\\"></span><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
         svg {
           color: inherit;
           stroke: currentColor;
@@ -27,6 +27,13 @@ exports[`Select should render correctly 1`] = `
             background-color: #fff;
           }
 
+          .multiple {
+            height: auto;
+            min-height: calc(1.688 * 16pt);
+            padding: 4pt calc(.875rem * 2)
+              4pt 8pt;
+          }
+
           .select:hover {
             border-color: #000;
           }
@@ -74,7 +81,7 @@ exports[`Select should render correctly 1`] = `
             align-items: center;
             color: #666;
           }
-        </style></div><div class=\\"select \\"><span class=\\"value placeholder\\"></span><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
+        </style></div><div class=\\"select  \\"><span class=\\"value placeholder\\"></span><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
         svg {
           color: inherit;
           stroke: currentColor;
@@ -100,6 +107,13 @@ exports[`Select should render correctly 1`] = `
             background-color: #fff;
           }
 
+          .multiple {
+            height: auto;
+            min-height: calc(1 * 16pt);
+            padding: 4pt calc(.75rem * 2)
+              4pt 8pt;
+          }
+
           .select:hover {
             border-color: #000;
           }
@@ -147,7 +161,7 @@ exports[`Select should render correctly 1`] = `
             align-items: center;
             color: #666;
           }
-        </style></div><div class=\\"select \\"><span class=\\"value placeholder\\"></span><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
+        </style></div><div class=\\"select  \\"><span class=\\"value placeholder\\"></span><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
         svg {
           color: inherit;
           stroke: currentColor;
@@ -173,6 +187,13 @@ exports[`Select should render correctly 1`] = `
             background-color: #fff;
           }
 
+          .multiple {
+            height: auto;
+            min-height: calc(1.344 * 16pt);
+            padding: 4pt calc(.75rem * 2)
+              4pt 8pt;
+          }
+
           .select:hover {
             border-color: #000;
           }
@@ -220,7 +241,7 @@ exports[`Select should render correctly 1`] = `
             align-items: center;
             color: #666;
           }
-        </style></div><div class=\\"select \\"><span class=\\"value placeholder\\"></span><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
+        </style></div><div class=\\"select  \\"><span class=\\"value placeholder\\"></span><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
         svg {
           color: inherit;
           stroke: currentColor;
@@ -246,6 +267,13 @@ exports[`Select should render correctly 1`] = `
             background-color: #fff;
           }
 
+          .multiple {
+            height: auto;
+            min-height: calc(2 * 16pt);
+            padding: 4pt calc(1.225rem * 2)
+              4pt 8pt;
+          }
+
           .select:hover {
             border-color: #000;
           }
@@ -296,11 +324,95 @@ exports[`Select should render correctly 1`] = `
         </style></div></div>"
 `;
 
+exports[`Select should work correctly with labels 1`] = `
+"<div class=\\"select  \\"><span class=\\"value placeholder\\"></span><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
+        svg {
+          color: inherit;
+          stroke: currentColor;
+          transition: all 200ms ease;
+        }
+      </style></svg></div><style>
+          .select {
+            display: inline-flex;
+            align-items: center;
+            user-select: none;
+            white-space: nowrap;
+            position: relative;
+            cursor: pointer;
+            max-width: 80vw;
+            width: initial;
+            overflow: hidden;
+            transition: border 0.2s ease 0s, color 0.2s ease-out 0s, box-shadow 0.2s ease 0s;
+            border: 1px solid #eaeaea;
+            border-radius: 5px;
+            padding: 0 4pt 0 8pt;
+            height: calc(1.688 * 16pt);
+            min-width: 10rem;
+            background-color: #fff;
+          }
+
+          .multiple {
+            height: auto;
+            min-height: calc(1.688 * 16pt);
+            padding: 4pt calc(.875rem * 2)
+              4pt 8pt;
+          }
+
+          .select:hover {
+            border-color: #000;
+          }
+
+          .select:hover .icon {
+            color: #000;
+          }
+
+          .value {
+            display: inline-flex;
+            flex: 1;
+            height: 100%;
+            align-items: center;
+            line-height: 1;
+            padding: 0;
+            margin-right: 1.25rem;
+            font-size: .875rem;
+            color: #000;
+            width: calc(100% - 1.25rem);
+          }
+
+          .value > :global(div),
+          .value > :global(div:hover) {
+            border-radius: 0;
+            background-color: transparent;
+            padding: 0;
+            margin: 0;
+            color: inherit;
+          }
+
+          .placeholder {
+            color: #999;
+          }
+
+          .icon {
+            position: absolute;
+            right: 4pt;
+            font-size: .875rem;
+            top: 50%;
+            bottom: 0;
+            transform: translateY(-50%) rotate(0deg);
+            pointer-events: none;
+            transition: transform 200ms ease;
+            display: flex;
+            align-items: center;
+            color: #666;
+          }
+        </style></div>"
+`;
+
 exports[`Select should work with different icons 1`] = `
 initialize {
   "0": Object {
     "attribs": Object {
-      "class": "select ",
+      "class": "select  ",
     },
     "children": Array [
       Object {
@@ -334,6 +446,13 @@ initialize {
             background-color: #fff;
           }
 
+          .multiple {
+            height: auto;
+            min-height: calc(1.688 * 16pt);
+            padding: 4pt calc(.875rem * 2)
+              4pt 8pt;
+          }
+
           .select:hover {
             border-color: #000;
           }
@@ -431,6 +550,13 @@ initialize {
             background-color: #fff;
           }
 
+          .multiple {
+            height: auto;
+            min-height: calc(1.688 * 16pt);
+            padding: 4pt calc(.875rem * 2)
+              4pt 8pt;
+          }
+
           .select:hover {
             border-color: #000;
           }
@@ -554,7 +680,7 @@ exports[`Select should work with different icons 2`] = `
 initialize {
   "0": Object {
     "attribs": Object {
-      "class": "select ",
+      "class": "select  ",
     },
     "children": Array [
       Object {
@@ -616,6 +742,13 @@ initialize {
             background-color: #fff;
           }
 
+          .multiple {
+            height: auto;
+            min-height: calc(1.688 * 16pt);
+            padding: 4pt calc(.875rem * 2)
+              4pt 8pt;
+          }
+
           .select:hover {
             border-color: #000;
           }
@@ -751,6 +884,13 @@ initialize {
             background-color: #fff;
           }
 
+          .multiple {
+            height: auto;
+            min-height: calc(1.688 * 16pt);
+            padding: 4pt calc(.875rem * 2)
+              4pt 8pt;
+          }
+
           .select:hover {
             border-color: #000;
           }
@@ -865,6 +1005,13 @@ initialize {
             background-color: #fff;
           }
 
+          .multiple {
+            height: auto;
+            min-height: calc(1.688 * 16pt);
+            padding: 4pt calc(.875rem * 2)
+              4pt 8pt;
+          }
+
           .select:hover {
             border-color: #000;
           }
diff --git a/components/select/__tests__/__snapshots__/multiple.test.tsx.snap b/components/select/__tests__/__snapshots__/multiple.test.tsx.snap
new file mode 100644
index 0000000..94b950f
--- /dev/null
+++ b/components/select/__tests__/__snapshots__/multiple.test.tsx.snap
@@ -0,0 +1,150 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Select Multiple should render correctly 1`] = `
+"<div class=\\"select multiple \\"><span class=\\"value placeholder\\"></span><div class=\\"item  mock \\"><style>
+        .item {
+        }
+
+        .justify {
+          justify-content: undefined;
+        }
+
+        .direction {
+          flex-direction: undefined;
+        }
+
+        .alignContent {
+          align-content: undefined;
+        }
+
+        .alignItems {
+          align-items: undefined;
+        }
+
+        .xs {
+          flex-grow: 1;
+          max-width: 100%;
+          flex-basis: 0;
+        }
+
+        @media only screen and (max-width: 650px) {
+          .xs {
+            flex-grow: 1;
+            max-width: 100%;
+            flex-basis: 0;
+          }
+        }
+
+        @media only screen and (min-width: 650px) {
+          .sm {
+            flex-grow: 1;
+            max-width: 100%;
+            flex-basis: 0;
+          }
+        }
+
+        @media only screen and (min-width: 900px) {
+          .md {
+            flex-grow: 1;
+            max-width: 100%;
+            flex-basis: 0;
+          }
+        }
+
+        @media only screen and (min-width: 1280px) {
+          .lg {
+            flex-grow: 1;
+            max-width: 100%;
+            flex-basis: 0;
+          }
+        }
+
+        @media only screen and (min-width: 1920px) {
+          .xl {
+            flex-grow: 1;
+            max-width: 100%;
+            flex-basis: 0;
+          }
+        }
+      </style></div><div class=\\"icon\\"><svg viewBox=\\"0 0 24 24\\" width=\\"1.25em\\" height=\\"1.25em\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" fill=\\"none\\" shape-rendering=\\"geometricPrecision\\"><path d=\\"M6 9l6 6 6-6\\"></path><style>
+        svg {
+          color: inherit;
+          stroke: currentColor;
+          transition: all 200ms ease;
+        }
+      </style></svg></div><style>
+          .select {
+            display: inline-flex;
+            align-items: center;
+            user-select: none;
+            white-space: nowrap;
+            position: relative;
+            cursor: pointer;
+            max-width: 80vw;
+            width: initial;
+            overflow: hidden;
+            transition: border 0.2s ease 0s, color 0.2s ease-out 0s, box-shadow 0.2s ease 0s;
+            border: 1px solid #eaeaea;
+            border-radius: 5px;
+            padding: 0 4pt 0 8pt;
+            height: calc(1.688 * 16pt);
+            min-width: 10rem;
+            background-color: #fff;
+          }
+
+          .multiple {
+            height: auto;
+            min-height: calc(1.688 * 16pt);
+            padding: 4pt calc(.875rem * 2)
+              4pt 8pt;
+          }
+
+          .select:hover {
+            border-color: #000;
+          }
+
+          .select:hover .icon {
+            color: #000;
+          }
+
+          .value {
+            display: inline-flex;
+            flex: 1;
+            height: 100%;
+            align-items: center;
+            line-height: 1;
+            padding: 0;
+            margin-right: 1.25rem;
+            font-size: .875rem;
+            color: #000;
+            width: calc(100% - 1.25rem);
+          }
+
+          .value > :global(div),
+          .value > :global(div:hover) {
+            border-radius: 0;
+            background-color: transparent;
+            padding: 0;
+            margin: 0;
+            color: inherit;
+          }
+
+          .placeholder {
+            color: #999;
+          }
+
+          .icon {
+            position: absolute;
+            right: 4pt;
+            font-size: .875rem;
+            top: 50%;
+            bottom: 0;
+            transform: translateY(-50%) rotate(0deg);
+            pointer-events: none;
+            transition: transform 200ms ease;
+            display: flex;
+            align-items: center;
+            color: #666;
+          }
+        </style></div>"
+`;
diff --git a/components/select/__tests__/index.test.tsx b/components/select/__tests__/index.test.tsx
index cb759ce..eedb141 100644
--- a/components/select/__tests__/index.test.tsx
+++ b/components/select/__tests__/index.test.tsx
@@ -17,6 +17,19 @@ describe('Select', () => {
     expect(() => wrapper.unmount()).not.toThrow()
   })
 
+  it('should work correctly with labels', () => {
+    const wrapper = mount(
+      <Select>
+        <Select.Option label>1</Select.Option>
+        <Select.Option divider>1</Select.Option>
+        <Select.Option value="1">1</Select.Option>
+        <Select.Option value="2">Option 2</Select.Option>
+      </Select>,
+    )
+    expect(wrapper.html()).toMatchSnapshot()
+    expect(() => wrapper.unmount()).not.toThrow()
+  })
+
   it('should work with different icons', () => {
     const MockIcon = () => <span>icon</span>
     const pure = render(<Select pure />)
diff --git a/components/select/__tests__/multiple.test.tsx b/components/select/__tests__/multiple.test.tsx
new file mode 100644
index 0000000..cc413ee
--- /dev/null
+++ b/components/select/__tests__/multiple.test.tsx
@@ -0,0 +1,55 @@
+import React from 'react'
+import { mount } from 'enzyme'
+import { Select } from 'components'
+import { nativeEvent, updateWrapper } from 'tests/utils'
+
+describe('Select Multiple', () => {
+  it('should render correctly', () => {
+    const wrapper = mount(
+      <Select multiple>
+        <Select.Option value="1">1</Select.Option>
+        <Select.Option value="2">Option 2</Select.Option>
+      </Select>,
+    )
+    expect(wrapper.html()).toMatchSnapshot()
+    expect(() => wrapper.unmount()).not.toThrow()
+  })
+
+  it('should render value with initial-value', () => {
+    const wrapper = mount(
+      <Select initialValue={['1', '2']} multiple>
+        <Select.Option value="1">test-1</Select.Option>
+        <Select.Option value="2">test-2</Select.Option>
+        <Select.Option value="3">test-3</Select.Option>
+      </Select>,
+    )
+    expect(wrapper.find('.option').length).toBeGreaterThan(1)
+    const text = wrapper.find('.option').map(item => item.text())
+    expect(text.includes('test-1')).toBeTruthy()
+    expect(text.includes('test-2')).toBeTruthy()
+    expect(text.includes('test-3')).not.toBeTruthy()
+  })
+
+  it('should trigger events when option changed', async () => {
+    let value = ''
+    const changeHandler = jest.fn().mockImplementation(val => (value = val))
+    const wrapper = mount(
+      <Select onChange={changeHandler} multiple>
+        <Select.Option value="1">1</Select.Option>
+        <Select.Option value="2">Option 2</Select.Option>
+      </Select>,
+    )
+    wrapper.find('.select').simulate('click', nativeEvent)
+    wrapper.find('.select-dropdown').find('.option').at(0).simulate('click', nativeEvent)
+    await updateWrapper(wrapper, 350)
+    expect(changeHandler).toHaveBeenCalled()
+    expect(Array.isArray(value)).toBeTruthy()
+    expect(value.includes('1')).toBeTruthy()
+
+    wrapper.find('.select-dropdown').find('.option').at(0).simulate('click', nativeEvent)
+    await updateWrapper(wrapper, 350)
+    expect(Array.isArray(value)).toBeTruthy()
+    expect(value.includes('1')).not.toBeTruthy()
+    changeHandler.mockRestore()
+  })
+})
diff --git a/components/use-current-state/__tests__/current-state.test.tsx b/components/use-current-state/__tests__/current-state.test.tsx
index 8f90c12..f65ca1f 100644
--- a/components/use-current-state/__tests__/current-state.test.tsx
+++ b/components/use-current-state/__tests__/current-state.test.tsx
@@ -13,6 +13,14 @@ describe('UseCurrentState', () => {
     expect(result.current[2].current).toEqual('test')
   })
 
+  it('functional initial mode should be supported', () => {
+    const { result } = renderHook(() => useCurrentState(() => 1 + 1))
+    expect(result.current[0]).toEqual(2)
+    act(() => result.current[1](0))
+    expect(result.current[0]).toEqual(0)
+    expect(result.current[2].current).toEqual(0)
+  })
+
   it('functional update mode should be supported', () => {
     const { result } = renderHook(() => useCurrentState(0))
     expect(result.current[0]).toEqual(0)