mirror of
https://github.com/zhigang1992/react.git
synced 2026-03-26 06:55:07 +08:00
feat(select): add clearable option to select multiple with test and english doc (#396)
* docs: add clearable option to select multiple with test and english doc * fix: fix types for onClear * fix: fix import path for use-theme add more test for coverage * docs(select): add chinese document Co-authored-by: unix <unix.bio@gmail.com>
This commit is contained in:
@@ -15,6 +15,26 @@ describe('Select Multiple', () => {
|
||||
expect(() => wrapper.unmount()).not.toThrow()
|
||||
})
|
||||
|
||||
it('should render correctly with clearable option', () => {
|
||||
const wrapper = mount(
|
||||
<Select multiple initialValue={['1']}>
|
||||
<Select.Option value="1">1</Select.Option>
|
||||
<Select.Option value="2">Option 2</Select.Option>
|
||||
</Select>,
|
||||
)
|
||||
expect(wrapper.find('.clear-icon').length).toBe(1)
|
||||
})
|
||||
|
||||
it('should render correctly without clearable option', () => {
|
||||
const wrapper = mount(
|
||||
<Select multiple clearable={false} initialValue={['1']}>
|
||||
<Select.Option value="1">1</Select.Option>
|
||||
<Select.Option value="2">Option 2</Select.Option>
|
||||
</Select>,
|
||||
)
|
||||
expect(wrapper.find('.clear-icon').length).toBe(0)
|
||||
})
|
||||
|
||||
it('should render value with initial-value', () => {
|
||||
const wrapper = mount(
|
||||
<Select initialValue={['1', '2']} multiple>
|
||||
@@ -56,3 +76,18 @@ describe('Select Multiple', () => {
|
||||
changeHandler.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
it('should trigger event correctly when clicked', async () => {
|
||||
const changeHandler = jest.fn()
|
||||
const wrapper = mount(
|
||||
<Select onChange={changeHandler} multiple initialValue={['1']}>
|
||||
<Select.Option value="1">1</Select.Option>
|
||||
<Select.Option value="2">Option 2</Select.Option>
|
||||
</Select>,
|
||||
)
|
||||
expect(wrapper.find('.clear-icon').length).toBe(1)
|
||||
wrapper.find('.clear-icon').simulate('click', nativeEvent)
|
||||
await updateWrapper(wrapper, 350)
|
||||
expect(changeHandler).toHaveBeenCalled()
|
||||
expect(wrapper.find('.clear-icon').length).toBe(0)
|
||||
})
|
||||
|
||||
66
components/select/select-icon-clear.tsx
Normal file
66
components/select/select-icon-clear.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import useTheme from '../use-theme'
|
||||
|
||||
interface Props {
|
||||
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void
|
||||
heightRatio?: string | undefined
|
||||
}
|
||||
|
||||
const SelectIconClear: React.FC<Props> = ({ onClick, heightRatio }) => {
|
||||
const theme = useTheme()
|
||||
const width = useMemo(() => {
|
||||
return heightRatio ? `calc(10.66px * ${heightRatio})` : '18px'
|
||||
}, [heightRatio])
|
||||
const clickHandler = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.nativeEvent.stopImmediatePropagation()
|
||||
onClick && onClick(event)
|
||||
}
|
||||
return (
|
||||
<div onClick={clickHandler} className="clear-icon">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
shapeRendering="geometricPrecision">
|
||||
<path d="M18 6L6 18" />
|
||||
<path d="M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
<style jsx>{`
|
||||
.clear-icon {
|
||||
padding: 0;
|
||||
padding-left: ${theme.layout.gapQuarter};
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition: color 150ms ease 0s;
|
||||
color: ${theme.palette.accents_3};
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.clear-icon:hover {
|
||||
color: ${theme.palette.foreground};
|
||||
}
|
||||
|
||||
svg {
|
||||
color: currentColor;
|
||||
width: ${width};
|
||||
height: ${width};
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MemoSelectIconClear = React.memo(SelectIconClear)
|
||||
|
||||
export default MemoSelectIconClear
|
||||
@@ -1,26 +1,33 @@
|
||||
import React from 'react'
|
||||
import useTheme from '../use-theme'
|
||||
import Grid from '../grid'
|
||||
import SelectClearIcon from './select-icon-clear'
|
||||
|
||||
interface Props {
|
||||
disabled: boolean
|
||||
onClear: (() => void) | null
|
||||
size: string
|
||||
}
|
||||
|
||||
const SelectMultipleValue: React.FC<React.PropsWithChildren<Props>> = ({
|
||||
disabled,
|
||||
size,
|
||||
onClear,
|
||||
children,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<div className="item">{children}</div>
|
||||
<div className="item">
|
||||
{children}
|
||||
{!!onClear && <SelectClearIcon heightRatio="1.5" onClick={onClear} />}
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.item {
|
||||
display: inline-flex;
|
||||
height: calc(${size} * 2);
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
padding: 0 0.5rem;
|
||||
@@ -30,8 +37,8 @@ const SelectMultipleValue: React.FC<React.PropsWithChildren<Props>> = ({
|
||||
color: ${disabled ? theme.palette.accents_4 : theme.palette.accents_6};
|
||||
}
|
||||
|
||||
.item > :global(div),
|
||||
.item > :global(div:hover) {
|
||||
.item > :global(div:not(.clear-icon)),
|
||||
.item > :global(div:not(.clear-icon):hover) {
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Props {
|
||||
onChange?: (value: string | string[]) => void
|
||||
pure?: boolean
|
||||
multiple?: boolean
|
||||
clearable?: boolean
|
||||
className?: string
|
||||
width?: string
|
||||
dropdownClassName?: string
|
||||
@@ -37,6 +38,7 @@ const defaultProps = {
|
||||
icon: SelectIcon as React.ComponentType,
|
||||
pure: false,
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
width: 'initial',
|
||||
className: '',
|
||||
disableMatchWidth: false,
|
||||
@@ -55,6 +57,7 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
|
||||
onChange,
|
||||
pure,
|
||||
multiple,
|
||||
clearable,
|
||||
placeholder,
|
||||
width,
|
||||
className,
|
||||
@@ -125,7 +128,10 @@ const Select: React.FC<React.PropsWithChildren<SelectProps>> = ({
|
||||
const el = React.cloneElement(child, { preventAllEvents: true })
|
||||
if (!multiple) return el
|
||||
return (
|
||||
<SelectMultipleValue size={sizes.fontSize} disabled={disabled}>
|
||||
<SelectMultipleValue
|
||||
size={sizes.fontSize}
|
||||
disabled={disabled}
|
||||
onClear={clearable ? () => updateValue(child.props.value) : null}>
|
||||
{el}
|
||||
</SelectMultipleValue>
|
||||
)
|
||||
|
||||
@@ -111,6 +111,25 @@ Display a dropdown list of items.
|
||||
`}
|
||||
/>
|
||||
|
||||
<Playground
|
||||
title="Multiple without clear"
|
||||
desc="`Select` supports multiple values to be selected without the clear icon."
|
||||
scope={{ Select }}
|
||||
code={`
|
||||
<Select placeholder="Frameworks" multiple width="200px" clearable={false} 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."
|
||||
@@ -175,23 +194,24 @@ Display a dropdown list of items.
|
||||
<Attributes edit="/pages/en-us/components/select.mdx">
|
||||
<Attributes.Title>Select.Props</Attributes.Title>
|
||||
|
||||
| Attribute | Description | Type | Accepted values | Default |
|
||||
| --------------------- | ----------------------------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
|
||||
| **value** | selected value | `string`, `string[]` | - | - |
|
||||
| **initialValue** | initial value | `string`, `string[]` | - | - |
|
||||
| **placeholder** | placeholder string | `string` | - | - |
|
||||
| **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 | <Code>(val: string | string[]) => void </Code> | - | - |
|
||||
| **dropdownClassName** | className of dropdown menu | `string` | - | - |
|
||||
| **dropdownStyle** | style of dropdown menu | `object` | - | - |
|
||||
| **disableMatchWidth** | disable Option from follow Select width | `boolean` | - | `false` |
|
||||
| **getPopupContainer** | dropdown render parent element, the default is `body` | `() => HTMLElement` | - | - |
|
||||
| ... | native props | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
|
||||
| Attribute | Description | Type | Accepted values | Default |
|
||||
| --------------------- | -------------------------------------------------------- | --------------------------------------------------- | --------------------------------- | --------------- |
|
||||
| **value** | selected value | `string`, `string[]` | - | - |
|
||||
| **initialValue** | initial value | `string`, `string[]` | - | - |
|
||||
| **placeholder** | placeholder string | `string` | - | - |
|
||||
| **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` |
|
||||
| **clearable** | add clear icon on multiple selection (ignored otherwise) | `boolean` | - | `true` |
|
||||
| **disabled** | disable current radio | `boolean` | - | `false` |
|
||||
| **onChange** | selected value | <Code>(val: string | string[]) => void </Code> | - | - |
|
||||
| **dropdownClassName** | className of dropdown menu | `string` | - | - |
|
||||
| **dropdownStyle** | style of dropdown menu | `object` | - | - |
|
||||
| **disableMatchWidth** | disable Option from follow Select width | `boolean` | - | `false` |
|
||||
| **getPopupContainer** | dropdown render parent element, the default is `body` | `() => HTMLElement` | - | - |
|
||||
| ... | native props | `HTMLAttributes` | `'name', 'alt', 'className', ...` | - |
|
||||
|
||||
<Attributes.Title>Select.Option.Props</Attributes.Title>
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ export const meta = {
|
||||
|
||||
<Playground
|
||||
title="多选"
|
||||
desc="Select 组件支持同时选中多个值。"
|
||||
desc="`Select` 组件支持同时选中多个值。"
|
||||
scope={{ Select }}
|
||||
code={`
|
||||
<Select placeholder="选择框架" multiple width="200px" initialValue={['1', '3', '4', '6']}>
|
||||
@@ -111,6 +111,25 @@ export const meta = {
|
||||
`}
|
||||
/>
|
||||
|
||||
<Playground
|
||||
title="无移除图标的多选"
|
||||
desc="`Select` 支持不携带移除图标的多选控件。"
|
||||
scope={{ Select }}
|
||||
code={`
|
||||
<Select placeholder="选择框架" multiple width="200px" clearable={false} 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="与其他组件组合使用。"
|
||||
@@ -185,6 +204,7 @@ export const meta = {
|
||||
| **icon** | 右侧图标组件 | `ComponentType` | - | `SVG Component` |
|
||||
| **pure** | 隐藏右侧图标组件 | `boolean` | - | `false` |
|
||||
| **multiple** | 是否支持多选 | `boolean` | - | `false` |
|
||||
| **clearable** | 多选时是否展示移除图标 | `boolean` | - | `true` |
|
||||
| **disabled** | 禁用所有的交互 | `boolean` | - | `false` |
|
||||
| **onChange** | 选项被选中所触发的事件 | <Code>(val: string | string[]) => void </Code> | - | - |
|
||||
| **dropdownClassName** | 下拉框的自定义类名 | `string` | - | - |
|
||||
|
||||
Reference in New Issue
Block a user