diff --git a/docs/select/demo/multiple.md b/docs/select/demo/multiple.md index daf20bf2a3..ec6ab79250 100644 --- a/docs/select/demo/multiple.md +++ b/docs/select/demo/multiple.md @@ -15,7 +15,9 @@ multiple select --- ````jsx -import { Select } from '@alifd/next'; +import { Select, Balloon } from '@alifd/next'; + +const { Tooltip } = Balloon; const dataSource = [ {value: '10001', label: 'Lucy King'}, @@ -51,16 +53,26 @@ class Demo extends React.Component{ } } +const maxTagPlaceholder = (selectedValues, totalValues) => { + const trigger = {`已选择 ${selectedValues.length}/${totalValues.length} 项`}; + const labels = selectedValues.map(obj => obj.label); + + return { labels.join(', ') } +} ReactDOM.render(
- -      - +        + 受控写法

+ 设置最大显示Tag数 (maxTagCount)
+

+ 设置一行展示 (tagInline)
+

, mountNode); ```` diff --git a/docs/select/index.en-us.md b/docs/select/index.en-us.md index c42f79439a..1cc4637853 100644 --- a/docs/select/index.en-us.md +++ b/docs/select/index.en-us.md @@ -96,7 +96,7 @@ This is because the layer's animation of the overlay is implemented by `classNam |cacheValue | dataSource keeps the selected content | Boolean | true | |valueRender | Methods for rendering Select to display content


















** Parameters**:
_item_: {Object} Render node's item
**return value **:
{ReactNode} show content
| Function | item => item.label \|\| item.value | | searchValue | Controlled search value, generally not set | String | - | -| maxTagTextLength | max length of each tag | Number | - | +| tagInline | show in one line or not | Boolean | false | | maxTagCount | max count of tag can be show | Number | - | | maxTagPlaceholder | if selected tags' length is over maxTagCount, what to show

**签名**:
Function() => void | Function | (selected, total) => `${selected} / ${total}` | | onRemove | tag Delete callback


**Signature**:
Function(item: object) => void
**Parameters**:
_item_: {object} Render node's Item | Function | func.noop | diff --git a/docs/select/index.md b/docs/select/index.md index 47e00921a8..fd9eacea1e 100644 --- a/docs/select/index.md +++ b/docs/select/index.md @@ -62,53 +62,53 @@ const dataSource = [ ### Select -| 参数 | 说明 | 类型 | 默认值 | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------- | -| size | 选择器尺寸

**可选值**:
'small', 'medium', 'large' | Enum | 'medium' | -| value | 当前值,用于受控模式 | any | - | -| defaultValue | 初始的默认值 | any | - | -| placeholder | 没有值的时候的占位符 | String | - | -| autoWidth | 下拉菜单是否与选择器对齐 | Boolean | true | -| label | 自定义内联 label | ReactNode | - | -| hasClear | 是否有清除按钮(单选模式有效) | Boolean | - | -| state | 校验状态

**可选值**:
'error', 'loading' | Enum | - | -| readOnly | 是否只读,只读模式下可以展开弹层但不能选 | Boolean | - | -| disabled | 是否禁用选择器 | Boolean | - | -| visible | 当前弹层是否显示 | Boolean | - | -| defaultVisible | 弹层初始化是否显示 | Boolean | - | -| onVisibleChange | 弹层显示或隐藏时触发的回调

**签名**:
Function(visible: Boolean) => void
**参数**:
_visible_: {Boolean} 弹层是否显示 | Function | func.noop | -| popupContainer | 弹层挂载的容器节点 | String/Function | - | -| popupClassName | 弹层的 className | any | - | -| popupStyle | 弹层的内联样式 | Object | - | -| popupProps | 添加到弹层上的属性 | Object | {} | -| popupContent | 自定义弹层的内容 | ReactNode | - | -| filterLocal | 是否使用本地过滤,在数据源为远程的时候需要关闭此项 | Boolean | true | -| filter | 本地过滤方法,返回一个 Boolean 值确定是否保留

**签名**:
Function() => void | Function | filter | -| onToggleHighlightItem | 键盘上下键切换菜单高亮选项的回调

**签名**:
Function() => void | Function | func.noop | -| useVirtual | 是否开启虚拟滚动模式 | Boolean | - | -| dataSource | 传入的数据源,可以动态渲染子项,详见 [dataSource的使用](#dataSource的使用) | Array<Object/Boolean/Number/String> | - | -| itemRender | 渲染 MenuItem 内容的方法

**签名**:
Function(item: Object, searchValue: String) => ReactNode
**参数**:
_item_: {Object} 渲染节点的item
_searchValue_: {String} 搜索关键字(如果开启搜索)
**返回值**:
{ReactNode} item node
| Function | - | -| mode | 选择器模式

**可选值**:
'single', 'multiple', 'tag' | Enum | 'single' | -| notFoundContent | 弹层内容为空的文案 | ReactNode | - | -| onChange | Select发生改变时触发的回调

**签名**:
Function(value: mixed, actionType: String, item: mixed) => void
**参数**:
_value_: {mixed} 选中的值
_actionType_: {String} 触发的方式, 'itemClick', 'enter', 'tag'
_item_: {mixed} 选中的值的对象数据 (useDetailValue=false有效) | Function | - | -| hasBorder | 是否有边框 | Boolean | - | -| hasArrow | 是否有下拉箭头 | Boolean | true | -| showSearch | 展开后是否能搜索(tag 模式下固定为true) | Boolean | false | -| onSearch | 当搜索框值变化时回调

**签名**:
Function(value: String) => void
**参数**:
_value_: {String} 数据 | Function | func.noop | -| onSearchClear | 当搜索框值被清空时候的回调

**签名**:
Function(actionType: String) => void
**参数**:
_actionType_: {String} 触发的方式, 'select'(选择清空), 'popupClose'(弹窗关闭清空) | Function | func.noop | -| hasSelectAll | 多选模式下是否有全选功能 | Boolean/String | - | -| fillProps | 填充到选择框里的值的 key | String | - | -| useDetailValue | onChange 返回的 value 使用 dataSource 的对象 | Boolean | - | -| cacheValue | dataSource 变化的时是否保留已选的内容 | Boolean | true | -| valueRender | 渲染 Select 展现内容的方法

**签名**:
Function(item: Object) => ReactNode
**参数**:
_item_: {Object} 渲染节点的item
**返回值**:
{ReactNode} 展现内容
| Function | item => item.label \|\| item.value | -| searchValue | 受控搜索值,一般不需要设置 | String | - | -| maxTagTextLength | tag最多显示的字符数 | Number | - | -| maxTagCount | 最多显示多少个 tag | Number | - | -| maxTagPlaceholder | 隐藏多余 tag 时显示的内容,在 maxTagCount 生效时起作用

**签名**:
Function() => void | Function | (selected, total) => `${selected} / ${total}` | -| hiddenSelected | 选择后是否立即隐藏菜单 (mode=multiple/tag 模式生效) | Boolean | - | -| onRemove | tag 删除回调

**签名**:
Function(item: object) => void
**参数**:
_item_: {object} 渲染节点的item | Function | func.noop | -| onFocus | 焦点事件

**签名**:
Function() => void | Function | func.noop | -| onBlur | 失去焦点事件

**签名**:
Function() => void | Function | func.noop | +| 参数 | 说明 | 类型 | 默认值 | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------- | +| size | 选择器尺寸

**可选值**:
'small', 'medium', 'large' | Enum | 'medium' | +| value | 当前值,用于受控模式 | any | - | +| defaultValue | 初始的默认值 | any | - | +| placeholder | 没有值的时候的占位符 | String | - | +| autoWidth | 下拉菜单是否与选择器对齐 | Boolean | true | +| label | 自定义内联 label | ReactNode | - | +| hasClear | 是否有清除按钮(单选模式有效) | Boolean | - | +| state | 校验状态

**可选值**:
'error', 'loading' | Enum | - | +| readOnly | 是否只读,只读模式下可以展开弹层但不能选 | Boolean | - | +| disabled | 是否禁用选择器 | Boolean | - | +| visible | 当前弹层是否显示 | Boolean | - | +| defaultVisible | 弹层初始化是否显示 | Boolean | - | +| onVisibleChange | 弹层显示或隐藏时触发的回调

**签名**:
Function(visible: Boolean) => void
**参数**:
_visible_: {Boolean} 弹层是否显示 | Function | func.noop | +| popupContainer | 弹层挂载的容器节点 | String/Function | - | +| popupClassName | 弹层的 className | any | - | +| popupStyle | 弹层的内联样式 | Object | - | +| popupProps | 添加到弹层上的属性 | Object | {} | +| popupContent | 自定义弹层的内容 | ReactNode | - | +| filterLocal | 是否使用本地过滤,在数据源为远程的时候需要关闭此项 | Boolean | true | +| filter | 本地过滤方法,返回一个 Boolean 值确定是否保留

**签名**:
Function() => void | Function | filter | +| onToggleHighlightItem | 键盘上下键切换菜单高亮选项的回调

**签名**:
Function() => void | Function | func.noop | +| useVirtual | 是否开启虚拟滚动模式 | Boolean | - | +| dataSource | 传入的数据源,可以动态渲染子项,详见 [dataSource的使用](#dataSource的使用) | Array<Object/Boolean/Number/String> | - | +| itemRender | 渲染 MenuItem 内容的方法

**签名**:
Function(item: Object, searchValue: String) => ReactNode
**参数**:
_item_: {Object} 渲染节点的item
_searchValue_: {String} 搜索关键字(如果开启搜索)
**返回值**:
{ReactNode} item node
| Function | - | +| mode | 选择器模式

**可选值**:
'single', 'multiple', 'tag' | Enum | 'single' | +| notFoundContent | 弹层内容为空的文案 | ReactNode | - | +| onChange | Select发生改变时触发的回调

**签名**:
Function(value: mixed, actionType: String, item: mixed) => void
**参数**:
_value_: {mixed} 选中的值
_actionType_: {String} 触发的方式, 'itemClick', 'enter', 'tag'
_item_: {mixed} 选中的值的对象数据 (useDetailValue=false有效) | Function | - | +| hasBorder | 是否有边框 | Boolean | - | +| hasArrow | 是否有下拉箭头 | Boolean | true | +| showSearch | 展开后是否能搜索(tag 模式下固定为true) | Boolean | false | +| onSearch | 当搜索框值变化时回调

**签名**:
Function(value: String) => void
**参数**:
_value_: {String} 数据 | Function | func.noop | +| onSearchClear | 当搜索框值被清空时候的回调

**签名**:
Function(actionType: String) => void
**参数**:
_actionType_: {String} 触发的方式, 'select'(选择清空), 'popupClose'(弹窗关闭清空) | Function | func.noop | +| hasSelectAll | 多选模式下是否有全选功能 | Boolean/String | - | +| fillProps | 填充到选择框里的值的 key | String | - | +| useDetailValue | onChange 返回的 value 使用 dataSource 的对象 | Boolean | - | +| cacheValue | dataSource 变化的时是否保留已选的内容 | Boolean | true | +| valueRender | 渲染 Select 展现内容的方法

**签名**:
Function(item: Object) => ReactNode
**参数**:
_item_: {Object} 渲染节点的item
**返回值**:
{ReactNode} 展现内容
| Function | item => item.label \|\| item.value | +| searchValue | 受控搜索值,一般不需要设置 | String | - | +| tagInline | 是否一行显示,仅在 mode 为 multiple 的时候生效 | Boolean | false | +| maxTagCount | 最多显示多少个 tag | Number | - | +| maxTagPlaceholder | 隐藏多余 tag 时显示的内容,在 maxTagCount 生效时起作用

**签名**:
Function(selected: number, total: number) => void
**参数**:
_selected_: {number} 当前已选中的元素个数
_total_: {number} 总待选元素个数 | Function | - | +| hiddenSelected | 选择后是否立即隐藏菜单 (mode=multiple/tag 模式生效) | Boolean | - | +| onRemove | tag 删除回调

**签名**:
Function(item: object) => void
**参数**:
_item_: {object} 渲染节点的item | Function | func.noop | +| onFocus | 焦点事件

**签名**:
Function() => void | Function | func.noop | +| onBlur | 失去焦点事件

**签名**:
Function() => void | Function | func.noop | ### Select.AutoComplete diff --git a/src/locale/en-us.js b/src/locale/en-us.js index ecf75294c8..4a85c5c016 100644 --- a/src/locale/en-us.js +++ b/src/locale/en-us.js @@ -72,6 +72,7 @@ export default { selectPlaceholder: 'Please Select', autoCompletePlaceholder: 'Please Input', notFoundContent: 'No Options', + maxTagPlaceholder: 'Selected {selected}/{total} Total', }, Table: { empty: 'No Data', diff --git a/src/locale/ja-jp.js b/src/locale/ja-jp.js index 990e7c571a..ab51c24fd4 100644 --- a/src/locale/ja-jp.js +++ b/src/locale/ja-jp.js @@ -71,6 +71,7 @@ export default { selectPlaceholder: '選択', autoCompletePlaceholder: '入力', notFoundContent: '選択肢なし', + maxTagPlaceholder: '選択済み {selected}/{total}', }, Table: { empty: 'データなし', diff --git a/src/locale/zh-cn.js b/src/locale/zh-cn.js index b2b9ea23e0..d44104e068 100644 --- a/src/locale/zh-cn.js +++ b/src/locale/zh-cn.js @@ -71,6 +71,7 @@ export default { selectPlaceholder: '请选择', autoCompletePlaceholder: '请输入', notFoundContent: '无选项', + maxTagPlaceholder: '已选择 {selected}/{total} 项', }, Table: { empty: '没有数据', diff --git a/src/locale/zh-tw.js b/src/locale/zh-tw.js index d277c509fa..1930087d94 100644 --- a/src/locale/zh-tw.js +++ b/src/locale/zh-tw.js @@ -71,6 +71,7 @@ export default { selectPlaceholder: '請選擇', autoCompletePlaceholder: '請輸入', notFoundContent: '無選項', + maxTagPlaceholder: '已選擇 {selected}/{total} 項', }, Table: { empty: '沒有數據', diff --git a/src/select/main.scss b/src/select/main.scss index 49a6b6161f..91ce8bfe20 100644 --- a/src/select/main.scss +++ b/src/select/main.scss @@ -170,6 +170,29 @@ } } + &-multiple { + .#{$css-prefix}input { + height: auto; + align-items: start; + } + #{$select-prefix}-compact { + position: relative; + white-space: nowrap; + #{$select-prefix}-trigger-search { + width: auto; + } + #{$select-prefix}-tag-compact { + position: absolute; + top: 0; + right: 0; + z-index: 1; + padding: 0 4px 0 16px; + color: $color-text1-3; + background: linear-gradient(90deg, rgba(255, 255, 255, 0), #FFF 10px); + } + } + } + &-multiple, &-tag { #{$select-prefix}-values { /* Tag 有 3px 的 margin-bottom,所以包裹 Tag 的容器要作一下处理 */ @@ -195,6 +218,9 @@ #{$select-prefix}-values { @include select-size($form-element-small-height, $select-s-lineheight); } + #{$select-prefix}-values-compact { + height: $form-element-small-height !important; + } #{$tag-prefix} { border: 0; @include select-tag-size($select-s-lineheight, -1); @@ -203,7 +229,7 @@ line-height: $select-s-lineheight; } } - .#{$css-prefix}input-label, .#{$css-prefix}input-control { + .#{$css-prefix}input-label, .#{$css-prefix}input-control, #{$select-prefix}-tag-compact { line-height: $form-element-small-height - 2; } } @@ -212,10 +238,13 @@ #{$select-prefix}-values { @include select-size($form-element-medium-height, $select-m-lineheight); } + #{$select-prefix}-values-compact { + height: $form-element-medium-height !important; + } #{$tag-prefix} { @include select-tag-size($select-m-lineheight); } - .#{$css-prefix}input-label, .#{$css-prefix}input-control { + .#{$css-prefix}input-label, .#{$css-prefix}input-control, #{$select-prefix}-tag-compact { line-height: $form-element-medium-height - 2; } } @@ -224,10 +253,13 @@ #{$select-prefix}-values { @include select-size($form-element-large-height, $select-l-lineheight); } + #{$select-prefix}-values-compact { + height: $form-element-large-height !important; + } #{$tag-prefix} { @include select-tag-size($select-l-lineheight); } - .#{$css-prefix}input-label, .#{$css-prefix}input-control { + .#{$css-prefix}input-label, .#{$css-prefix}input-control, #{$select-prefix}-tag-compact { line-height: $form-element-large-height - 2; } } @@ -421,3 +453,5 @@ } } } + +@import "./rtl.scss"; diff --git a/src/select/rtl.scss b/src/select/rtl.scss new file mode 100644 index 0000000000..7c8e404571 --- /dev/null +++ b/src/select/rtl.scss @@ -0,0 +1,10 @@ +#{$select-prefix}#{$select-prefix}-multiple[dir='rtl'] { + #{$select-prefix}-compact { + #{$select-prefix}-tag-compact { + left: 0; + right: auto; + padding: 0 16px 0 4px; + background: linear-gradient(270deg, rgba(255, 255, 255, 0), #FFF 10px); + } + } +} diff --git a/src/select/select.jsx b/src/select/select.jsx index fda2c2f89b..a52027c915 100644 --- a/src/select/select.jsx +++ b/src/select/select.jsx @@ -2,10 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { func, obj, KEYCODE, env } from '../util'; +import { func, obj, KEYCODE, env, str } from '../util'; import Tag from '../tag'; import Input from '../input'; import Icon from '../icon'; +import zhCN from '../locale/zh-cn'; import Base from './base'; import { isNull, getValueDataSource, valueToSelectKey } from './util'; @@ -123,18 +124,17 @@ class Select extends Base { */ searchValue: PropTypes.string, /** - * tag最多显示的字符数 - * @type {[type]} + * 是否一行显示,仅在 mode 为 multiple 的时候生效 */ - maxTagTextLength: PropTypes.number, + tagInline: PropTypes.bool, /** * 最多显示多少个 tag - * @type {[type]} */ maxTagCount: PropTypes.number, /** * 隐藏多余 tag 时显示的内容,在 maxTagCount 生效时起作用 - * @type {[type]} + * @param {number} selectedValues 当前已选中的元素 + * @param {number} totalValues 总待选元素 */ maxTagPlaceholder: PropTypes.func, /** @@ -159,16 +159,18 @@ class Select extends Base { */ onBlur: PropTypes.func, onKeyDown: PropTypes.func, + locale: PropTypes.object, }; static defaultProps = { ...Base.defaultProps, + locale: zhCN.Select, mode: 'single', showSearch: false, cacheValue: true, + tagInline: false, onSearch: noop, onSearchClear: noop, - maxTagPlaceholder: (selected, total) => `${selected} / ${total}`, hasArrow: true, onRemove: noop, // highlightFirstItem: true, @@ -203,6 +205,7 @@ class Select extends Base { 'handleSearch', 'handleSearchKeyDown', 'handleSelectAll', + 'maxTagPlaceholder', ]); } @@ -695,20 +698,30 @@ class Select extends Base { } } + maxTagPlaceholder(selectedValues, totalValues) { + const { locale } = this.props; + + return `${str.template(locale.maxTagPlaceholder, { + selected: selectedValues.length, + total: totalValues.length, + })}`; + } + /** * 如果用户是自定义的弹层,则直接以 value 为准,不再校验 dataSource * @param {object} props */ renderValues() { const { + prefix, mode, size, valueRender, fillProps, disabled, maxTagCount, - maxTagTextLength, maxTagPlaceholder, + tagInline, } = this.props; let value = this.state.value; @@ -745,38 +758,49 @@ class Select extends Base { } else if (value) { let limitedCountValue = value; let maxTagPlaceholderEl; + const totalValue = this.dataStore.getFlattenDS(); + const holder = + 'maxTagPlaceholder' in this.props + ? maxTagPlaceholder + : this.maxTagPlaceholder; - if (maxTagCount !== undefined && value.length > maxTagCount) { - const totalLen = this.dataStore.getFlattenDS().length; - + if ( + maxTagCount !== undefined && + value.length > maxTagCount && + !tagInline + ) { limitedCountValue = limitedCountValue.slice(0, maxTagCount); maxTagPlaceholderEl = ( - {maxTagPlaceholder(value.length, totalLen)} + {holder(value, totalValue)} ); } + + if (value.length > 0 && tagInline) { + maxTagPlaceholderEl = ( +
+ {holder(value, totalValue)} +
+ ); + } + value = limitedCountValue; if (!Array.isArray(value)) { value = [value]; } + const selectedValueNodes = value.map(v => { if (!v) { return null; } const labelNode = fillProps ? v[fillProps] : valueRender(v); - let content = labelNode; - if ( - maxTagTextLength && - typeof content === 'string' && - content.length > maxTagTextLength - ) { - content = `${content.slice(0, maxTagTextLength)}..`; - } + return ( - {content} + {labelNode} ); }); if (maxTagPlaceholderEl) { - selectedValueNodes.push(maxTagPlaceholderEl); + if (tagInline) { + selectedValueNodes.unshift(maxTagPlaceholderEl); + } else { + selectedValueNodes.push(maxTagPlaceholderEl); + } } return selectedValueNodes; } @@ -1006,7 +1034,7 @@ class Select extends Base { } renderSearchInput(valueNodes, placeholder, inputEl) { - const { prefix, mode } = this.props; + const { prefix, mode, tagInline } = this.props; const isSingle = mode === 'single'; const mirrorText = this.state.searchValue; @@ -1014,17 +1042,29 @@ class Select extends Base { const cls = classNames({ [`${prefix}select-values`]: true, [`${prefix}input-text-field`]: true, + [`${prefix}select-compact`]: !isSingle && tagInline, }); - return ( - - {isSingle && valueNodes ? {valueNodes} : valueNodes} - - {inputEl} - {mirrorText || placeholder}  - + const searchInput = [ + isSingle && valueNodes ? {valueNodes} : valueNodes, + ]; + const triggerSearch = ( + + {inputEl} + {mirrorText || placeholder}  ); + + if (!isSingle && tagInline) { + searchInput.unshift(triggerSearch); + } else { + searchInput.push(triggerSearch); + } + + return {searchInput}; } /** diff --git a/test/select/index-spec.js b/test/select/index-spec.js index b1964cf06e..8037b4584d 100644 --- a/test/select/index-spec.js +++ b/test/select/index-spec.js @@ -467,7 +467,7 @@ describe('Select', () => { wrapper.find('i.next-icon-delete-filling').simulate('click'); }); - it('should support custom content with mode=tag', done => { + it('should support maxTagCount', done => { const value = [ { label: 'xxx', value: '0' }, { label: 'empty', value: 1 }, @@ -475,7 +475,6 @@ describe('Select', () => { { label: 'yyy', value: 1 }, ]; wrapper.setProps({ - maxTagTextLength: 2, visible: true, maxTagCount: 2, mode: 'tag', @@ -486,6 +485,25 @@ describe('Select', () => { done() }); + it('should support tagInline', done => { + const value = [ + { label: 'xxx', value: '0' }, + { label: 'empty', value: 1 }, + { label: 'zzz', value: 1 }, + { label: 'yyy', value: 1 }, + ]; + wrapper.setProps({ + visible: true, + tagInline: true, + mode: 'tag', + value + }); + wrapper.update(); + + assert(wrapper.find('span.next-select .next-select-compact div.next-select-tag-compact').length === 1); + done() + }); + it('should support onChange with mode=single ', done => { const dataSource = [ { label: 'xxx', value: '0' },