diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..89202874 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,13 @@ +const base = require('@umijs/fabric/dist/eslint'); + +module.exports = { + ...base, + rules: { + ...base.rules, + 'default-case': 0, + 'eslint-comments/disable-enable-pair': 0, + 'jsx-a11y/interactive-supports-focus': 0, + 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], + '@typescript-eslint/no-object-literal-type-assertion': 0, + }, +}; diff --git a/.fatherrc.js b/.fatherrc.js new file mode 100644 index 00000000..767a2abf --- /dev/null +++ b/.fatherrc.js @@ -0,0 +1,8 @@ +export default { + cjs: 'babel', + esm: { type: 'babel', importLibToEs: true }, + preCommit: { + eslint: true, + prettier: true, + }, +}; diff --git a/.gitignore b/.gitignore index 75edf312..dc0bfbfa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.storybook +.doc *.iml *.log .idea diff --git a/assets/select.less b/assets/select.less index 908ddb88..2e473db4 100644 --- a/assets/select.less +++ b/assets/select.less @@ -1,536 +1,3 @@ -@selectPrefixCls: rc-tree-select; +@import '~rc-select/assets/index'; -.effect() { - animation-duration: .3s; - animation-fill-mode: both; - transform-origin: 0 0; -} - -.@{selectPrefixCls} { - box-sizing: border-box; - display: inline-block; - position: relative; - vertical-align: middle; - color: #666; - - &-allow-clear { - .@{selectPrefixCls}-selection--single .@{selectPrefixCls}-selection__rendered { - padding-right: 40px; - } - } - - ul, li { - margin: 0; - padding: 0; - list-style: none; - } - - > ul > li > a { - padding: 0; - background-color: #fff; - } - - // arrow - &-arrow { - height: 26px; - position: absolute; - top: 1px; - right: 1px; - width: 20px; - &:after { - content: ''; - border-color: #999999 transparent transparent transparent; - border-style: solid; - border-width: 5px 4px 0 4px; - height: 0; - width: 0; - margin-left: -4px; - margin-top: -2px; - position: absolute; - top: 50%; - left: 50%; - } - } - - &-selection { - outline: none; - user-select: none; - -webkit-user-select: none; - - box-sizing: border-box; - display: block; - - background-color: #fff; - border-radius: 6px; - border: 1px solid #d9d9d9; - - &__clear { - font-weight: bold; - position: absolute; - } - } - - &-enabled { - .@{selectPrefixCls}-selection { - &:hover { - border-color: #23c0fa; - box-shadow: 0 0 2px fadeout(#2db7f5, 20%); - } - &:active { - border-color: #2db7f5; - } - } - - &.@{selectPrefixCls}-focused { - .@{selectPrefixCls}-selection { - //border-color: #23c0fa; - border-color: #7700fa; - box-shadow: 0 0 2px fadeout(#2db7f5, 20%); - } - } - } - - - - &-selection--single { - height: 28px; - cursor: pointer; - position: relative; - - .@{selectPrefixCls}-selection__rendered { - display: block; - padding-left: 10px; - padding-right: 20px; - line-height: 28px; - } - - .@{selectPrefixCls}-selection-selected-value { - display: block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .@{selectPrefixCls}-selection__clear { - top: 5px; - right: 20px; - &:after { - content: '×'; - } - } - } - - &-disabled { - color: #ccc; - cursor: not-allowed; - - .@{selectPrefixCls}-selection--single, - .@{selectPrefixCls}-selection__choice__remove { - cursor: not-allowed; - color: #ccc; - - &:hover{ - cursor: not-allowed; - color: #ccc; - } - } - } - - &-search__field__wrap { - display: inline-block; - position: relative; - } - - &-search__field__placeholder { - display: block; - position: absolute; - top: 0; - left: 3px; - color: #aaa; - } - - &-search__field__mirror { - position: absolute; - top: 0; - left: -9999px; - white-space: pre; - pointer-events: none; - } - - &-search--inline { - float: left; - width: 100%; - .@{selectPrefixCls}-search__field__wrap { - width: 100%; - } - .@{selectPrefixCls}-search__field { - border: none; - font-size: 100%; - //margin-top: 5px; - background: transparent; - outline: 0; - width: 100%; - } - > i { - float: right; - } - } - - &-enabled&-selection--multiple { - cursor: text; - } - - &-selection--multiple { - min-height: 28px; - - .@{selectPrefixCls}-search--inline { - width: auto; - .@{selectPrefixCls}-search__field { - width: 0.75em; - } - } - - .@{selectPrefixCls}-search__field__placeholder { - top: 5px; - left: 8px; - } - - .@{selectPrefixCls}-selection__rendered { - //display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 8px; - padding-bottom: 2px; - padding-right: 10px; - } - - > ul > li { - margin-top: 4px; - height: 20px; - line-height: 20px; - } - - .@{selectPrefixCls}-selection__clear { - top: 5px; - right: 8px; - } - } - - &-enabled { - .@{selectPrefixCls}-selection__choice { - cursor: default; - &:hover { - .@{selectPrefixCls}-selection__choice__remove { - opacity: 1; - transform: scale(1); - } - .@{selectPrefixCls}-selection__choice__remove + - .@{selectPrefixCls}-selection__choice__content { - margin-left: -8px; - margin-right: 8px; - } - } - } - } - - & &-selection__choice { - background-color: #f3f3f3; - border-radius: 4px; - float: left; - padding: 0 15px; - margin-right: 4px; - position: relative; - overflow: hidden; - transition: padding .3s cubic-bezier(0.6, -0.28, 0.735, 0.045), width .3s cubic-bezier(0.6, -0.28, 0.735, 0.045); - - &__content { - margin-left: 0; - margin-right: 0; - transition: margin .3s cubic-bezier(0.165, 0.84, 0.44, 1); - } - - &-zoom-enter, &-zoom-appear, &-zoom-leave { - .effect(); - opacity: 0; - animation-play-state: paused; - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); - } - - &-zoom-leave { - opacity: 1; - animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); - } - - &-zoom-enter.@{selectPrefixCls}-selection__choice-zoom-enter-active, - &-zoom-appear.@{selectPrefixCls}-selection__choice-zoom-appear-active { - animation-play-state: running; - animation-name: rcSelectChoiceZoomIn; - } - - &-zoom-leave.@{selectPrefixCls}-selection__choice-zoom-leave-active { - animation-play-state: running; - animation-name: rcSelectChoiceZoomOut; - } - - @keyframes rcSelectChoiceZoomIn { - 0% { - transform: scale(0.6); - opacity: 0; - } - 100% { - transform: scale(1); - opacity: 1; - } - } - - @keyframes rcSelectChoiceZoomOut { - to { - transform: scale(0); - opacity: 0; - } - } - - &__remove { - color: #919191; - cursor: pointer; - font-weight: bold; - padding: 0 0 0 8px; - position: absolute; - opacity: 0; - transform: scale(0); - top: 0; - right: 2px; - transition: opacity .3s, transform .3s; - - &:before { - content: '×' - } - - &:hover { - color: #333; - } - } - } - - &-dropdown { - background-color: white; - border: 1px solid #d9d9d9; - box-shadow: 0 0px 4px #d9d9d9; - border-radius: 4px; - box-sizing: border-box; - z-index: 100; - left: -9999px; - top: -9999px; - //border-top: none; - //border-top-left-radius: 0; - //border-top-right-radius: 0; - position: absolute; - outline: none; - - &-hidden { - display: none; - } - - &-menu { - outline: none; - margin: 0; - padding: 0; - list-style: none; - z-index: 9999; - - > li { - margin: 0; - padding: 0; - } - - &-item-group-list { - margin: 0; - padding: 0; - - > li.@{selectPrefixCls}-menu-item { - padding-left: 20px; - } - } - - &-item-group-title { - color: #999; - line-height: 1.5; - padding: 8px 10px; - border-bottom: 1px solid #dedede; - } - - li&-item { - margin: 0; - position: relative; - display: block; - padding: 7px 10px; - font-weight: normal; - color: #666666; - white-space: nowrap; - - &-selected { - background-color: #ddd; - } - - &-active { - background-color: #5897fb; - color: white; - cursor: pointer; - } - - &-disabled { - color: #ccc; - cursor: not-allowed; - } - - &-divider { - height: 1px; - margin: 1px 0; - overflow: hidden; - background-color: #e5e5e5; - line-height: 0; - } - } - } - - &-slide-up-enter, &-slide-up-appear { - .effect(); - opacity: 0; - animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); - animation-play-state: paused; - } - - &-slide-up-leave { - .effect(); - opacity: 1; - animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); - animation-play-state: paused; - } - - &-slide-up-enter&-slide-up-enter-active&-placement-bottomLeft, &-slide-up-appear&-slide-up-appear-active&-placement-bottomLeft { - animation-name: rcSelectDropdownSlideUpIn; - animation-play-state: running; - } - - &-slide-up-leave&-slide-up-leave-active&-placement-bottomLeft { - animation-name: rcSelectDropdownSlideUpOut; - animation-play-state: running; - } - - &-slide-up-enter&-slide-up-enter-active&-placement-topLeft, &-slide-up-appear&-slide-up-appear-active&-placement-topLeft { - animation-name: rcSelectDropdownSlideDownIn; - animation-play-state: running; - } - - &-slide-up-leave&-slide-up-leave-active&-placement-topLeft { - animation-name: rcSelectDropdownSlideDownOut; - animation-play-state: running; - } - - @keyframes rcSelectDropdownSlideUpIn { - 0% { - opacity: 0; - transform-origin: 0% 0%; - transform: scaleY(0); - } - 100% { - opacity: 1; - transform-origin: 0% 0%; - transform: scaleY(1); - } - } - @keyframes rcSelectDropdownSlideUpOut { - 0% { - opacity: 1; - transform-origin: 0% 0%; - transform: scaleY(1); - } - 100% { - opacity: 0; - transform-origin: 0% 0%; - transform: scaleY(0); - } - } - - @keyframes rcSelectDropdownSlideDownIn { - 0% { - opacity: 0; - transform-origin: 0% 100%; - transform: scaleY(0); - } - 100% { - opacity: 1; - transform-origin: 0% 100%; - transform: scaleY(1); - } - } - @keyframes rcSelectDropdownSlideDownOut { - 0% { - opacity: 1; - transform-origin: 0% 100%; - transform: scaleY(1); - } - 100% { - opacity: 0; - transform-origin: 0% 100%; - transform: scaleY(0); - } - } - } - - &-dropdown-search { - display: block; - padding: 4px; - .@{selectPrefixCls}-search__field__wrap { - width: 100%; - } - .@{selectPrefixCls}-search__field__placeholder { - top: 4px; - } - .@{selectPrefixCls}-search__field { - padding: 4px; - width: 100%; - box-sizing: border-box; - border: 1px solid #d9d9d9; - border-radius: 4px; - outline: none; - } - &.@{selectPrefixCls}-search--hide { - display: none; - } - } - - &-open { - .@{selectPrefixCls}-arrow:after { - border-color: transparent transparent #888 transparent; - border-width: 0 4px 5px 4px; - } - } - - &-not-found { - display: inline-block; - padding: 8px; - } -} - -.custom-icon-demo { - .@{selectPrefixCls} { - &-selection__choice__remove { - &:before { - content: ''; - } - } - - &-arrow { - &:after { - display: none; - } - } - - &-selection__clear { - &:after { - content: ''; - } - } - } -} \ No newline at end of file +@select-prefix: ~'rc-tree-select'; \ No newline at end of file diff --git a/assets/tree.less b/assets/tree.less index 7a74b213..df4daa8a 100644 --- a/assets/tree.less +++ b/assets/tree.less @@ -1,164 +1,3 @@ -@treePrefixCls: rc-tree-select-tree; -.@{treePrefixCls} { - margin: 0; - padding: 5px; - li { - padding: 0; - margin: 0; - list-style: none; - white-space: nowrap; - outline: 0; - a[draggable], - a[draggable="true"] { - color: #333; - -moz-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; - user-select: none; - /* Required to make elements draggable in old WebKit */ - -khtml-user-drag: element; - -webkit-user-drag: element; - } - &.drag-over { - > a[draggable] { - background-color: #316ac5; - color: white; - border: 1px #316ac5 solid; - opacity: 0.8; - } - } - &.drag-over-gap-top { - > a[draggable] { - border-top: 2px blue solid; - } - } - &.drag-over-gap-bottom { - > a[draggable] { - border-bottom: 2px blue solid; - } - } - &.filter-node { - > .@{treePrefixCls}-node-content-wrapper { - color: #a60000!important; - font-weight: bold!important; - } - } - ul { - margin: 0; - padding: 0 0 0 18px; - &.@{treePrefixCls}-line { - background: url("data:image/gif;base64,R0lGODlhCQACAIAAAMzMzP///yH5BAEAAAEALAAAAAAJAAIAAAIEjI9pUAA7") 0 0 repeat-y; - } - } - a { - display: inline-block; - padding: 1px 3px 0 0; - margin: 0; - cursor: pointer; - height: 17px; - text-decoration: none; - vertical-align: top; - } - span { - &.@{treePrefixCls}-switcher, - &.@{treePrefixCls}-checkbox, - &.@{treePrefixCls}-iconEle { - line-height: 16px; - margin-right: 2px; - width: 16px; - height: 16px; - display: inline-block; - vertical-align: middle; - border: 0 none; - cursor: pointer; - outline: none; - background-color: transparent; - background-repeat: no-repeat; - background-attachment: scroll; - background-image: url("data:image/png;base64,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"); - } - &.@{treePrefixCls}-icon_loading { - margin-right: 2px; - vertical-align: top; - background: url("data:image/gif;base64,R0lGODlhEAAQAKIGAMLY8YSx5HOm4Mjc88/g9Ofw+v///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgAGACwAAAAAEAAQAAADMGi6RbUwGjKIXCAA016PgRBElAVlG/RdLOO0X9nK61W39qvqiwz5Ls/rRqrggsdkAgAh+QQFCgAGACwCAAAABwAFAAADD2hqELAmiFBIYY4MAutdCQAh+QQFCgAGACwGAAAABwAFAAADD1hU1kaDOKMYCGAGEeYFCQAh+QQFCgAGACwKAAIABQAHAAADEFhUZjSkKdZqBQG0IELDQAIAIfkEBQoABgAsCgAGAAUABwAAAxBoVlRKgyjmlAIBqCDCzUoCACH5BAUKAAYALAYACgAHAAUAAAMPaGpFtYYMAgJgLogA610JACH5BAUKAAYALAIACgAHAAUAAAMPCAHWFiI4o1ghZZJB5i0JACH5BAUKAAYALAAABgAFAAcAAAMQCAFmIaEp1motpDQySMNFAgA7") no-repeat scroll 0 0 transparent; - } - &.@{treePrefixCls}-switcher { - &-noop { - cursor: auto; - background: none; - } - &_open { - background-position: -93px -56px; - } - &_close { - background-position: -75px -56px; - } - } - &.@{treePrefixCls}-checkbox { - width: 13px; - height: 13px; - margin: 0 3px; - background-position: 0 0; - &-checked { - background-position: -14px 0; - } - &-indeterminate { - background-position: -14px -28px; - } - &-disabled { - background-position: 0 -56px; - } - &.@{treePrefixCls}-checkbox-checked.@{treePrefixCls}-checkbox-disabled { - background-position: -14px -56px; - } - &.@{treePrefixCls}-checkbox-indeterminate.@{treePrefixCls}-checkbox-disabled { - position: relative; - background: #ccc; - border-radius: 3px; - &::after { - content: ' '; - -webkit-transform: scale(1); - transform: scale(1); - position: absolute; - left: 3px; - top: 5px; - width: 5px; - height: 0; - border: 2px solid #fff; - border-top: 0; - border-left: 0; - } - } - } - } - } - &-child-tree { - display: none; - &-open { - display: block; - } - } - &-treenode-disabled { - >span, - >a, - >a span { - color: #ccc; - cursor: not-allowed; - } - } - &-node-selected { - background-color: #ffe6b0; - border: 1px #ffb951 solid; - opacity: 0.8; - } - &-icon__open { - margin-right: 2px; - background-position: -110px -16px; - vertical-align: top; - } - &-icon__close { - margin-right: 2px; - background-position: -110px 0; - vertical-align: top; - } -} +@import '~rc-tree/assets/index'; + +@treePrefixCls: ~'rc-tree-select-tree'; \ No newline at end of file diff --git a/examples/basic.html b/examples/basic.html deleted file mode 100644 index b3a42524..00000000 --- a/examples/basic.html +++ /dev/null @@ -1 +0,0 @@ -placeholder \ No newline at end of file diff --git a/examples/basic.js b/examples/basic.tsx similarity index 85% rename from examples/basic.js rename to examples/basic.tsx index dca20674..0071c2f5 100644 --- a/examples/basic.js +++ b/examples/basic.tsx @@ -1,13 +1,9 @@ -/* eslint react/no-multi-comp:0, no-console:0, no-alert: 0 */ - -import 'rc-tree-select/assets/index.less'; +import '../assets/index.less'; import React from 'react'; -import ReactDOM from 'react-dom'; import 'rc-dialog/assets/index.css'; import Dialog from 'rc-dialog'; -import TreeSelect, { TreeNode, SHOW_PARENT } from 'rc-tree-select'; -import { gData } from './util'; -import './demo.less'; +import TreeSelect, { TreeNode, SHOW_PARENT } from '../src'; +import { gData } from './utils/dataUtil'; function isLeaf(value) { if (!value) { @@ -33,14 +29,14 @@ function isLeaf(value) { function findPath(value, data) { const sel = []; function loop(selected, children) { - for (let i = 0; i < children.length; i++) { + for (let i = 0; i < children.length; i += 1) { const item = children[i]; if (selected === item.value) { sel.push(item); return; } if (item.children) { - loop(selected, item.children, item); + loop(selected, item.children); if (sel.length) { sel.push(item); return; @@ -128,9 +124,9 @@ class Demo extends React.Component { console.log(args); }; - onDropdownVisibleChange = (visible, info) => { + onDropdownVisibleChange = visible => { const { value } = this.state; - console.log(visible, value, info); + console.log(visible, value); if (Array.isArray(value) && value.length > 1 && value.length < 3) { window.alert('please select more than two item or less than one item.'); return false; @@ -138,9 +134,7 @@ class Demo extends React.Component { return true; }; - filterTreeNode = (input, child) => { - return String(child.props.title).indexOf(input) === 0; - }; + filterTreeNode = (input, child) => String(child.props.title).indexOf(input) === 0; render() { const { @@ -166,8 +160,7 @@ class Demo extends React.Component { animation="zoom" maskAnimation="fade" onClose={this.onClose} - style={{ width: 600, height: 400, overflow: 'auto' }} - id="area" + // style={{ width: 600, height: 400, overflow: 'auto' }} >
请下拉选择} searchPlaceholder="please search" showSearch @@ -197,7 +190,7 @@ class Demo extends React.Component { style={{ width: 300 }} transitionName="rc-tree-select-dropdown-slide-up" choiceTransitionName="rc-tree-select-selection__choice-zoom" - dropdownStyle={{ maxHeight: 200, overflow: 'auto' }} + // dropdownStyle={{ maxHeight: 200, overflow: 'auto' }} placeholder={请下拉选择} searchPlaceholder="please search" showSearch @@ -212,23 +205,13 @@ class Demo extends React.Component { open={tsOpen} onChange={(val, ...args) => { console.log('onChange', val, ...args); - if (val === '0-0-0-0-value') { - this.setState({ tsOpen: true }); - } else { - this.setState({ tsOpen: false }); - } this.setState({ value: val }); }} - onDropdownVisibleChange={(v, info) => { - console.log('single onDropdownVisibleChange', v, info); - // document clicked - if (info.documentClickClose && value === '0-0-0-0-value') { - return false; - } + onDropdownVisibleChange={v => { + console.log('single onDropdownVisibleChange', v); this.setState({ tsOpen: v, }); - return true; }} onSelect={this.onSelect} /> @@ -238,7 +221,7 @@ class Demo extends React.Component { style={{ width: 300 }} transitionName="rc-tree-select-dropdown-slide-up" choiceTransitionName="rc-tree-select-selection__choice-zoom" - dropdownStyle={{ maxHeight: 200, overflow: 'auto' }} + // dropdownStyle={{ maxHeight: 200, overflow: 'auto' }} placeholder={请下拉选择} searchPlaceholder="please search" showSearch @@ -256,7 +239,7 @@ class Demo extends React.Component { style={{ width: 300 }} transitionName="rc-tree-select-dropdown-slide-up" choiceTransitionName="rc-tree-select-selection__choice-zoom" - dropdownStyle={{ maxHeight: 200, overflow: 'auto' }} + // dropdownStyle={{ maxHeight: 200, overflow: 'auto' }} placeholder={请下拉选择} searchPlaceholder="please search" multiple @@ -273,7 +256,8 @@ class Demo extends React.Component { className="check-select" transitionName="rc-tree-select-dropdown-slide-up" choiceTransitionName="rc-tree-select-selection__choice-zoom" - dropdownStyle={{ height: 200, overflow: 'auto' }} + style={{ width: 300 }} + // dropdownStyle={{ height: 200, overflow: 'auto' }} dropdownPopupAlign={{ overflow: { adjustY: 0, adjustX: 0 }, offset: [0, 2] }} onDropdownVisibleChange={this.onDropdownVisibleChange} placeholder={请下拉选择} @@ -300,7 +284,7 @@ class Demo extends React.Component { style={{ width: 500 }} transitionName="rc-tree-select-dropdown-slide-up" choiceTransitionName="rc-tree-select-selection__choice-zoom" - dropdownStyle={{ maxHeight: 200, overflow: 'auto' }} + // dropdownStyle={{ maxHeight: 200, overflow: 'auto' }} placeholder={请下拉选择} searchPlaceholder="please search" showSearch @@ -317,10 +301,10 @@ class Demo extends React.Component {

use treeDataSimpleMode

请下拉选择} - searchPlaceholder="please search" - treeLine + // searchPlaceholder="please search" + // treeLine maxTagTextLength={10} searchValue={simpleSearchValue} onSearch={val => { @@ -333,7 +317,10 @@ class Demo extends React.Component { treeCheckable showCheckedStrategy={SHOW_PARENT} onChange={this.onChange} - onSelect={this.onSelect} + onSelect={(...args) => { + this.setState({ simpleSearchValue: '' }); + this.onSelect(...args); + }} />

Testing in extreme conditions (Boundary conditions test)

@@ -363,7 +350,7 @@ class Demo extends React.Component {

use TreeNode Component (not recommend)

, document.getElementById('__react-content')); +export default Demo; diff --git a/examples/big-data.html b/examples/big-data.html deleted file mode 100644 index b3a42524..00000000 --- a/examples/big-data.html +++ /dev/null @@ -1 +0,0 @@ -placeholder \ No newline at end of file diff --git a/examples/big-data.js b/examples/big-data.tsx similarity index 76% rename from examples/big-data.js rename to examples/big-data.tsx index 5dd0fb9c..b36955d9 100644 --- a/examples/big-data.js +++ b/examples/big-data.tsx @@ -1,11 +1,7 @@ -/* eslint react/no-multi-comp:0, no-console:0 */ - -import 'rc-tree-select/assets/index.less'; +import '../assets/index.less'; import React from 'react'; -import ReactDOM from 'react-dom'; -import TreeSelect, { SHOW_PARENT } from 'rc-tree-select'; -import Gen from './big-data-generator'; -import './demo.less'; +import TreeSelect, { SHOW_PARENT } from '../src'; +import Gen from './utils/big-data-generator'; class Demo extends React.Component { state = { @@ -22,7 +18,7 @@ class Demo extends React.Component { onChangeStrictly = value1 => { console.log('onChangeStrictly', value1); - const ind = parseInt(Math.random() * 3, 10); + const ind = parseInt(`${Math.random() * 3}`, 10); value1.push({ value: `0-0-0-${ind}-value`, label: `0-0-0-${ind}-label`, halfChecked: true }); this.setState({ value1, @@ -38,12 +34,14 @@ class Demo extends React.Component { { value: '0-0-value', label: '0-0-label', halfChecked: true }, { value: '0-0-0-value', label: '0-0-0-label' }, ], - // value: ['0-0-0-0-value', '0-0-0-1-value', '0-0-0-2-value'], }); }; render() { const { value1, gData1, value, gData } = this.state; + + // console.log('>>>', gData, gData1, value1); + return (
@@ -52,7 +50,7 @@ class Demo extends React.Component {

normal check

checkStrictly , document.getElementById('__react-content')); +export default Demo; diff --git a/examples/controlled.html b/examples/controlled.html deleted file mode 100644 index b3a42524..00000000 --- a/examples/controlled.html +++ /dev/null @@ -1 +0,0 @@ -placeholder \ No newline at end of file diff --git a/examples/controlled.js b/examples/controlled.tsx similarity index 84% rename from examples/controlled.js rename to examples/controlled.tsx index 9c3254da..fd6669ab 100644 --- a/examples/controlled.js +++ b/examples/controlled.tsx @@ -1,11 +1,7 @@ -/* eslint react/no-multi-comp:0, no-console:0, no-alert: 0 */ - -import 'rc-tree-select/assets/index.less'; +import '../assets/index.less'; import React from 'react'; -import ReactDOM from 'react-dom'; import 'rc-dialog/assets/index.css'; -import TreeSelect, { TreeNode } from 'rc-tree-select'; -import './demo.less'; +import TreeSelect, { TreeNode } from '../src'; class Demo extends React.Component { state = { @@ -32,7 +28,7 @@ class Demo extends React.Component {

Conrolled treeExpandedKeys

@@ -70,4 +66,4 @@ class Demo extends React.Component { } } -ReactDOM.render(, document.getElementById('__react-content')); +export default Demo; diff --git a/examples/custom-icons.html b/examples/custom-icons.html deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/custom-icons.js b/examples/custom-icons.tsx similarity index 67% rename from examples/custom-icons.js rename to examples/custom-icons.tsx index 73c9fc99..275cfb31 100644 --- a/examples/custom-icons.js +++ b/examples/custom-icons.tsx @@ -1,14 +1,11 @@ -/* eslint react/no-multi-comp:0, no-console:0, no-alert: 0 */ - -import 'rc-tree-select/assets/index.less'; +import '../assets/index.less'; import React from 'react'; -import ReactDOM from 'react-dom'; import 'rc-dialog/assets/index.css'; -import TreeSelect from 'rc-tree-select'; -import { gData } from './util'; -import './demo.less'; +import TreeSelect from '../src'; +import { gData } from './utils/dataUtil'; -const bubblePath = 'M632 888H392c-4.4 0-8 3.6-8 8v32c0 ' + +const bubblePath = + 'M632 888H392c-4.4 0-8 3.6-8 8v32c0 ' + '17.7 14.3 32 32 32h192c17.7 0 32-14.3 32-32v-3' + '2c0-4.4-3.6-8-8-8zM512 64c-181.1 0-328 146.9-3' + '28 328 0 121.4 66 227.4 164 284.1V792c0 17.7 1' + @@ -19,44 +16,47 @@ const bubblePath = 'M632 888H392c-4.4 0-8 3.6-8 8v32c0 ' + ' 114.6-256 256-256s256 114.6 256 256c0 92.5-49' + '.4 176.3-128.1 221.8z'; -const clearPath = 'M793 242H366v-74c0-6.7-7.7-10.4-12.9' + +const clearPath = + 'M793 242H366v-74c0-6.7-7.7-10.4-12.9' + '-6.3l-142 112c-4.1 3.2-4.1 9.4 0 12.6l142 112c' + '5.2 4.1 12.9 0.4 12.9-6.3v-74h415v470H175c-4.4' + ' 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h618c35.3 0 64-' + '28.7 64-64V306c0-35.3-28.7-64-64-64z'; -const arrowPath = 'M765.7 486.8L314.9 134.7c-5.3-4.1' + +const arrowPath = + 'M765.7 486.8L314.9 134.7c-5.3-4.1' + '-12.9-0.4-12.9 6.3v77.3c0 4.9 2.3 9.6 6.1 12.6l36' + '0 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6' + '.7 7.7 10.4 12.9 6.3l450.8-352.1c16.4-12.8 16.4-3' + '7.6 0-50.4z'; -const getSvg = (path, iStyle = {}, style = {}) => { - return ( - - - - - - ); -} - +const getSvg = (path, iStyle = {}, style = {}) => ( + + + + + +); -const switcherIcon = (obj) => { +const switcherIcon = obj => { if (obj.isLeaf) { - return getSvg(arrowPath, + return getSvg( + arrowPath, { cursor: 'pointer', backgroundColor: 'white' }, - { transform: 'rotate(270deg)' }); + { transform: 'rotate(270deg)' }, + ); } - return getSvg(arrowPath, + return getSvg( + arrowPath, { cursor: 'pointer', backgroundColor: 'white' }, - { transform: `rotate(${obj.expanded ? 90 : 0}deg)` }); + { transform: `rotate(${obj.expanded ? 90 : 0}deg)` }, + ); }; const inputIcon = getSvg(bubblePath); @@ -87,7 +87,8 @@ function Demo() { transitionName="rc-tree-select-dropdown-slide-up" style={{ width: 300 }} dropdownStyle={{ maxHeight: 200, overflow: 'auto', zIndex: 1500 }} - showSearch allowClear + showSearch + allowClear {...iconProps} />
@@ -99,11 +100,12 @@ function Demo() { transitionName="rc-tree-select-dropdown-slide-up" style={{ width: 300 }} dropdownStyle={{ maxHeight: 200, overflow: 'auto', zIndex: 1500 }} - showSearch allowClear + showSearch + allowClear {...iconPropsFunction} />
); } -ReactDOM.render(, document.getElementById('__react-content')); +export default Demo; diff --git a/examples/debug.tsx b/examples/debug.tsx new file mode 100644 index 00000000..30e25a7a --- /dev/null +++ b/examples/debug.tsx @@ -0,0 +1,89 @@ +/* eslint-disable react/no-array-index-key */ + +import React from 'react'; +import TreeSelect, { TreeNode } from '../src'; +import '../assets/index.less'; +import { RawValueType } from '../src/interface'; + +const treeData = [ + { value: 'parent', label: 'Parent', children: [{ key: 'child', label: 'Child' }] }, + { + value: 'parent2', + label: 'Parent 2', + children: new Array(20).fill(null).map((_, index) => ({ + key: index, + label: `Hello_${index}`, + })), + }, + { value: 'disabled', label: 'Disabled', disabled: true }, + { value: 'disableCheckbox', label: 'No Checkbox', disableCheckbox: true }, +]; + +const Demo: React.FC<{}> = () => { + const [search, setSearch] = React.useState(''); + const [value, setValue] = React.useState([]); + + return ( +
{ + console.log('Focus:', target); + }} + onBlur={({ target }) => { + console.log('Blur:', target); + }} + > +

Debug

+ + + + + + + {new Array(20).fill(null).map((_, index) => ( + + ))} + + + + + { + console.log('Search:', str); + setSearch(str); + }} + onChange={(...args) => { + console.log('Change:', ...args); + }} + /> + { + console.log('Change:', newValue, ...args); + setValue(newValue); + }} + placeholder="Control Mode" + /> + + + + + + +
+ ); +}; + +export default Demo; diff --git a/examples/demo.less b/examples/demo.less deleted file mode 100644 index 5d46fd9f..00000000 --- a/examples/demo.less +++ /dev/null @@ -1,14 +0,0 @@ -.rc-tree-select-selection--multiple { - max-height: 50px; - overflow-y: scroll; -} -.rc-tree-select-dropdown { - max-height: 350px; - overflow-y: scroll; -} -.check-select { - width: 300px; - .rc-tree-select-selection--multiple { - min-height: 50px; - } -} \ No newline at end of file diff --git a/examples/disable.html b/examples/disable.html deleted file mode 100644 index b3a42524..00000000 --- a/examples/disable.html +++ /dev/null @@ -1 +0,0 @@ -placeholder \ No newline at end of file diff --git a/examples/disable.js b/examples/disable.tsx similarity index 83% rename from examples/disable.js rename to examples/disable.tsx index 93e7a9a7..3075fc2a 100644 --- a/examples/disable.js +++ b/examples/disable.tsx @@ -1,10 +1,9 @@ -/* eslint react/no-multi-comp:0, no-console:0 */ -import 'rc-tree-select/assets/index.less'; -import TreeSelect from 'rc-tree-select'; +/* eslint-disable import/no-named-as-default-member */ +import '../assets/index.less'; import React from 'react'; -import ReactDOM from 'react-dom'; +import TreeSelect from '../src'; -const SHOW_PARENT = TreeSelect.SHOW_PARENT; +const { SHOW_PARENT } = TreeSelect; const treeData = [ { @@ -83,4 +82,5 @@ class Demo extends React.Component { } } -ReactDOM.render(, document.getElementById('__react-content')); +export default Demo; +/* eslint-enable */ diff --git a/examples/dynamic.html b/examples/dynamic.html deleted file mode 100644 index b3a42524..00000000 --- a/examples/dynamic.html +++ /dev/null @@ -1 +0,0 @@ -placeholder \ No newline at end of file diff --git a/examples/dynamic.js b/examples/dynamic.js deleted file mode 100644 index 0c9f1620..00000000 --- a/examples/dynamic.js +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint react/no-multi-comp:0, no-console:0 */ - -import 'rc-tree-select/assets/index.less'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import TreeSelect from 'rc-tree-select'; -import { getNewTreeData, generateTreeNodes } from './util'; - -class Demo extends React.Component { - static propTypes = {}; - - state = { - treeData: [ - { label: 'pNode 01', value: '0-0', key: '0-0' }, - { label: 'pNode 02', value: '0-1', key: '0-1' }, - { label: 'pNode 03', value: '0-2', key: '0-2', isLeaf: true }, - ], - // value: '0-0', - value: { value: '0-0-0-value', label: '0-0-0-label' }, - }; - - onChange = value => { - console.log(value); - this.setState({ - value, - }); - }; - - onLoadData = treeNode => { - console.log(treeNode); - return new Promise(resolve => { - setTimeout(() => { - let { treeData } = this.state; - treeData = treeData.slice(); - getNewTreeData(treeData, treeNode.props.eventKey, generateTreeNodes(treeNode), 2); - this.setState({ treeData }); - resolve(); - }, 500); - }); - }; - - render() { - const { treeData, value } = this.state; - return ( -
-

dynamic render

- -
- ); - } -} - -ReactDOM.render(, document.getElementById('__react-content')); diff --git a/examples/dynamic.tsx b/examples/dynamic.tsx new file mode 100644 index 00000000..5738893c --- /dev/null +++ b/examples/dynamic.tsx @@ -0,0 +1,97 @@ +import '../assets/index.less'; +import React from 'react'; +import TreeSelect from '../src'; +import { getNewTreeData, generateTreeNodes } from './utils/dataUtil'; + +function getTreeData() { + return [ + { label: 'pNode 01', value: '0-0', key: '0-0' }, + { label: 'pNode 02', value: '0-1', key: '0-1' }, + { label: 'pNode 03', value: '0-2', key: '0-2', isLeaf: true }, + ]; +} + +class Demo extends React.Component { + static propTypes = {}; + + state = { + treeData: getTreeData(), + // value: '0-0', + value: { value: '0-0-0-value', label: '0-0-0-label' }, + loadedKeys: [], + }; + + onChange = value => { + console.log(value); + this.setState({ + value, + }); + }; + + loadData = treeNode => { + console.log('trigger load:', treeNode); + return new Promise(resolve => { + setTimeout(() => { + let { treeData } = this.state; + treeData = treeData.slice(); + getNewTreeData(treeData, treeNode.props.eventKey, generateTreeNodes(treeNode), 2); + this.setState({ treeData }); + resolve(); + }, 500); + }); + }; + + onTreeLoad = loadedKeys => { + this.setState({ loadedKeys }); + }; + + onResetTree = () => { + this.setState({ + treeData: getTreeData(), + }); + }; + + onResetLoadedKeys = () => { + this.setState({ + loadedKeys: [], + }); + }; + + render() { + const { treeData, value, loadedKeys } = this.state; + return ( +
+

dynamic render

+ +

Controlled

+ + + + +
+ ); + } +} + +export default Demo; diff --git a/examples/filter.html b/examples/filter.html deleted file mode 100644 index b3a42524..00000000 --- a/examples/filter.html +++ /dev/null @@ -1 +0,0 @@ -placeholder \ No newline at end of file diff --git a/examples/filter.js b/examples/filter.tsx similarity index 89% rename from examples/filter.js rename to examples/filter.tsx index 7ba4d7cb..5e6fe3ac 100644 --- a/examples/filter.js +++ b/examples/filter.tsx @@ -1,10 +1,9 @@ -/* eslint react/no-multi-comp:0, no-console:0 */ - -import 'rc-tree-select/assets/index.less'; +import '../assets/index.less'; import React from 'react'; -import ReactDOM from 'react-dom'; -import TreeSelect, { SHOW_PARENT } from 'rc-tree-select'; -import { gData } from './util'; +import TreeSelect, { SHOW_PARENT } from '../src'; +import { gData } from './utils/dataUtil'; + +console.log('TreeData:', gData); class Demo extends React.Component { state = { @@ -62,7 +61,7 @@ class Demo extends React.Component { style={{ width: 300 }} transitionName="rc-tree-select-dropdown-slide-up" choiceTransitionName="rc-tree-select-selection__choice-zoom" - dropdownStyle={{ height: 200, overflow: 'auto' }} + // dropdownStyle={{ height: 200, overflow: 'auto' }} dropdownPopupAlign={{ overflow: { adjustY: 0, adjustX: 0 }, offset: [0, 2] }} placeholder={请下拉选择} searchPlaceholder="please search" @@ -103,4 +102,4 @@ class Demo extends React.Component { } } -ReactDOM.render(, document.getElementById('__react-content')); +export default Demo; diff --git a/examples/form.html b/examples/form.html deleted file mode 100644 index b3a42524..00000000 --- a/examples/form.html +++ /dev/null @@ -1 +0,0 @@ -placeholder \ No newline at end of file diff --git a/examples/form.js b/examples/form.js deleted file mode 100644 index afebebeb..00000000 --- a/examples/form.js +++ /dev/null @@ -1,129 +0,0 @@ -/* eslint react/no-multi-comp:0, no-console:0 */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; -import TreeSelect from 'rc-tree-select'; -import Select from 'rc-select'; -import { createForm } from 'rc-form'; -import 'rc-select/assets/index.css'; -import 'rc-tree-select/assets/index.less'; -import { regionStyle, errorStyle } from './styles'; -import { gData } from './util'; -import './demo.less'; - -const { Option } = Select; - -class TreeSelectInput extends Component { - onChange = (value, ...args) => { - console.log(value, ...args); - const props = this.props; - if (props.onChange) { - props.onChange(value); - } - }; - - render() { - return ; - } -} - -// @createForm() -class Form extends Component { - static propTypes = { - form: PropTypes.object, - }; - - onSubmit = e => { - const { form } = this.props; - console.log('submit'); - e.preventDefault(); - form.validateFields((error, values) => { - if (!error) { - console.log('ok', values); - } else { - console.log('error', error, values); - } - }); - }; - - reset = e => { - const { form } = this.props; - e.preventDefault(); - form.resetFields(); - }; - - render() { - const { form } = this.props; - const { getFieldDecorator, getFieldError } = form; - const tProps = { - multiple: true, - treeData: gData, - treeCheckable: true, - // treeDefaultExpandAll: true, - }; - return ( -
-

validity

-
-
-
-

no onChange

- {getFieldDecorator('tree-select', { - initialValue: ['0-0-0-value'], - rules: [{ required: true, type: 'array', message: 'tree-select 需要必填' }], - })()} -
-

- {getFieldError('tree-select') ? getFieldError('tree-select').join(',') : null} -

-
- -
-
-

custom onChange

- {getFieldDecorator('tree-select1', { - initialValue: ['0-0-0-value'], - rules: [{ required: true, type: 'array', message: 'tree-select1 需要必填' }], - })()} -
-

- {getFieldError('tree-select1') ? getFieldError('tree-select1').join(',') : null} -

-
- -
- {getFieldDecorator('select', { - initialValue: 'jack', - rules: [{ required: true, type: 'array', message: 'select 需要必填' }], - })( - , - )} -

- {getFieldError('select') ? getFieldError('select').join(',') : null} -

-
- -
- -   - -
-
-
- ); - } -} - -// ReactDOM.render(
, document.getElementById('__react-content')); -const NewForm = createForm()(Form); -ReactDOM.render(, document.getElementById('__react-content')); diff --git a/examples/form.tsx b/examples/form.tsx new file mode 100644 index 00000000..66d27e9e --- /dev/null +++ b/examples/form.tsx @@ -0,0 +1,138 @@ +import React, { Component } from 'react'; +import Select from 'rc-select'; +import Form, { useForm, Field } from 'rc-field-form'; +import TreeSelect from '../src'; +import 'rc-select/assets/index.less'; +import '../assets/index.less'; +import { gData } from './utils/dataUtil'; + +const { Option } = Select; + +const regionStyle = { + border: '1px solid red', + marginTop: 10, + padding: 10, +}; + +const errorStyle = { + color: 'red', + marginTop: 10, + padding: 10, +}; + +class TreeSelectInput extends Component<{ onChange?: Function; style: React.CSSProperties }> { + onChange = (value, ...args) => { + console.log(value, ...args); + const { props } = this; + if (props.onChange) { + props.onChange(value); + } + }; + + render() { + return ; + } +} + +const Demo = () => { + const [form] = useForm(); + + const onFinish = values => { + console.log('Submit:', values); + }; + + const onReset = () => { + form.resetFields(); + }; + + const tProps = { + multiple: true, + treeData: gData, + treeCheckable: true, + }; + + return ( +
+

validity

+ +
+
+

no onChange

+ + {(control, { errors }) => ( +
+ + +

{errors.join(',')}

+
+ )} +
+
+
+ +
+
+

custom onChange

+ + {(control, { errors }) => ( +
+ + +

{errors.join(',')}

+
+ )} +
+
+
+ +
+
+ + {(control, { errors }) => ( +
+ + +

{errors.join(',')}

+
+ )} +
+
+
+ +
+ +   + +
+ +
+ ); +}; + +export default Demo; diff --git a/examples/styles.js b/examples/styles.js deleted file mode 100644 index 39b4c0a5..00000000 --- a/examples/styles.js +++ /dev/null @@ -1,11 +0,0 @@ -export const regionStyle = { - border: '1px solid red', - marginTop: 10, - padding: 10, -}; - -export const errorStyle = { - color: 'red', - marginTop: 10, - padding: 10, -}; diff --git a/examples/big-data-generator.js b/examples/utils/big-data-generator.tsx similarity index 70% rename from examples/big-data-generator.js rename to examples/utils/big-data-generator.tsx index 8ec86807..ee293da1 100644 --- a/examples/big-data-generator.js +++ b/examples/utils/big-data-generator.tsx @@ -1,16 +1,14 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { generateData, calcTotal } from './util'; -import { createRef } from '../src/util'; +import { generateData, calcTotal } from './dataUtil'; -class Gen extends React.Component { - static propTypes = { - onGen: PropTypes.func, - x: PropTypes.number, - y: PropTypes.number, - z: PropTypes.number, - }; +interface GenProps { + onGen: Function; + x: number; + y: number; + z: number; +} +class Gen extends React.Component { static defaultProps = { onGen: () => {}, x: 20, @@ -18,12 +16,11 @@ class Gen extends React.Component { z: 1, }; - constructor() { - super(); - this.xRef = createRef(); - this.yRef = createRef(); - this.zRef = createRef(); - } + xRef = React.createRef(); + + yRef = React.createRef(); + + zRef = React.createRef(); state = { nums: '', @@ -45,12 +42,17 @@ class Gen extends React.Component { }); }; - getVals = () => { - return { - x: parseInt(this.xRef.current.value, 10), - y: parseInt(this.yRef.current.value, 10), - z: parseInt(this.zRef.current.value, 10), - }; + getVals = () => ({ + x: parseInt(this.xRef.current.value, 10), + y: parseInt(this.yRef.current.value, 10), + z: parseInt(this.zRef.current.value, 10), + }); + + getDefaultValue = (key: string) => { + if (key in this.props) { + return String(this.props[key]); + } + return null; }; render() { @@ -64,7 +66,7 @@ class Gen extends React.Component { x:{' '} n >= 0 ? x * (y ** (n--)) + rec(n) : 0; + const rec = n => (n >= 0 ? x * y ** n-- + rec(n) : 0); return rec(z + 1); } console.log('总节点数(单个tree):', calcTotal()); @@ -53,9 +52,12 @@ export function generateTreeNodes(treeNode) { function setLeaf(treeData, curKey, level) { const loopLeaf = (data, lev) => { const l = lev - 1; - data.forEach((item) => { - if ((item.key.length > curKey.length) ? item.key.indexOf(curKey) !== 0 : - curKey.indexOf(item.key) !== 0) { + data.forEach(item => { + if ( + item.key.length > curKey.length + ? item.key.indexOf(curKey) !== 0 + : curKey.indexOf(item.key) !== 0 + ) { return; } if (item.children) { @@ -69,9 +71,9 @@ function setLeaf(treeData, curKey, level) { } export function getNewTreeData(treeData, curKey, child, level) { - const loop = (data) => { + const loop = data => { if (level < 1 || curKey.length - 3 > level * 2) return; - data.forEach((item) => { + data.forEach(item => { if (curKey.indexOf(item.key) === 0) { if (item.children) { loop(item.children); @@ -85,7 +87,6 @@ export function getNewTreeData(treeData, curKey, child, level) { setLeaf(treeData, curKey, level); } - function loopData(data, callback) { const loop = (d, level = 0) => { d.forEach((item, index) => { @@ -104,7 +105,7 @@ function isPositionPrefix(smallPos, bigPos) { return false; } // attention: "0-0-1" "0-0-10" - if ((bigPos.length > smallPos.length) && (bigPos.charAt(smallPos.length) !== '-')) { + if (bigPos.length > smallPos.length && bigPos.charAt(smallPos.length) !== '-') { return false; } return bigPos.substr(0, smallPos.length) === smallPos; @@ -123,8 +124,8 @@ export function getFilterValue(val, sVal, delVal) { } }); const newPos = []; - delPos.forEach((item) => { - allPos.forEach((i) => { + delPos.forEach(item => { + allPos.forEach(i => { if (isPositionPrefix(item, i) || isPositionPrefix(i, item)) { // 过滤掉 父级节点 和 所有子节点。 // 因为 node节点 不选时,其 父级节点 和 所有子节点 都不选。 diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..0c6601ae --- /dev/null +++ b/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + setupFiles: ['./tests/setup.js'], + snapshotSerializers: [require.resolve('enzyme-to-json/serializer')], +}; \ No newline at end of file diff --git a/now.json b/now.json index 8b975ffc..c62f59c3 100644 --- a/now.json +++ b/now.json @@ -6,7 +6,7 @@ { "src": "package.json", "use": "@now/static-build", - "config": { "distDir": "build" } + "config": { "distDir": ".doc" } } ] } \ No newline at end of file diff --git a/package.json b/package.json index f218adf2..d5180142 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "tree-select" ], "homepage": "https://github.com/react-component/tree-select", - "author": "hualei5280@gmail.com", + "author": "smith3816@gmail.com", "repository": { "type": "git", "url": "https://github.com/react-component/tree-select.git" @@ -21,6 +21,7 @@ "es", "lib", "dist", + "assets/*.less", "assets/*.css", "assets/*.png", "assets/*.gif" @@ -28,58 +29,41 @@ "license": "MIT", "main": "./lib/index", "module": "./es/index", - "config": { - "port": 8007, - "entry": { - "rc-tree-select": [ - "./assets/index.less", - "./src/index.js" - ] - } - }, "scripts": { - "build": "rc-tools run build", - "compile": "rc-tools run compile --babel-runtime", - "gh-pages": "rc-tools run gh-pages", - "start": "rc-tools run server", - "pub": "rc-tools run pub", - "lint": "rc-tools run lint", - "test": "rc-tools run test", - "coverage": "rc-tools run test --coverage", - "pre-commit": "rc-tools run pre-commit", - "lint-staged": "lint-staged", + "start": "cross-env NODE_ENV=development father doc dev --storybook", + "build": "father doc build --storybook", + "compile": "father build", + "prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish", + "lint": "eslint src/ examples/ --ext .tsx,.ts,.jsx,.js", + "test": "father test", "now-build": "npm run build" }, - "dependencies": { - "classnames": "^2.2.1", - "dom-scroll-into-view": "^1.2.1", - "prop-types": "^15.5.8", - "raf": "^3.4.0", - "rc-animate": "^2.8.2", - "rc-tree": "~2.0.0", - "rc-trigger": "^3.0.0-rc.2", - "rc-util": "^4.5.0", - "react-lifecycles-compat": "^3.0.4", - "shallowequal": "^1.0.2", - "warning": "^4.0.1" + "peerDependencies": { + "react": "*", + "react-dom": "*" }, "devDependencies": { - "lint-staged": "^8.1.1", - "pre-commit": "1.x", - "rc-dialog": "^7.0.0", - "rc-form": "^2.4.2", - "rc-select": "^8.8.3", - "rc-tools": "^9.3.5", - "react": "^16.0.0", - "react-dom": "^16.0.0" + "@types/react": "^16.8.19", + "@types/react-dom": "^16.8.4", + "@types/warning": "^3.0.0", + "cross-env": "^5.2.0", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.1.1", + "enzyme-to-json": "^3.4.0", + "father": "^2.13.2", + "np": "^5.0.3", + "rc-dialog": "^7.5.7", + "rc-field-form": "^0.0.0-alpha.21", + "rc-trigger": "^2.6.5", + "rc-virtual-list": "^0.0.0-alpha.28", + "react": "^16.8.0", + "react-dom": "^16.8.0", + "typescript": "^3.5.2" }, - "pre-commit": [ - "lint-staged" - ], - "lint-staged": { - "*.{js,jsx,ts,tsx}": [ - "npm run pre-commit", - "git add" - ] + "dependencies": { + "classnames": "2.x", + "rc-select": "^10.0.0-alpha.24", + "rc-tree": "^3.0.0-alpha.35", + "rc-util": "^4.9.0" } } diff --git a/src/Base/BasePopup.jsx b/src/Base/BasePopup.jsx deleted file mode 100644 index 3f751f43..00000000 --- a/src/Base/BasePopup.jsx +++ /dev/null @@ -1,273 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { polyfill } from 'react-lifecycles-compat'; - -import Tree from 'rc-tree'; -import { createRef } from '../util'; - -export const popupContextTypes = { - onPopupKeyDown: PropTypes.func.isRequired, - onTreeNodeSelect: PropTypes.func.isRequired, - onTreeNodeCheck: PropTypes.func.isRequired, -}; - -class BasePopup extends React.Component { - static propTypes = { - prefixCls: PropTypes.string, - upperSearchValue: PropTypes.string, - valueList: PropTypes.array, - searchHalfCheckedKeys: PropTypes.array, - valueEntities: PropTypes.object, - keyEntities: PropTypes.object, - treeIcon: PropTypes.bool, - treeLine: PropTypes.bool, - treeNodeFilterProp: PropTypes.string, - treeCheckable: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]), - treeCheckStrictly: PropTypes.bool, - treeDefaultExpandAll: PropTypes.bool, - treeDefaultExpandedKeys: PropTypes.array, - treeExpandedKeys: PropTypes.array, - loadData: PropTypes.func, - multiple: PropTypes.bool, - onTreeExpand: PropTypes.func, - - treeNodes: PropTypes.node, - filteredTreeNodes: PropTypes.node, - notFoundContent: PropTypes.node, - - ariaId: PropTypes.string, - switcherIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - - // HOC - renderSearch: PropTypes.func, - onTreeExpanded: PropTypes.func, - }; - - static contextTypes = { - rcTreeSelect: PropTypes.shape({ - ...popupContextTypes, - }), - }; - - constructor(props) { - super(); - - const { treeDefaultExpandAll, treeDefaultExpandedKeys, keyEntities } = props; - - // TODO: make `expandedKeyList` control - let expandedKeyList = treeDefaultExpandedKeys; - if (treeDefaultExpandAll) { - expandedKeyList = Object.keys(keyEntities); - } - - this.state = { - keyList: [], - expandedKeyList, - // Cache `expandedKeyList` when tree is in filter. This is used in `getDerivedStateFromProps` - cachedExpandedKeyList: [], // eslint-disable-line react/no-unused-state - loadedKeys: [], - }; - - this.treeRef = createRef(); - } - - static getDerivedStateFromProps(nextProps, prevState) { - const { prevProps = {}, loadedKeys, expandedKeyList, cachedExpandedKeyList } = prevState || {}; - const { - valueList, - valueEntities, - keyEntities, - treeExpandedKeys, - filteredTreeNodes, - upperSearchValue, - } = nextProps; - - const newState = { - prevProps: nextProps, - }; - - // Check value update - if (valueList !== prevProps.valueList) { - newState.keyList = valueList - .map(({ value }) => valueEntities[value]) - .filter(entity => entity) - .map(({ key }) => key); - } - - // Show all when tree is in filter mode - if ( - !treeExpandedKeys && - filteredTreeNodes && - filteredTreeNodes.length && - filteredTreeNodes !== prevProps.filteredTreeNodes - ) { - newState.expandedKeyList = Object.keys(keyEntities); - } - - // Cache `expandedKeyList` when filter set - if (upperSearchValue && !prevProps.upperSearchValue) { - newState.cachedExpandedKeyList = expandedKeyList; - } else if (!upperSearchValue && prevProps.upperSearchValue && !treeExpandedKeys) { - newState.expandedKeyList = cachedExpandedKeyList || []; - newState.cachedExpandedKeyList = []; - } - - // Use expandedKeys if provided - if (prevProps.treeExpandedKeys !== treeExpandedKeys) { - newState.expandedKeyList = treeExpandedKeys; - } - - // Clean loadedKeys if key not exist in keyEntities anymore - if (nextProps.loadData) { - newState.loadedKeys = loadedKeys.filter(key => key in keyEntities); - } - - return newState; - } - - onTreeExpand = expandedKeyList => { - const { treeExpandedKeys, onTreeExpand, onTreeExpanded } = this.props; - - // Set uncontrolled state - if (!treeExpandedKeys) { - this.setState({ expandedKeyList }, onTreeExpanded); - } - - if (onTreeExpand) { - onTreeExpand(expandedKeyList); - } - }; - - onLoad = loadedKeys => { - this.setState({ loadedKeys }); - }; - - getTree = () => { - return this.treeRef.current; - }; - - /** - * Not pass `loadData` when searching. To avoid loop ajax call makes browser crash. - */ - getLoadData = () => { - const { loadData, upperSearchValue } = this.props; - if (upperSearchValue) return null; - return loadData; - }; - - /** - * This method pass to Tree component which is used for add filtered class - * in TreeNode > li - */ - filterTreeNode = treeNode => { - const { upperSearchValue, treeNodeFilterProp } = this.props; - - const filterVal = treeNode.props[treeNodeFilterProp]; - if (typeof filterVal === 'string') { - return upperSearchValue && filterVal.toUpperCase().indexOf(upperSearchValue) !== -1; - } - - return false; - }; - - renderNotFound = () => { - const { prefixCls, notFoundContent } = this.props; - - return {notFoundContent}; - }; - - render() { - const { keyList, expandedKeyList, loadedKeys } = this.state; - const { - prefixCls, - treeNodes, - filteredTreeNodes, - treeIcon, - treeLine, - treeCheckable, - treeCheckStrictly, - multiple, - ariaId, - renderSearch, - switcherIcon, - searchHalfCheckedKeys, - } = this.props; - const { - rcTreeSelect: { onPopupKeyDown, onTreeNodeSelect, onTreeNodeCheck }, - } = this.context; - - const loadData = this.getLoadData(); - - const treeProps = {}; - - if (treeCheckable) { - treeProps.checkedKeys = keyList; - } else { - treeProps.selectedKeys = keyList; - } - - let $notFound; - let $treeNodes; - if (filteredTreeNodes) { - if (filteredTreeNodes.length) { - treeProps.checkStrictly = true; - $treeNodes = filteredTreeNodes; - - // Fill halfCheckedKeys - if (treeCheckable && !treeCheckStrictly) { - treeProps.checkedKeys = { - checked: keyList, - halfChecked: searchHalfCheckedKeys, - }; - } - } else { - $notFound = this.renderNotFound(); - } - } else if (!treeNodes || !treeNodes.length) { - $notFound = this.renderNotFound(); - } else { - $treeNodes = treeNodes; - } - - let $tree; - if ($notFound) { - $tree = $notFound; - } else { - $tree = ( - - {$treeNodes} - - ); - } - - return ( -
- {renderSearch ? renderSearch() : null} - {$tree} -
- ); - } -} - -polyfill(BasePopup); - -export default BasePopup; diff --git a/src/Base/BaseSelector.jsx b/src/Base/BaseSelector.jsx deleted file mode 100644 index ba07d30e..00000000 --- a/src/Base/BaseSelector.jsx +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Input Box is in different position for different mode. - * This not the same design as `Select` cause it's followed by antd 0.x `Select`. - * We will not follow the new design immediately since antd 3.x is already released. - * - * So this file named as Selector to avoid confuse. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { polyfill } from 'react-lifecycles-compat'; -import { createRef } from '../util'; - -export const selectorPropTypes = { - prefixCls: PropTypes.string, - className: PropTypes.string, - style: PropTypes.object, - open: PropTypes.bool, - selectorValueList: PropTypes.array, - allowClear: PropTypes.bool, - showArrow: PropTypes.bool, - onClick: PropTypes.func, - onBlur: PropTypes.func, - onFocus: PropTypes.func, - removeSelected: PropTypes.func, - - // Pass by component - ariaId: PropTypes.string, - inputIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - clearIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), -}; - -export const selectorContextTypes = { - onSelectorFocus: PropTypes.func.isRequired, - onSelectorBlur: PropTypes.func.isRequired, - onSelectorKeyDown: PropTypes.func.isRequired, - onSelectorClear: PropTypes.func.isRequired, -}; - -export default function (modeName) { - class BaseSelector extends React.Component { - static propTypes = { - ...selectorPropTypes, - - // Pass by HOC - renderSelection: PropTypes.func.isRequired, - renderPlaceholder: PropTypes.func, - tabIndex: PropTypes.number, - }; - - static contextTypes = { - rcTreeSelect: PropTypes.shape({ - ...selectorContextTypes, - }), - }; - - static defaultProps = { - tabIndex: 0, - } - - constructor() { - super(); - - this.domRef = createRef(); - } - - onFocus = (...args) => { - const { onFocus, focused } = this.props; - const { rcTreeSelect: { onSelectorFocus } } = this.context; - - if (!focused) { - onSelectorFocus(); - } - - if (onFocus) { - onFocus(...args); - } - }; - - onBlur = (...args) => { - const { onBlur } = this.props; - const { rcTreeSelect: { onSelectorBlur } } = this.context; - - // TODO: Not trigger when is inner component get focused - onSelectorBlur(); - - if (onBlur) { - onBlur(...args); - } - }; - - focus = () => { - this.domRef.current.focus(); - } - - blur = () => { - this.domRef.current.focus(); - } - - renderClear() { - const { prefixCls, allowClear, selectorValueList, clearIcon } = this.props; - const { rcTreeSelect: { onSelectorClear } } = this.context; - - if (!allowClear || !selectorValueList.length || !selectorValueList[0].value) { - return null; - } - - return ( - - {typeof clearIcon === 'function' ? - React.createElement(clearIcon, { ...this.props }) : clearIcon} - - ); - } - - renderArrow() { - const { prefixCls, showArrow, inputIcon } = this.props; - if (!showArrow) { - return null; - } - - return ( - - {typeof inputIcon === 'function' ? - React.createElement(inputIcon, { ...this.props }) : inputIcon} - - ); - } - - render() { - const { - prefixCls, className, style, - open, focused, disabled, allowClear, - onClick, - ariaId, - renderSelection, renderPlaceholder, - tabIndex, - } = this.props; - const { rcTreeSelect: { onSelectorKeyDown } } = this.context; - - let myTabIndex = tabIndex; - if (disabled) { - myTabIndex = null; - } - - return ( - - - {renderSelection()} - {this.renderClear()} - {this.renderArrow()} - - {renderPlaceholder && renderPlaceholder()} - - - ); - } - } - - polyfill(BaseSelector); - - return BaseSelector; -} diff --git a/src/Context.tsx b/src/Context.tsx new file mode 100644 index 00000000..7a81bb52 --- /dev/null +++ b/src/Context.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { IconType } from 'rc-tree/lib/interface'; +import { Key, LegacyDataNode } from './interface'; + +interface ContextProps { + checkable: boolean; + checkedKeys: Key[]; + halfCheckedKeys: Key[]; + treeExpandedKeys: Key[]; + treeDefaultExpandedKeys: Key[]; + onTreeExpand: (keys: Key[]) => void; + treeDefaultExpandAll: boolean; + treeIcon: IconType; + switcherIcon: IconType; + treeLine: boolean; + treeNodeFilterProp: string; + treeLoadedKeys: Key[]; + loadData: (treeNode: LegacyDataNode) => Promise; + onTreeLoad: (loadedKeys: Key[]) => void; +} + +export const SelectContext = React.createContext(null); diff --git a/src/OptionList.tsx b/src/OptionList.tsx new file mode 100644 index 00000000..d77256d3 --- /dev/null +++ b/src/OptionList.tsx @@ -0,0 +1,261 @@ +import React from 'react'; +import KeyCode from 'rc-util/lib/KeyCode'; +import { RefOptionListProps } from 'rc-select/lib/OptionList'; +import Tree, { TreeProps } from 'rc-tree'; +import { EventDataNode } from 'rc-tree/lib/interface'; +import { FlattenDataNode, RawValueType, DataNode, TreeDataNode, Key } from './interface'; +import { SelectContext } from './Context'; +import useKeyValueMapping from './hooks/useKeyValueMapping'; +import useKeyValueMap from './hooks/useKeyValueMap'; + +const HIDDEN_STYLE = { + width: 0, + height: 0, + display: 'flex', + overflow: 'hidden', + opacity: 0, + border: 0, + padding: 0, + margin: 0, +}; + +interface TreeEventInfo { + node: { key: Key }; + selected?: boolean; + checked?: boolean; +} + +export interface OptionListProps { + prefixCls: string; + id: string; + options: OptionsType; + flattenOptions: FlattenDataNode[]; + height: number; + itemHeight: number; + values: Set; + multiple: boolean; + open: boolean; + defaultActiveFirstOption?: boolean; + notFoundContent?: React.ReactNode; + menuItemSelectedIcon?: any; + childrenAsData: boolean; + searchValue: string; + + onSelect: (value: RawValueType, option: { selected: boolean }) => void; + onToggleOpen: (open?: boolean) => void; + /** Tell Select that some value is now active to make accessibility work */ + onActiveValue: (value: RawValueType, index: number) => void; + onScroll: React.UIEventHandler; +} + +const OptionList: React.RefForwardingComponent> = ( + props, + ref, +) => { + const { + prefixCls, + height, + itemHeight, + options, + flattenOptions, + multiple, + searchValue, + onSelect, + onToggleOpen, + open, + notFoundContent, + } = props; + const { + checkable, + checkedKeys, + halfCheckedKeys, + treeExpandedKeys, + treeDefaultExpandAll, + treeDefaultExpandedKeys, + onTreeExpand, + treeIcon, + switcherIcon, + treeLine, + treeNodeFilterProp, + loadData, + treeLoadedKeys, + onTreeLoad, + } = React.useContext(SelectContext); + + const treeRef = React.useRef(); + + const [cacheKeyMap, cacheValueMap] = useKeyValueMap(flattenOptions); + const [getEntityByKey, getEntityByValue] = useKeyValueMapping(cacheKeyMap, cacheValueMap); + + // ========================== Values ========================== + const valueKeys = React.useMemo( + () => + checkedKeys.map(val => { + const entity = getEntityByValue(val); + return entity ? entity.key : null; + }), + [checkedKeys], + ); + + const mergedCheckedKeys = React.useMemo(() => { + if (!checkable) { + return null; + } + + return { + checked: valueKeys, + halfChecked: halfCheckedKeys, + }; + }, [valueKeys, halfCheckedKeys, checkable]); + + // ========================== Scroll ========================== + React.useEffect(() => { + // Single mode should scroll to current key + if (open && !multiple && valueKeys.length) { + treeRef.current.scrollTo({ key: valueKeys[0] }); + } + }, [open]); + + // ========================== Search ========================== + const lowerSearchValue = String(searchValue).toLowerCase(); + const filterTreeNode = (treeNode: EventDataNode) => { + if (!lowerSearchValue) { + return false; + } + return String(treeNode[treeNodeFilterProp]) + .toLowerCase() + .includes(lowerSearchValue); + }; + + // =========================== Keys =========================== + const [expandedKeys, setExpandedKeys] = React.useState(treeDefaultExpandedKeys); + const [searchExpandedKeys, setSearchExpandedKeys] = React.useState(null); + const mergedExpandedKeys = treeExpandedKeys || (searchValue ? searchExpandedKeys : expandedKeys); + + React.useEffect(() => { + if (searchValue) { + setSearchExpandedKeys(flattenOptions.map(o => o.key)); + } + }, [searchValue]); + + const onInternalExpand = (keys: Key[]) => { + setExpandedKeys(keys); + setSearchExpandedKeys(keys); + + if (onTreeExpand) { + onTreeExpand(keys); + } + }; + + // ========================== Events ========================== + const onListMouseDown: React.MouseEventHandler = event => { + event.preventDefault(); + }; + + const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => { + const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select'); + if (entity !== null) { + onSelect(entity.data.value, { selected: !checkedKeys.includes(entity.data.value) }); + } + + if (!multiple) { + onToggleOpen(false); + } + }; + + // ========================= Keyboard ========================= + const [activeKey, setActiveKey] = React.useState(null); + const activeEntity = getEntityByKey(activeKey); + + React.useImperativeHandle(ref, () => ({ + onKeyDown: event => { + const { which } = event; + switch (which) { + // >>> Arrow keys + case KeyCode.UP: + case KeyCode.DOWN: + case KeyCode.LEFT: + case KeyCode.RIGHT: + treeRef.current.onKeyDown(event as React.KeyboardEvent); + break; + + // >>> Select item + case KeyCode.ENTER: { + if (activeEntity !== null) { + onInternalSelect(null, { + node: { key: activeKey }, + selected: !checkedKeys.includes(activeEntity.data.value), + }); + } + break; + } + + // >>> Close + case KeyCode.ESC: { + onToggleOpen(false); + } + } + }, + onKeyUp: () => {}, + })); + + // ========================== Render ========================== + if (options.length === 0) { + return ( +
+ {notFoundContent} +
+ ); + } + + const treeProps: Partial = {}; + if (treeLoadedKeys) { + treeProps.loadedKeys = treeLoadedKeys; + } + if (mergedExpandedKeys) { + treeProps.expandedKeys = mergedExpandedKeys; + } + + return ( +
+ {activeEntity && open && ( + + {activeEntity.data.value} + + )} + + +
+ ); +}; + +const RefOptionList = React.forwardRef>(OptionList); +RefOptionList.displayName = 'OptionList'; + +export default RefOptionList; diff --git a/src/Popup/MultiplePopup.jsx b/src/Popup/MultiplePopup.jsx deleted file mode 100644 index 09b09a27..00000000 --- a/src/Popup/MultiplePopup.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import BasePopup from '../Base/BasePopup'; - -export default BasePopup; diff --git a/src/Popup/SinglePopup.jsx b/src/Popup/SinglePopup.jsx deleted file mode 100644 index 80bfd202..00000000 --- a/src/Popup/SinglePopup.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import BasePopup from '../Base/BasePopup'; -import SearchInput from '../SearchInput'; -import { createRef } from '../util'; - -class SinglePopup extends React.Component { - static propTypes = { - ...BasePopup.propTypes, - searchValue: PropTypes.string, - showSearch: PropTypes.bool, - dropdownPrefixCls: PropTypes.string, - disabled: PropTypes.bool, - searchPlaceholder: PropTypes.string, - }; - - constructor() { - super(); - - this.inputRef = createRef(); - this.searchRef = createRef(); - this.popupRef = createRef(); - } - - onPlaceholderClick = () => { - this.inputRef.current.focus(); - }; - - getTree = () => { - return this.popupRef.current && this.popupRef.current.getTree(); - }; - - renderPlaceholder = () => { - const { searchPlaceholder, searchValue, prefixCls } = this.props; - - if (!searchPlaceholder) { - return null; - } - - return ( - - {searchPlaceholder} - - ); - }; - - renderSearch = () => { - const { showSearch, dropdownPrefixCls } = this.props; - - if (!showSearch) { - return null; - } - - return ( - - - - ); - }; - - render() { - return ; - } -} - -export default SinglePopup; diff --git a/src/SearchInput.jsx b/src/SearchInput.jsx deleted file mode 100644 index 90954c38..00000000 --- a/src/SearchInput.jsx +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Since search box is in different position with different mode. - * - Single: in the popup box - * - multiple: in the selector - * Move the code as a SearchInput for easy management. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { polyfill } from 'react-lifecycles-compat'; -import { createRef } from './util'; - -export const searchContextTypes = { - onSearchInputChange: PropTypes.func.isRequired, -}; - -class SearchInput extends React.Component { - static propTypes = { - open: PropTypes.bool, - searchValue: PropTypes.string, - prefixCls: PropTypes.string, - disabled: PropTypes.bool, - renderPlaceholder: PropTypes.func, - needAlign: PropTypes.bool, - ariaId: PropTypes.string, - }; - - static contextTypes = { - rcTreeSelect: PropTypes.shape({ - ...searchContextTypes, - }), - }; - - constructor() { - super(); - - this.inputRef = createRef(); - this.mirrorInputRef = createRef(); - } - - componentDidMount() { - const { open, needAlign } = this.props; - if (needAlign) { - this.alignInputWidth(); - } - - if (open) { - this.focus(true); - } - } - - componentDidUpdate(prevProps) { - const { open, searchValue, needAlign } = this.props; - - if (open && prevProps.open !== open) { - this.focus(); - } - - - if (needAlign && searchValue !== prevProps.searchValue) { - this.alignInputWidth(); - } - } - - /** - * `scrollWidth` is not correct in IE, do the workaround. - * ref: https://github.com/react-component/tree-select/issues/65 - */ - alignInputWidth = () => { - this.inputRef.current.style.width = - `${this.mirrorInputRef.current.clientWidth}px`; - }; - - /** - * Need additional timeout for focus cause parent dom is not ready when didMount trigger - */ - focus = (isDidMount) => { - if (this.inputRef.current) { - this.inputRef.current.focus(); - if (isDidMount) { - setTimeout(() => { - this.inputRef.current.focus(); - }, 0); - } - } - }; - - blur = () => { - if (this.inputRef.current) { - this.inputRef.current.blur(); - } - }; - - render() { - const { searchValue, prefixCls, disabled, renderPlaceholder, open, ariaId } = this.props; - const { rcTreeSelect: { - onSearchInputChange, onSearchInputKeyDown, - } } = this.context; - - return ( - - - - {searchValue}  - - - {renderPlaceholder ? renderPlaceholder() : null} - - ); - } -} - -polyfill(SearchInput); - -export default SearchInput; diff --git a/src/Select.jsx b/src/Select.jsx deleted file mode 100644 index cc03590c..00000000 --- a/src/Select.jsx +++ /dev/null @@ -1,1068 +0,0 @@ -/** - * ARIA: https://www.w3.org/TR/wai-aria/#combobox - * Sample 1: https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/combobox/aria1.1pattern/listbox-combo.html - * Sample 2: https://www.w3.org/blog/wai-components-gallery/widget/combobox-with-aria-autocompleteinline/ - * - * Tab logic: - * Popup is close - * 1. Focus input (mark component as focused) - * 2. Press enter to show the popup - * 3. If popup has input, focus it - * - * Popup is open - * 1. press tab to close the popup - * 2. Focus back to the selection input box - * 3. Let the native tab going on - * - * TreeSelect use 2 design type. - * In single mode, we should focus on the `span` - * In multiple mode, we should focus on the `input` - */ - -import React from 'react'; -import { findDOMNode } from 'react-dom'; -import PropTypes from 'prop-types'; -import { polyfill } from 'react-lifecycles-compat'; -import KeyCode from 'rc-util/lib/KeyCode'; -import shallowEqual from 'shallowequal'; -import raf from 'raf'; -import scrollIntoView from 'dom-scroll-into-view'; - -import SelectTrigger from './SelectTrigger'; -import { selectorContextTypes } from './Base/BaseSelector'; -import { popupContextTypes } from './Base/BasePopup'; -import SingleSelector from './Selector/SingleSelector'; -import MultipleSelector, { multipleSelectorContextTypes } from './Selector/MultipleSelector'; -import SinglePopup from './Popup/SinglePopup'; -import MultiplePopup from './Popup/MultiplePopup'; - -import { SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './strategies'; - -import { - createRef, - generateAriaId, - formatInternalValue, - formatSelectorValue, - parseSimpleTreeData, - convertDataToTree, - convertTreeToEntities, - conductCheck, - getHalfCheckedKeys, - flatToHierarchy, - isPosRelated, - isLabelInValue, - getFilterTree, - cleanEntity, - findPopupContainer, -} from './util'; -import { valueProp } from './propTypes'; -import SelectNode from './SelectNode'; - -class Select extends React.Component { - static propTypes = { - prefixCls: PropTypes.string, - prefixAria: PropTypes.string, - multiple: PropTypes.bool, - showArrow: PropTypes.bool, - open: PropTypes.bool, - value: valueProp, - autoFocus: PropTypes.bool, - - defaultOpen: PropTypes.bool, - defaultValue: valueProp, - - showSearch: PropTypes.bool, - placeholder: PropTypes.node, - inputValue: PropTypes.string, // [Legacy] Deprecated. Use `searchValue` instead. - searchValue: PropTypes.string, - autoClearSearchValue: PropTypes.bool, - searchPlaceholder: PropTypes.node, // [Legacy] Confuse with placeholder - disabled: PropTypes.bool, - children: PropTypes.node, - labelInValue: PropTypes.bool, - maxTagCount: PropTypes.number, - maxTagPlaceholder: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - maxTagTextLength: PropTypes.number, - showCheckedStrategy: PropTypes.oneOf([SHOW_ALL, SHOW_PARENT, SHOW_CHILD]), - - dropdownMatchSelectWidth: PropTypes.bool, - treeData: PropTypes.array, - treeDataSimpleMode: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), - treeNodeFilterProp: PropTypes.string, - treeNodeLabelProp: PropTypes.string, - treeCheckable: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]), - treeCheckStrictly: PropTypes.bool, - treeIcon: PropTypes.bool, - treeLine: PropTypes.bool, - treeDefaultExpandAll: PropTypes.bool, - treeDefaultExpandedKeys: PropTypes.array, - treeExpandedKeys: PropTypes.array, - loadData: PropTypes.func, - filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), - - notFoundContent: PropTypes.node, - - onSearch: PropTypes.func, - onSelect: PropTypes.func, - onDeselect: PropTypes.func, - onChange: PropTypes.func, - onDropdownVisibleChange: PropTypes.func, - - onTreeExpand: PropTypes.func, - - inputIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - clearIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - removeIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - switcherIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - }; - - static childContextTypes = { - rcTreeSelect: PropTypes.shape({ - ...selectorContextTypes, - ...multipleSelectorContextTypes, - ...popupContextTypes, - - onSearchInputChange: PropTypes.func, - onSearchInputKeyDown: PropTypes.func, - }), - }; - - static defaultProps = { - prefixCls: 'rc-tree-select', - prefixAria: 'rc-tree-select', - showArrow: true, - showSearch: true, - autoClearSearchValue: true, - showCheckedStrategy: SHOW_CHILD, - - // dropdownMatchSelectWidth change the origin design, set to false now - // ref: https://github.com/react-component/select/blob/4cad95e098a341a09de239ad6981067188842020/src/Select.jsx#L344 - // ref: https://github.com/react-component/select/pull/71 - treeNodeFilterProp: 'value', - treeNodeLabelProp: 'title', - treeIcon: false, - notFoundContent: 'Not Found', - }; - - constructor(props) { - super(props); - - const { prefixAria, defaultOpen, open } = props; - - this.state = { - open: open || defaultOpen, - valueList: [], - searchHalfCheckedKeys: [], - missValueList: [], // Contains the value not in the tree - selectorValueList: [], // Used for multiple selector - valueEntities: {}, - keyEntities: {}, - searchValue: '', - - init: true, - }; - - this.selectorRef = createRef(); - this.selectTriggerRef = createRef(); - - // ARIA need `aria-controls` props mapping - // Since this need user input. Let's generate ourselves - this.ariaId = generateAriaId(`${prefixAria}-list`); - } - - getChildContext() { - return { - rcTreeSelect: { - onSelectorFocus: this.onSelectorFocus, - onSelectorBlur: this.onSelectorBlur, - onSelectorKeyDown: this.onComponentKeyDown, - onSelectorClear: this.onSelectorClear, - onMultipleSelectorRemove: this.onMultipleSelectorRemove, - - onTreeNodeSelect: this.onTreeNodeSelect, - onTreeNodeCheck: this.onTreeNodeCheck, - onPopupKeyDown: this.onComponentKeyDown, - - onSearchInputChange: this.onSearchInputChange, - onSearchInputKeyDown: this.onSearchInputKeyDown, - }, - }; - } - - static getDerivedStateFromProps(nextProps, prevState) { - const { prevProps = {} } = prevState; - const { - treeCheckable, - treeCheckStrictly, - filterTreeNode, - treeNodeFilterProp, - treeDataSimpleMode, - } = nextProps; - const newState = { - prevProps: nextProps, - init: false, - }; - - // Process the state when props updated - function processState(propName, updater) { - if (prevProps[propName] !== nextProps[propName]) { - updater(nextProps[propName], prevProps[propName]); - return true; - } - return false; - } - - let valueRefresh = false; - - // Open - processState('open', propValue => { - newState.open = propValue; - }); - - // Tree Nodes - let treeNodes; - let treeDataChanged = false; - let treeDataModeChanged = false; - processState('treeData', propValue => { - treeNodes = convertDataToTree(propValue); - treeDataChanged = true; - }); - - processState('treeDataSimpleMode', (propValue, prevValue) => { - if (!propValue) return; - - const prev = !prevValue || prevValue === true ? {} : prevValue; - - // Shallow equal to avoid dynamic prop object - if (!shallowEqual(propValue, prev)) { - treeDataModeChanged = true; - } - }); - - // Parse by `treeDataSimpleMode` - if (treeDataSimpleMode && (treeDataChanged || treeDataModeChanged)) { - const simpleMapper = { - id: 'id', - pId: 'pId', - rootPId: null, - ...(treeDataSimpleMode !== true ? treeDataSimpleMode : {}), - }; - treeNodes = convertDataToTree(parseSimpleTreeData(nextProps.treeData, simpleMapper)); - } - - // If `treeData` not provide, use children TreeNodes - if (!nextProps.treeData) { - processState('children', propValue => { - treeNodes = Array.isArray(propValue) ? propValue : [propValue]; - }); - } - - // Convert `treeData` to entities - if (treeNodes) { - const entitiesMap = convertTreeToEntities(treeNodes); - newState.treeNodes = treeNodes; - newState.posEntities = entitiesMap.posEntities; - newState.valueEntities = entitiesMap.valueEntities; - newState.keyEntities = entitiesMap.keyEntities; - - valueRefresh = true; - } - - // Value List - if (prevState.init) { - processState('defaultValue', propValue => { - newState.valueList = formatInternalValue(propValue, nextProps); - valueRefresh = true; - }); - } - - processState('value', propValue => { - newState.valueList = formatInternalValue(propValue, nextProps); - valueRefresh = true; - }); - - // Selector Value List - if (valueRefresh) { - // Find out that value not exist in the tree - const missValueList = []; - const filteredValueList = []; - const keyList = []; - - // Get latest value list - let latestValueList = newState.valueList; - if (!latestValueList) { - // Also need add prev missValueList to avoid new treeNodes contains the value - latestValueList = [...prevState.valueList, ...prevState.missValueList]; - } - - // Get key by value - latestValueList.forEach(wrapperValue => { - const { value } = wrapperValue; - const entity = (newState.valueEntities || prevState.valueEntities)[value]; - - if (entity) { - keyList.push(entity.key); - filteredValueList.push(wrapperValue); - return; - } - - // If not match, it may caused by ajax load. We need keep this - missValueList.push(wrapperValue); - }); - - // We need calculate the value when tree is checked tree - if (treeCheckable && !treeCheckStrictly) { - // Calculate the keys need to be checked - const { checkedKeys } = conductCheck( - keyList, - true, - newState.keyEntities || prevState.keyEntities, - ); - - // Format value list again for internal usage - newState.valueList = checkedKeys.map(key => ({ - value: (newState.keyEntities || prevState.keyEntities)[key].value, - })); - } else { - newState.valueList = filteredValueList; - } - - // Fill the missValueList, we still need display in the selector - newState.missValueList = missValueList; - - // Calculate the value list for `Selector` usage - newState.selectorValueList = formatSelectorValue( - newState.valueList, - nextProps, - newState.valueEntities || prevState.valueEntities, - ); - } - - // [Legacy] To align with `Select` component, - // We use `searchValue` instead of `inputValue` but still keep the api - // `inputValue` support `null` to work as `autoClearSearchValue` - processState('inputValue', propValue => { - if (propValue !== null) { - newState.searchValue = propValue; - } - }); - - // Search value - processState('searchValue', propValue => { - newState.searchValue = propValue; - }); - - // Do the search logic - if (newState.searchValue !== undefined || (prevState.searchValue && treeNodes)) { - const searchValue = - newState.searchValue !== undefined ? newState.searchValue : prevState.searchValue; - const upperSearchValue = String(searchValue).toUpperCase(); - - let filterTreeNodeFn = filterTreeNode; - if (filterTreeNode === false) { - // Don't filter if is false - filterTreeNodeFn = () => true; - } else if (typeof filterTreeNodeFn !== 'function') { - // When is not function (true or undefined), use inner filter - filterTreeNodeFn = (_, node) => { - const nodeValue = String(node.props[treeNodeFilterProp]).toUpperCase(); - return nodeValue.indexOf(upperSearchValue) !== -1; - }; - } - - newState.filteredTreeNodes = getFilterTree( - newState.treeNodes || prevState.treeNodes, - searchValue, - filterTreeNodeFn, - newState.valueEntities || prevState.valueEntities, - SelectNode, - ); - } - - // We should re-calculate the halfCheckedKeys when in search mode - if ( - valueRefresh && - treeCheckable && - !treeCheckStrictly && - (newState.searchValue || prevState.searchValue) - ) { - newState.searchHalfCheckedKeys = getHalfCheckedKeys( - newState.valueList, - newState.valueEntities || prevState.valueEntities, - ); - } - - // Checked Strategy - processState('showCheckedStrategy', () => { - newState.selectorValueList = - newState.selectorValueList || - formatSelectorValue( - newState.valueList || prevState.valueList, - nextProps, - newState.valueEntities || prevState.valueEntities, - ); - }); - - return newState; - } - - componentDidMount() { - const { autoFocus, disabled } = this.props; - - if (autoFocus && !disabled) { - this.focus(); - } - } - - componentDidUpdate(_, prevState) { - const { prefixCls } = this.props; - const { valueList, open, selectorValueList, valueEntities } = this.state; - const isMultiple = this.isMultiple(); - - if (prevState.valueList !== valueList) { - this.forcePopupAlign(); - } - - // Scroll to value position, only need sync on single mode - if (!isMultiple && selectorValueList.length && !prevState.open && open && this.popup) { - const { value } = selectorValueList[0]; - const { domTreeNodes } = this.popup.getTree(); - const { key } = valueEntities[value] || {}; - const treeNode = domTreeNodes[key]; - - if (treeNode) { - const domNode = findDOMNode(treeNode); - raf(() => { - const popupNode = findDOMNode(this.popup); - const triggerContainer = findPopupContainer(popupNode, `${prefixCls}-dropdown`); - const searchNode = this.popup.searchRef.current; - - if (domNode && triggerContainer && searchNode) { - scrollIntoView(domNode, triggerContainer, { - onlyScrollIfNeeded: true, - offsetTop: searchNode.offsetHeight, - }); - } - }); - } - } - } - - // ==================== Selector ==================== - onSelectorFocus = () => { - this.setState({ focused: true }); - }; - - onSelectorBlur = () => { - this.setState({ focused: false }); - - // TODO: Close when Popup is also not focused - // this.setState({ open: false }); - }; - - // Handle key board event in both Selector and Popup - onComponentKeyDown = event => { - const { open } = this.state; - const { keyCode } = event; - - if (!open) { - if ([KeyCode.ENTER, KeyCode.DOWN].indexOf(keyCode) !== -1) { - this.setOpenState(true); - } - } else if (KeyCode.ESC === keyCode) { - this.setOpenState(false); - } else if ([KeyCode.UP, KeyCode.DOWN, KeyCode.LEFT, KeyCode.RIGHT].indexOf(keyCode) !== -1) { - // TODO: Handle `open` state - event.stopPropagation(); - } - }; - - onDeselect = (wrappedValue, node, nodeEventInfo) => { - const { onDeselect } = this.props; - if (!onDeselect) return; - - onDeselect(wrappedValue, node, nodeEventInfo); - }; - - onSelectorClear = event => { - const { disabled } = this.props; - if (disabled) return; - - this.triggerChange([], []); - - if (!this.isSearchValueControlled()) { - this.setUncontrolledState({ - searchValue: '', - filteredTreeNodes: null, - }); - } - - event.stopPropagation(); - }; - - onMultipleSelectorRemove = (event, removeValue) => { - event.stopPropagation(); - - const { valueList, missValueList, valueEntities } = this.state; - - const { treeCheckable, treeCheckStrictly, treeNodeLabelProp, disabled } = this.props; - if (disabled) return; - - // Find trigger entity - const triggerEntity = valueEntities[removeValue]; - - // Clean up value - let newValueList = valueList; - if (triggerEntity) { - // If value is in tree - if (treeCheckable && !treeCheckStrictly) { - newValueList = valueList.filter(({ value }) => { - const entity = valueEntities[value]; - return !isPosRelated(entity.pos, triggerEntity.pos); - }); - } else { - newValueList = valueList.filter(({ value }) => value !== removeValue); - } - } - - const triggerNode = triggerEntity ? triggerEntity.node : null; - - const extraInfo = { - triggerValue: removeValue, - triggerNode, - }; - const deselectInfo = { - node: triggerNode, - }; - - // [Legacy] Little hack on this to make same action as `onCheck` event. - if (treeCheckable) { - const filteredEntityList = newValueList.map(({ value }) => valueEntities[value]); - - deselectInfo.event = 'check'; - deselectInfo.checked = false; - deselectInfo.checkedNodes = filteredEntityList.map(({ node }) => node); - deselectInfo.checkedNodesPositions = filteredEntityList.map(({ node, pos }) => ({ - node, - pos, - })); - - if (treeCheckStrictly) { - extraInfo.allCheckedNodes = deselectInfo.checkedNodes; - } else { - // TODO: It's too expansive to get `halfCheckedKeys` in onDeselect. Not pass this. - extraInfo.allCheckedNodes = flatToHierarchy(filteredEntityList).map(({ node }) => node); - } - } else { - deselectInfo.event = 'select'; - deselectInfo.selected = false; - deselectInfo.selectedNodes = newValueList.map( - ({ value }) => (valueEntities[value] || {}).node, - ); - } - - // Some value user pass prop is not in the tree, we also need clean it - const newMissValueList = missValueList.filter(({ value }) => value !== removeValue); - - let wrappedValue; - if (this.isLabelInValue()) { - wrappedValue = { - label: triggerNode ? triggerNode.props[treeNodeLabelProp] : null, - value: removeValue, - }; - } else { - wrappedValue = removeValue; - } - - this.onDeselect(wrappedValue, triggerNode, deselectInfo); - - this.triggerChange(newMissValueList, newValueList, extraInfo); - }; - - // ===================== Popup ====================== - onValueTrigger = (isAdd, nodeList, nodeEventInfo, nodeExtraInfo) => { - const { node } = nodeEventInfo; - const { value } = node.props; - const { missValueList, valueEntities, keyEntities, searchValue } = this.state; - const { - disabled, - inputValue, - treeNodeLabelProp, - onSelect, - onSearch, - multiple, - treeCheckable, - treeCheckStrictly, - autoClearSearchValue, - } = this.props; - const label = node.props[treeNodeLabelProp]; - - if (disabled) return; - - // Wrap the return value for user - let wrappedValue; - if (this.isLabelInValue()) { - wrappedValue = { - value, - label, - }; - } else { - wrappedValue = value; - } - - // [Legacy] Origin code not trigger `onDeselect` every time. Let's align the behaviour. - if (isAdd) { - if (onSelect) { - onSelect(wrappedValue, node, nodeEventInfo); - } - } else { - this.onDeselect(wrappedValue, node, nodeEventInfo); - } - - // Get wrapped value list. - // This is a bit hack cause we use key to match the value. - let newValueList = nodeList.map(({ props }) => ({ - value: props.value, - label: props[treeNodeLabelProp], - })); - - // When is `treeCheckable` and with `searchValue`, `valueList` is not full filled. - // We need calculate the missing nodes. - if (treeCheckable && !treeCheckStrictly) { - let keyList = newValueList.map(({ value: val }) => valueEntities[val].key); - if (isAdd) { - keyList = conductCheck(keyList, true, keyEntities).checkedKeys; - } else { - keyList = conductCheck([valueEntities[value].key], false, keyEntities, { - checkedKeys: keyList, - }).checkedKeys; - } - newValueList = keyList.map(key => { - const { - node: { props }, - } = keyEntities[key]; - return { - value: props.value, - label: props[treeNodeLabelProp], - }; - }); - } - - // Clean up `searchValue` when this prop is set - if (autoClearSearchValue || inputValue === null) { - // Clean state `searchValue` if uncontrolled - if (!this.isSearchValueControlled() && (multiple || treeCheckable)) { - this.setUncontrolledState({ - searchValue: '', - filteredTreeNodes: null, - }); - } - - // Trigger onSearch if `searchValue` to be empty. - // We should also trigger onSearch with empty string here - // since if user use `treeExpandedKeys`, it need user have the ability to reset it. - if (onSearch && searchValue && searchValue.length) { - onSearch(''); - } - } - - // [Legacy] Provide extra info - const extraInfo = { - ...nodeExtraInfo, - triggerValue: value, - triggerNode: node, - }; - - this.triggerChange(missValueList, newValueList, extraInfo); - }; - - onTreeNodeSelect = (_, nodeEventInfo) => { - const { valueList, valueEntities } = this.state; - const { treeCheckable, multiple } = this.props; - if (treeCheckable) return; - - if (!multiple) { - this.setOpenState(false); - } - - const isAdd = nodeEventInfo.selected; - const { - props: { value: selectedValue }, - } = nodeEventInfo.node; - - let newValueList; - - if (!multiple) { - newValueList = [{ value: selectedValue }]; - } else { - newValueList = valueList.filter(({ value }) => value !== selectedValue); - if (isAdd) { - newValueList.push({ value: selectedValue }); - } - } - - const selectedNodes = newValueList - .map(({ value }) => valueEntities[value]) - .filter(entity => entity) - .map(({ node }) => node); - - this.onValueTrigger(isAdd, selectedNodes, nodeEventInfo, { selected: isAdd }); - }; - - onTreeNodeCheck = (_, nodeEventInfo) => { - const { searchValue, keyEntities, valueEntities, valueList } = this.state; - const { treeCheckStrictly } = this.props; - - const { checkedNodes, checkedNodesPositions } = nodeEventInfo; - const isAdd = nodeEventInfo.checked; - - const extraInfo = { - checked: isAdd, - }; - - let checkedNodeList = checkedNodes; - - // [Legacy] Check event provide `allCheckedNodes`. - // When `treeCheckStrictly` or internal `searchValue` is set, TreeNode will be unrelated: - // - Related: Show the top checked nodes and has children prop. - // - Unrelated: Show all the checked nodes. - if (searchValue) { - const oriKeyList = valueList - .map(({ value }) => valueEntities[value]) - .filter(entity => entity) - .map(({ key }) => key); - - let keyList; - if (isAdd) { - keyList = Array.from( - new Set([ - ...oriKeyList, - ...checkedNodeList.map(({ props: { value } }) => valueEntities[value].key), - ]), - ); - } else { - keyList = conductCheck([nodeEventInfo.node.props.eventKey], false, keyEntities, { - checkedKeys: oriKeyList, - }).checkedKeys; - } - - checkedNodeList = keyList.map(key => keyEntities[key].node); - - // Let's follow as not `treeCheckStrictly` format - extraInfo.allCheckedNodes = keyList.map(key => cleanEntity(keyEntities[key])); - } else if (treeCheckStrictly) { - extraInfo.allCheckedNodes = nodeEventInfo.checkedNodes; - } else { - extraInfo.allCheckedNodes = flatToHierarchy(checkedNodesPositions); - } - - this.onValueTrigger(isAdd, checkedNodeList, nodeEventInfo, extraInfo); - }; - - // ==================== Trigger ===================== - - onDropdownVisibleChange = open => { - const { multiple, treeCheckable } = this.props; - const { searchValue } = this.state; - - // When set open success and single mode, - // we will reset the input content. - if (open && !multiple && !treeCheckable && searchValue) { - this.setUncontrolledState({ - searchValue: '', - filteredTreeNodes: null, - }); - } - - this.setOpenState(open, true); - }; - - onSearchInputChange = ({ target: { value } }) => { - const { treeNodes, valueEntities } = this.state; - const { onSearch, filterTreeNode, treeNodeFilterProp } = this.props; - - if (onSearch) { - onSearch(value); - } - - let isSet = false; - - if (!this.isSearchValueControlled()) { - isSet = this.setUncontrolledState({ - searchValue: value, - }); - this.setOpenState(true); - } - - if (isSet) { - // Do the search logic - const upperSearchValue = String(value).toUpperCase(); - - let filterTreeNodeFn = filterTreeNode; - if (filterTreeNode === false) { - filterTreeNodeFn = () => true; - } else if (!filterTreeNodeFn) { - filterTreeNodeFn = (_, node) => { - const nodeValue = String(node.props[treeNodeFilterProp]).toUpperCase(); - return nodeValue.indexOf(upperSearchValue) !== -1; - }; - } - - this.setState({ - filteredTreeNodes: getFilterTree( - treeNodes, - value, - filterTreeNodeFn, - valueEntities, - SelectNode, - ), - }); - } - }; - - onSearchInputKeyDown = event => { - const { searchValue, valueList } = this.state; - - const { keyCode } = event; - - if (KeyCode.BACKSPACE === keyCode && this.isMultiple() && !searchValue && valueList.length) { - const lastValue = valueList[valueList.length - 1].value; - this.onMultipleSelectorRemove(event, lastValue); - } - }; - - onChoiceAnimationLeave = () => { - raf(() => { - this.forcePopupAlign(); - }); - }; - - setPopupRef = popup => { - this.popup = popup; - }; - - /** - * Only update the value which is not in props - */ - setUncontrolledState = state => { - let needSync = false; - const newState = {}; - - Object.keys(state).forEach(name => { - if (name in this.props) return; - - needSync = true; - newState[name] = state[name]; - }); - - if (needSync) { - this.setState(newState); - } - - return needSync; - }; - - // [Legacy] Origin provide `documentClickClose` which triggered by `Trigger` - // Currently `TreeSelect` align the hide popup logic as `Select` which blur to hide. - // `documentClickClose` is not accurate anymore. Let's just keep the key word. - setOpenState = (open, byTrigger = false) => { - const { onDropdownVisibleChange } = this.props; - - if ( - onDropdownVisibleChange && - onDropdownVisibleChange(open, { documentClickClose: !open && byTrigger }) === false - ) { - return; - } - - this.setUncontrolledState({ open }); - }; - - // Tree checkable is also a multiple case - isMultiple = () => { - const { multiple, treeCheckable } = this.props; - return !!(multiple || treeCheckable); - }; - - isLabelInValue = () => { - return isLabelInValue(this.props); - }; - - // [Legacy] To align with `Select` component, - // We use `searchValue` instead of `inputValue` - // but currently still need support that. - // Add this method the check if is controlled - isSearchValueControlled = () => { - const { inputValue } = this.props; - if ('searchValue' in this.props) return true; - return 'inputValue' in this.props && inputValue !== null; - }; - - forcePopupAlign = () => { - const $trigger = this.selectTriggerRef.current; - - if ($trigger) { - $trigger.forcePopupAlign(); - } - }; - - delayForcePopupAlign = () => { - // Wait 2 frame to avoid dom update & dom algin in the same time - // https://github.com/ant-design/ant-design/issues/12031 - raf(() => { - raf(this.forcePopupAlign); - }); - }; - - /** - * 1. Update state valueList. - * 2. Fire `onChange` event to user. - */ - triggerChange = (missValueList, valueList, extraInfo = {}) => { - const { valueEntities, searchValue, selectorValueList: prevSelectorValueList } = this.state; - const { onChange, disabled, treeCheckable, treeCheckStrictly } = this.props; - - if (disabled) return; - - // Trigger - const extra = { - // [Legacy] Always return as array contains label & value - preValue: prevSelectorValueList.map(({ label, value }) => ({ label, value })), - ...extraInfo, - }; - - // Format value by `treeCheckStrictly` - const selectorValueList = formatSelectorValue(valueList, this.props, valueEntities); - - if (!('value' in this.props)) { - const newState = { - missValueList, - valueList, - selectorValueList, - }; - - if (searchValue && treeCheckable && !treeCheckStrictly) { - newState.searchHalfCheckedKeys = getHalfCheckedKeys(valueList, valueEntities); - } - - this.setState(newState); - } - - // Only do the logic when `onChange` function provided - if (onChange) { - let connectValueList; - - // Get value by mode - if (this.isMultiple()) { - connectValueList = [...missValueList, ...selectorValueList]; - } else { - connectValueList = selectorValueList.slice(0, 1); - } - - let labelList = null; - let returnValue; - - if (this.isLabelInValue()) { - returnValue = connectValueList.map(({ label, value }) => ({ label, value })); - } else { - labelList = []; - returnValue = connectValueList.map(({ label, value }) => { - labelList.push(label); - return value; - }); - } - - if (!this.isMultiple()) { - returnValue = returnValue[0]; - } - - onChange(returnValue, labelList, extra); - } - }; - - focus() { - this.selectorRef.current.focus(); - } - - blur() { - this.selectorRef.current.blur(); - } - - // ===================== Render ===================== - - render() { - const { - valueList, - missValueList, - selectorValueList, - searchHalfCheckedKeys, - valueEntities, - keyEntities, - searchValue, - open, - focused, - treeNodes, - filteredTreeNodes, - } = this.state; - const { prefixCls, treeExpandedKeys, onTreeExpand } = this.props; - const isMultiple = this.isMultiple(); - - const passProps = { - ...this.props, - isMultiple, - valueList, - searchHalfCheckedKeys, - selectorValueList: [...missValueList, ...selectorValueList], - valueEntities, - keyEntities, - searchValue, - upperSearchValue: (searchValue || '').toUpperCase(), // Perf save - open, - focused, - onChoiceAnimationLeave: this.onChoiceAnimationLeave, - dropdownPrefixCls: `${prefixCls}-dropdown`, - ariaId: this.ariaId, - }; - - const Popup = isMultiple ? MultiplePopup : SinglePopup; - const $popup = ( - - ); - - const Selector = isMultiple ? MultipleSelector : SingleSelector; - const $selector = ; - - return ( - - {$selector} - - ); - } -} - -Select.TreeNode = SelectNode; -Select.SHOW_ALL = SHOW_ALL; -Select.SHOW_PARENT = SHOW_PARENT; -Select.SHOW_CHILD = SHOW_CHILD; - -// Let warning show correct component name -Select.displayName = 'TreeSelect'; - -polyfill(Select); - -export default Select; diff --git a/src/SelectNode.jsx b/src/SelectNode.jsx deleted file mode 100644 index edbefa83..00000000 --- a/src/SelectNode.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { TreeNode } from 'rc-tree'; -import { valueProp } from './propTypes'; - -/** - * SelectNode wrapped the tree node. - * Let's use SelectNode instead of TreeNode - * since TreeNode is so confuse here. - */ -const SelectNode = (props) => ( - -); - -SelectNode.propTypes = { - ...TreeNode.propTypes, - value: valueProp, -}; - -// Let Tree trade as TreeNode to reuse this for performance saving. -SelectNode.isTreeNode = 1; - -export default SelectNode; diff --git a/src/SelectTrigger.jsx b/src/SelectTrigger.jsx deleted file mode 100644 index 0eef6305..00000000 --- a/src/SelectTrigger.jsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { polyfill } from 'react-lifecycles-compat'; -import Trigger from 'rc-trigger'; -import classNames from 'classnames'; - -import { createRef } from './util'; - -const BUILT_IN_PLACEMENTS = { - bottomLeft: { - points: ['tl', 'bl'], - offset: [0, 4], - overflow: { - adjustX: 0, - adjustY: 1, - }, - ignoreShake: true, - }, - topLeft: { - points: ['bl', 'tl'], - offset: [0, -4], - overflow: { - adjustX: 0, - adjustY: 1, - }, - ignoreShake: true, - }, -}; - -class SelectTrigger extends React.Component { - static propTypes = { - // Pass by outside user props - disabled: PropTypes.bool, - showSearch: PropTypes.bool, - prefixCls: PropTypes.string, - dropdownPopupAlign: PropTypes.object, - dropdownClassName: PropTypes.string, - dropdownStyle: PropTypes.object, - transitionName: PropTypes.string, - animation: PropTypes.string, - getPopupContainer: PropTypes.func, - children: PropTypes.node, - - dropdownMatchSelectWidth: PropTypes.bool, - - // Pass by Select - isMultiple: PropTypes.bool, - dropdownPrefixCls: PropTypes.string, - onDropdownVisibleChange: PropTypes.func, - popupElement: PropTypes.node, - open: PropTypes.bool, - }; - - constructor() { - super(); - - this.triggerRef = createRef(); - } - - getDropdownTransitionName = () => { - const { transitionName, animation, dropdownPrefixCls } = this.props; - if (!transitionName && animation) { - return `${dropdownPrefixCls}-${animation}`; - } - return transitionName; - }; - - forcePopupAlign = () => { - const $trigger = this.triggerRef.current; - - if ($trigger) { - $trigger.forcePopupAlign(); - } - }; - - render() { - const { - disabled, isMultiple, - dropdownPopupAlign, dropdownMatchSelectWidth, dropdownClassName, - dropdownStyle, onDropdownVisibleChange, getPopupContainer, - dropdownPrefixCls, popupElement, open, - children, - } = this.props; - - // TODO: [Legacy] Use new action when trigger fixed: https://github.com/react-component/trigger/pull/86 - - // When false do nothing with the width - // ref: https://github.com/ant-design/ant-design/issues/10927 - let stretch; - if (dropdownMatchSelectWidth !== false) { - stretch = dropdownMatchSelectWidth ? 'width' : 'minWidth'; - } - - return ( - - {children} - - ); - } -} - -polyfill(SelectTrigger); - -export default SelectTrigger; diff --git a/src/Selector/MultipleSelector/Selection.jsx b/src/Selector/MultipleSelector/Selection.jsx deleted file mode 100644 index 64670966..00000000 --- a/src/Selector/MultipleSelector/Selection.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { toTitle, UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE } from '../../util'; - -class Selection extends React.Component { - static propTypes = { - prefixCls: PropTypes.string, - maxTagTextLength: PropTypes.number, - onRemove: PropTypes.func, - className: PropTypes.string, - style: PropTypes.object, - - label: PropTypes.node, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - removeIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - }; - - onRemove = event => { - const { onRemove, value } = this.props; - onRemove(event, value); - - event.stopPropagation(); - }; - - render() { - const { - prefixCls, - maxTagTextLength, - className, - style, - label, - value, - onRemove, - removeIcon, - } = this.props; - - let content = label || value; - if (maxTagTextLength && typeof content === 'string' && content.length > maxTagTextLength) { - content = `${content.slice(0, maxTagTextLength)}...`; - } - - return ( -
  • - {onRemove && ( - - {typeof removeIcon === 'function' - ? React.createElement(removeIcon, { ...this.props }) - : removeIcon} - - )} - {content} -
  • - ); - } -} - -export default Selection; diff --git a/src/Selector/MultipleSelector/SelectorList.jsx b/src/Selector/MultipleSelector/SelectorList.jsx deleted file mode 100644 index 283365bb..00000000 --- a/src/Selector/MultipleSelector/SelectorList.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import CSSMotionList from 'rc-animate/lib/CSSMotionList'; -import Selection from './Selection'; -import SearchInput from '../../SearchInput'; - -const NODE_SELECTOR = 'selector'; -const NODE_SEARCH = 'search'; -const TREE_SELECT_EMPTY_VALUE_KEY = 'RC_TREE_SELECT_EMPTY_VALUE_KEY'; - -const SelectorList = props => { - const { - selectorValueList, - choiceTransitionName, - prefixCls, - onChoiceAnimationLeave, - labelInValue, - maxTagCount, - maxTagPlaceholder, - showSearch, - valueEntities, - inputRef, - onMultipleSelectorRemove, - } = props; - const nodeKeys = []; - - // Check if `maxTagCount` is set - let myValueList = selectorValueList; - if (maxTagCount >= 0) { - myValueList = selectorValueList.slice(0, maxTagCount); - } - - // Basic selectors - myValueList.forEach(({ label, value }) => { - const { props: { disabled } = {} } = (valueEntities[value] || {}).node || {}; - nodeKeys.push({ - key: value, - type: NODE_SELECTOR, - label, - value, - disabled, - }); - }); - - // Rest node count - if (maxTagCount >= 0 && maxTagCount < selectorValueList.length) { - let content = `+ ${selectorValueList.length - maxTagCount} ...`; - if (typeof maxTagPlaceholder === 'string') { - content = maxTagPlaceholder; - } else if (typeof maxTagPlaceholder === 'function') { - const restValueList = selectorValueList.slice(maxTagCount); - content = maxTagPlaceholder( - labelInValue ? restValueList : restValueList.map(({ value }) => value), - ); - } - - nodeKeys.push({ - key: 'rc-tree-select-internal-max-tag-counter', - type: NODE_SELECTOR, - label: content, - value: null, - disabled: true, - }); - } - - // Search node - if (showSearch !== false) { - nodeKeys.push({ - key: '__input', - type: NODE_SEARCH, - }); - } - - return ( - - {({ type, label, value, disabled, className, style }) => { - if (type === NODE_SELECTOR) { - return ( - - ); - } - return ( -
  • - -
  • - ); - }} -
    - ); -}; - -SelectorList.propTypes = { - selectorValueList: PropTypes.array, - choiceTransitionName: PropTypes.string, - prefixCls: PropTypes.string, - onChoiceAnimationLeave: PropTypes.func, - labelInValue: PropTypes.bool, - showSearch: PropTypes.bool, - maxTagCount: PropTypes.number, - maxTagPlaceholder: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - valueEntities: PropTypes.object, - inputRef: PropTypes.func, - onMultipleSelectorRemove: PropTypes.func, -}; - -export default SelectorList; diff --git a/src/Selector/MultipleSelector/index.jsx b/src/Selector/MultipleSelector/index.jsx deleted file mode 100644 index 47d6ad07..00000000 --- a/src/Selector/MultipleSelector/index.jsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import generateSelector, { selectorPropTypes } from '../../Base/BaseSelector'; -import { createRef } from '../../util'; -import SelectorList from './SelectorList'; - -const Selector = generateSelector('multiple'); - -export const multipleSelectorContextTypes = { - onMultipleSelectorRemove: PropTypes.func.isRequired, -}; - -class MultipleSelector extends React.Component { - static propTypes = { - ...selectorPropTypes, - selectorValueList: PropTypes.array, - disabled: PropTypes.bool, - searchValue: PropTypes.string, - labelInValue: PropTypes.bool, - maxTagCount: PropTypes.number, - maxTagPlaceholder: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - - onChoiceAnimationLeave: PropTypes.func, - }; - - static contextTypes = { - rcTreeSelect: PropTypes.shape({ - ...multipleSelectorContextTypes, - - onSearchInputChange: PropTypes.func, - }), - }; - - constructor() { - super(); - this.inputRef = createRef(); - } - - onPlaceholderClick = () => { - this.inputRef.current.focus(); - }; - - focus = () => { - this.inputRef.current.focus(); - }; - - blur = () => { - this.inputRef.current.blur(); - }; - - renderPlaceholder = () => { - const { - prefixCls, - placeholder, - searchPlaceholder, - searchValue, - selectorValueList, - } = this.props; - - const currentPlaceholder = placeholder || searchPlaceholder; - - if (!currentPlaceholder) return null; - - const hidden = searchValue || selectorValueList.length; - - // [Legacy] Not remove the placeholder - return ( - - ); - }; - - renderSelection = () => { - const { - rcTreeSelect: { onMultipleSelectorRemove }, - } = this.context; - - return ( - - ); - }; - - render() { - return ( - - ); - } -} - -export default MultipleSelector; diff --git a/src/Selector/SingleSelector.jsx b/src/Selector/SingleSelector.jsx deleted file mode 100644 index 37c79e0d..00000000 --- a/src/Selector/SingleSelector.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import generateSelector, { selectorPropTypes } from '../Base/BaseSelector'; -import { toTitle, createRef } from '../util'; - -const Selector = generateSelector('single'); - -class SingleSelector extends React.Component { - static propTypes = { - ...selectorPropTypes, - }; - - constructor() { - super(); - this.selectorRef = createRef(); - } - - focus = () => { - this.selectorRef.current.focus(); - }; - - blur = () => { - this.selectorRef.current.blur(); - }; - - renderSelection = () => { - const { selectorValueList, placeholder, prefixCls } = this.props; - - let innerNode; - - if (selectorValueList.length) { - const { label, value } = selectorValueList[0]; - innerNode = ( - - {label || value} - - ); - } else { - innerNode = ( - - {placeholder} - - ); - } - - return {innerNode}; - }; - - render() { - return ( - - ); - } -} - -export default SingleSelector; diff --git a/src/TreeNode.tsx b/src/TreeNode.tsx new file mode 100644 index 00000000..424d5b44 --- /dev/null +++ b/src/TreeNode.tsx @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +import React from 'react'; +import { DataNode, Key } from './interface'; + +export interface TreeNodeProps extends Omit { + value: Key; + children?: React.ReactNode; +} + +/** This is a placeholder, not real render in dom */ +const TreeNode: React.FC = () => null; + +export default TreeNode; diff --git a/src/TreeSelect.tsx b/src/TreeSelect.tsx new file mode 100644 index 00000000..616d9151 --- /dev/null +++ b/src/TreeSelect.tsx @@ -0,0 +1,546 @@ +import React from 'react'; +import generateSelector, { SelectProps, RefSelectProps } from 'rc-select/lib/generate'; +import { getLabeledValue } from 'rc-select/lib/utils/valueUtil'; +import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil'; +import { conductCheck } from 'rc-tree/lib/utils/conductUtil'; +import { IconType } from 'rc-tree/lib/interface'; +import { FilterFunc, INTERNAL_PROPS_MARK } from 'rc-select/lib/interface/generator'; +import warning from 'rc-util/lib/warning'; +import OptionList from './OptionList'; +import TreeNode from './TreeNode'; +import { + Key, + DefaultValueType, + DataNode, + LabelValueType, + SimpleModeConfig, + RawValueType, + ChangeEventExtra, + LegacyDataNode, + SelectSource, +} from './interface'; +import { + flattenOptions, + filterOptions, + isValueDisabled, + findValueOption, + addValue, + removeValue, + getRawValueLabeled, + toArray, +} from './utils/valueUtil'; +import warningProps from './utils/warningPropsUtil'; +import { SelectContext } from './Context'; +import useTreeData from './hooks/useTreeData'; +import useKeyValueMap from './hooks/useKeyValueMap'; +import useKeyValueMapping from './hooks/useKeyValueMapping'; +import { + CheckedStrategy, + formatStrategyKeys, + SHOW_ALL, + SHOW_PARENT, + SHOW_CHILD, +} from './utils/strategyUtil'; +import { fillAdditionalInfo } from './utils/legacyUtil'; +import useSelectValues from './hooks/useSelectValues'; + +const OMIT_PROPS = [ + 'expandedKeys', + 'treeData', + 'treeCheckable', + 'showCheckedStrategy', + 'searchPlaceholder', + 'treeLine', + 'treeNodeFilterProp', + 'filterTreeNode', + 'dropdownPopupAlign', + 'treeDefaultExpandAll', + 'treeCheckStrictly', + 'treeExpandedKeys', + 'treeLoadedKeys', + 'onTreeExpand', + 'onTreeLoad', + 'loadData', + 'treeDataSimpleMode', + 'treeNodeLabelProp', + 'treeDefaultExpandedKeys', +]; + +const RefSelect = generateSelector({ + prefixCls: 'rc-tree-select', + components: { + optionList: OptionList, + }, + // Not use generate since we will handle ourself + convertChildrenToData: () => null, + flattenOptions, + // Handle `optionLabelProp` in TreeSelect component + getLabeledValue: getLabeledValue as any, + filterOptions, + isValueDisabled, + findValueOption, + omitDOMProps: (props: object) => { + const cloneProps = { ...props }; + OMIT_PROPS.forEach(prop => { + delete cloneProps[prop]; + }); + return cloneProps; + }, +}); + +RefSelect.displayName = 'Select'; + +export interface TreeSelectProps + extends Omit< + SelectProps, + | 'onChange' + | 'mode' + | 'menuItemSelectedIcon' + | 'dropdownRender' + | 'dropdownAlign' + | 'backfill' + | 'getInputElement' + | 'optionLabelProp' + | 'tokenSeparators' + | 'filterOption' + > { + multiple?: boolean; + showArrow?: boolean; + showSearch?: boolean; + open?: boolean; + defaultOpen?: boolean; + value?: ValueType; + defaultValue?: ValueType; + disabled?: boolean; + + placeholder?: React.ReactNode; + /** @deprecated Use `searchValue` instead */ + inputValue?: string; + searchValue?: string; + autoClearSearchValue?: boolean; + + maxTagTextLength?: number; + maxTagCount?: number; + maxTagPlaceholder?: (omittedValues: LabelValueType[]) => React.ReactNode; + + loadData?: (dataNode: LegacyDataNode) => Promise; + treeNodeFilterProp?: string; + treeNodeLabelProp?: string; + treeDataSimpleMode?: boolean | SimpleModeConfig; + treeExpandedKeys?: Key[]; + treeDefaultExpandedKeys?: Key[]; + treeLoadedKeys?: Key[]; + treeCheckable?: boolean | React.ReactNode; + treeCheckStrictly?: boolean; + showCheckedStrategy?: CheckedStrategy; + treeDefaultExpandAll?: boolean; + treeData?: DataNode[]; + treeLine?: boolean; + treeIcon?: IconType; + switcherIcon?: IconType; + children?: React.ReactNode; + + filterTreeNode?: boolean | FilterFunc; + dropdownPopupAlign?: any; + + // Event + onSearch?: (value: string) => void; + onChange?: (value: ValueType, labelList: React.ReactNode[], extra: ChangeEventExtra) => void; + onTreeExpand?: (expandedKeys: Key[]) => void; + onTreeLoad?: (loadedKeys: Key[]) => void; + + // Legacy + /** `searchPlaceholder` has been removed since search box has been merged into input box */ + searchPlaceholder?: React.ReactNode; +} + +const RefTreeSelect = React.forwardRef((props, ref) => { + const { + multiple, + treeCheckable, + treeCheckStrictly, + showCheckedStrategy = 'SHOW_CHILD', + labelInValue, + loadData, + treeLoadedKeys, + treeNodeFilterProp = 'value', + treeNodeLabelProp, + treeDataSimpleMode, + treeData, + treeExpandedKeys, + treeDefaultExpandedKeys, + treeDefaultExpandAll, + children, + treeIcon, + switcherIcon, + treeLine, + filterTreeNode, + dropdownPopupAlign, + onChange, + onTreeExpand, + onTreeLoad, + onDropdownVisibleChange, + onSelect, + onDeselect, + } = props; + const mergedCheckable = !!(treeCheckable || treeCheckStrictly); + const mergedMultiple = multiple || mergedCheckable; + const treeConduction = treeCheckable && !treeCheckStrictly; + const mergedLabelInValue = treeCheckStrictly || labelInValue; + + // ========================== Ref ========================== + const selectRef = React.useRef(null); + + React.useImperativeHandle(ref, () => ({ + focus: selectRef.current.focus, + blur: selectRef.current.blur, + })); + + // ======================= Tree Data ======================= + // Legacy both support `label` or `title` if not set. + // We have to fallback to function to handle this + const getTreeNodeLabelProp = (node: DataNode): React.ReactNode => { + if (treeNodeLabelProp) { + return node[treeNodeLabelProp]; + } + + if (!treeData) { + return node.title; + } + return node.label || node.title; + }; + + const mergedTreeData = useTreeData(treeData, children, { + getLabelProp: getTreeNodeLabelProp, + simpleMode: treeDataSimpleMode, + }); + + const flattedOptions = React.useMemo(() => flattenOptions(mergedTreeData), [mergedTreeData]); + const [cacheKeyMap, cacheValueMap] = useKeyValueMap(flattedOptions); + const [getEntityByKey, getEntityByValue] = useKeyValueMapping(cacheKeyMap, cacheValueMap); + + // Only generate keyEntities for check conduction when is `treeCheckable` + const { keyEntities: conductKeyEntities } = React.useMemo(() => { + if (treeConduction) { + return convertDataToEntities(mergedTreeData as any); + } + return { keyEntities: null }; + }, [mergedTreeData, treeCheckable, treeCheckStrictly]); + + // ========================= Value ========================= + const [value, setValue] = React.useState(props.defaultValue); + const mergedValue = 'value' in props ? props.value : value; + + /** Get `missingRawValues` which not exist in the tree yet */ + const splitRawValues = (newRawValues: RawValueType[]) => { + const missingRawValues = []; + const existRawValues = []; + + // Keep missing value in the cache + newRawValues.forEach(val => { + if (getEntityByValue(val)) { + existRawValues.push(val); + } else { + missingRawValues.push(val); + } + }); + + return { missingRawValues, existRawValues }; + }; + + const [rawValues, rawHalfCheckedKeys]: [RawValueType[], RawValueType[]] = React.useMemo(() => { + const valueHalfCheckedKeys: RawValueType[] = []; + const newRawValues: RawValueType[] = []; + + toArray(mergedValue).forEach(item => { + if (item && typeof item === 'object' && 'value' in item) { + if (item.halfChecked && treeCheckStrictly) { + const entity = getEntityByValue(item.value); + valueHalfCheckedKeys.push(entity ? entity.key : item.value); + } else { + newRawValues.push(item.value); + } + } else { + newRawValues.push(item as RawValueType); + } + }); + + // We need do conduction of values + if (treeConduction) { + const { missingRawValues, existRawValues } = splitRawValues(newRawValues); + const keyList = existRawValues.map(val => getEntityByValue(val).key); + + const { checkedKeys, halfCheckedKeys } = conductCheck(keyList, true, conductKeyEntities); + return [ + [...missingRawValues, ...checkedKeys.map(key => getEntityByKey(key).data.value)], + halfCheckedKeys, + ]; + } + return [newRawValues, valueHalfCheckedKeys]; + }, [mergedValue, mergedMultiple, mergedLabelInValue, treeCheckable, treeCheckStrictly]); + const selectValues = useSelectValues(rawValues, { + treeConduction, + value: mergedValue, + showCheckedStrategy, + conductKeyEntities, + getEntityByValue, + getEntityByKey, + getLabelProp: getTreeNodeLabelProp, + }); + + const triggerChange = ( + newRawValues: RawValueType[], + extra: { triggerValue: RawValueType; selected: boolean }, + source: SelectSource, + ) => { + setValue(mergedMultiple ? newRawValues : newRawValues[0]); + if (onChange) { + let eventValues: RawValueType[] = newRawValues; + if (treeConduction && showCheckedStrategy !== 'SHOW_ALL') { + const keyList = newRawValues.map(val => { + const entity = getEntityByValue(val); + return entity ? entity.key : val; + }); + const formattedKeyList = formatStrategyKeys( + keyList, + showCheckedStrategy, + conductKeyEntities, + ); + + eventValues = formattedKeyList.map(key => { + const entity = getEntityByKey(key); + return entity ? entity.data.value : key; + }); + } + + const { triggerValue, selected } = extra || { triggerValue: undefined, selected: undefined }; + + let returnValues = mergedLabelInValue + ? getRawValueLabeled(eventValues, mergedValue, getEntityByValue, getTreeNodeLabelProp) + : eventValues; + + // We need fill half check back + if (treeCheckStrictly) { + const halfValues = rawHalfCheckedKeys + .map(key => { + const entity = getEntityByKey(key); + return entity ? entity.data.value : key; + }) + .filter(val => !eventValues.includes(val)); + + returnValues = [ + ...(returnValues as LabelValueType[]), + ...getRawValueLabeled(halfValues, mergedValue, getEntityByValue, getTreeNodeLabelProp), + ]; + } + + const additionalInfo = { + // [Legacy] Always return as array contains label & value + preValue: selectValues, + triggerValue, + } as ChangeEventExtra; + + // [Legacy] Fill legacy data if user query. + // This is expansive that we only fill when user query + // https://github.com/react-component/tree-select/blob/fe33eb7c27830c9ac70cd1fdb1ebbe7bc679c16a/src/Select.jsx + let showPosition = true; + if (treeCheckStrictly || (source === 'selection' && !selected)) { + showPosition = false; + } + + fillAdditionalInfo(additionalInfo, triggerValue, newRawValues, mergedTreeData, showPosition); + + if (mergedCheckable) { + additionalInfo.checked = selected; + } else { + additionalInfo.selected = selected; + } + + onChange( + mergedMultiple ? returnValues : returnValues[0], + mergedLabelInValue + ? null + : eventValues.map(val => { + const entity = getEntityByValue(val); + return entity ? getTreeNodeLabelProp(entity.data) : null; + }), + additionalInfo, + ); + } + }; + + const onInternalSelect = (selectValue: RawValueType, option: DataNode, source: SelectSource) => { + const eventValue = mergedLabelInValue ? selectValue : selectValue; + + if (!mergedMultiple) { + // Single mode always set value + triggerChange([selectValue], { selected: true, triggerValue: selectValue }, source); + } else { + let newRawValues = addValue(rawValues, selectValue); + + // Add keys if tree conduction + if (treeConduction) { + // Should keep missing values + const { missingRawValues, existRawValues } = splitRawValues(newRawValues); + const keyList = existRawValues.map(val => getEntityByValue(val).key); + const { checkedKeys } = conductCheck(keyList, true, conductKeyEntities); + newRawValues = [ + ...missingRawValues, + ...checkedKeys.map(key => getEntityByKey(key).data.value), + ]; + } + + triggerChange(newRawValues, { selected: true, triggerValue: selectValue }, source); + } + + if (onSelect) { + onSelect(eventValue, option); + } + }; + + const onInternalDeselect = ( + selectValue: RawValueType, + option: DataNode, + source: SelectSource, + ) => { + const eventValue = mergedLabelInValue ? selectValue : selectValue; + + let newRawValues = removeValue(rawValues, selectValue); + + // Remove keys if tree conduction + if (treeConduction) { + const { missingRawValues, existRawValues } = splitRawValues(newRawValues); + const keyList = existRawValues.map(val => getEntityByValue(val).key); + const { checkedKeys } = conductCheck( + keyList, + { checked: false, halfCheckedKeys: rawHalfCheckedKeys }, + conductKeyEntities, + ); + newRawValues = [ + ...missingRawValues, + ...checkedKeys.map(key => getEntityByKey(key).data.value), + ]; + } + + triggerChange(newRawValues, { selected: false, triggerValue: selectValue }, source); + + if (onDeselect) { + onDeselect(eventValue, option); + } + }; + + const onInternalClear = () => { + triggerChange([], null, 'clear'); + }; + + // ========================= Open ========================== + const onInternalDropdownVisibleChange = React.useCallback( + (open: boolean) => { + if (onDropdownVisibleChange) { + const legacyParam = {}; + + Object.defineProperty(legacyParam, 'documentClickClose', { + get() { + warning(false, 'Second param of `onDropdownVisibleChange` has been removed.'); + return false; + }, + }); + + (onDropdownVisibleChange as any)(open, legacyParam); + } + }, + [onDropdownVisibleChange], + ); + + // ======================== Warning ======================== + if (process.env.NODE_ENV !== 'production') { + warningProps(props); + } + + // ======================== Render ========================= + // We pass some props into select props style + const selectProps: Partial> = { + optionLabelProp: null, + optionFilterProp: treeNodeFilterProp, + dropdownAlign: dropdownPopupAlign, + internalProps: { + mark: INTERNAL_PROPS_MARK, + onClear: onInternalClear, + skipTriggerChange: true, + skipTriggerSelect: true, + onRawSelect: onInternalSelect, + onRawDeselect: onInternalDeselect, + }, + }; + + if ('filterTreeNode' in props) { + selectProps.filterOption = filterTreeNode; + } + + return ( + + + + ); +}); + +// Use class component since typescript not support generic +// by `forwardRef` with function component yet. +class TreeSelect extends React.Component< + TreeSelectProps, + {} +> { + static TreeNode = TreeNode; + + static SHOW_ALL: typeof SHOW_ALL = SHOW_ALL; + + static SHOW_PARENT: typeof SHOW_PARENT = SHOW_PARENT; + + static SHOW_CHILD: typeof SHOW_CHILD = SHOW_CHILD; + + selectRef = React.createRef(); + + focus = () => { + this.selectRef.current.focus(); + }; + + blur = () => { + this.selectRef.current.blur(); + }; + + render() { + return ; + } +} + +export default TreeSelect; diff --git a/src/hooks/useKeyValueMap.ts b/src/hooks/useKeyValueMap.ts new file mode 100644 index 00000000..34f6a4e0 --- /dev/null +++ b/src/hooks/useKeyValueMap.ts @@ -0,0 +1,21 @@ +import React from 'react'; +import { FlattenDataNode, Key, RawValueType } from '../interface'; + +/** + * Return cached Key Value map with DataNode. + * Only re-calculate when `flattenOptions` changed. + */ +export default function useKeyValueMap(flattenOptions: FlattenDataNode[]) { + return React.useMemo(() => { + const cacheKeyMap: Map = new Map(); + const cacheValueMap: Map = new Map(); + + // Cache options by key + flattenOptions.forEach((dataNode: FlattenDataNode) => { + cacheKeyMap.set(dataNode.key, dataNode); + cacheValueMap.set(dataNode.data.value, dataNode); + }); + + return [cacheKeyMap, cacheValueMap]; + }, [flattenOptions]); +} diff --git a/src/hooks/useKeyValueMapping.ts b/src/hooks/useKeyValueMapping.ts new file mode 100644 index 00000000..0d1ad942 --- /dev/null +++ b/src/hooks/useKeyValueMapping.ts @@ -0,0 +1,57 @@ +import React from 'react'; +import { FlattenDataNode, Key, RawValueType } from '../interface'; + +export type SkipType = null | 'select' | 'checkbox'; + +export function isDisabled(dataNode: FlattenDataNode, skipType: SkipType): boolean { + if (!dataNode) { + return true; + } + + const { disabled, disableCheckbox } = dataNode.data; + + switch (skipType) { + case 'select': + return disabled; + case 'checkbox': + return disabled || disableCheckbox; + } + + return false; +} + +export default function useKeyValueMapping( + cacheKeyMap: Map, + cacheValueMap: Map, +): [ + (key: Key, skipType?: SkipType) => FlattenDataNode, + (value: RawValueType, skipType?: SkipType) => FlattenDataNode, +] { + const getEntityByKey = React.useCallback( + (key: Key, skipType: SkipType = 'select') => { + const dataNode = cacheKeyMap.get(key); + + if (isDisabled(dataNode, skipType)) { + return null; + } + + return dataNode; + }, + [cacheKeyMap], + ); + + const getEntityByValue = React.useCallback( + (value: RawValueType, skipType: SkipType = 'select') => { + const dataNode = cacheValueMap.get(value); + + if (isDisabled(dataNode, skipType)) { + return null; + } + + return dataNode; + }, + [cacheValueMap], + ); + + return [getEntityByKey, getEntityByValue]; +} diff --git a/src/hooks/useSelectValues.ts b/src/hooks/useSelectValues.ts new file mode 100644 index 00000000..7fff530e --- /dev/null +++ b/src/hooks/useSelectValues.ts @@ -0,0 +1,54 @@ +import React from 'react'; +import { DefaultValueType } from 'rc-select/lib/interface/generator'; +import { DataEntity } from 'rc-tree/lib/interface'; +import { RawValueType, FlattenDataNode, Key, LabelValueType, DataNode } from '../interface'; +import { SkipType } from './useKeyValueMapping'; +import { getRawValueLabeled } from '../utils/valueUtil'; +import { formatStrategyKeys, CheckedStrategy } from '../utils/strategyUtil'; + +interface Config { + treeConduction: boolean; + /** Current `value` of TreeSelect */ + value: DefaultValueType; + showCheckedStrategy: CheckedStrategy; + conductKeyEntities: Record; + getEntityByKey: (key: Key, skipType?: SkipType) => FlattenDataNode; + getEntityByValue: (value: RawValueType, skipType?: SkipType) => FlattenDataNode; + getLabelProp: (node: DataNode) => React.ReactNode; +} + +/** Return */ +export default function useSelectValues( + rawValues: RawValueType[], + { + value, + getEntityByValue, + getEntityByKey, + treeConduction, + showCheckedStrategy, + conductKeyEntities, + getLabelProp, + }: Config, +): LabelValueType[] { + return React.useMemo(() => { + let mergedRawValues = rawValues; + + if (treeConduction) { + const rawKeys = formatStrategyKeys( + rawValues.map(val => { + const entity = getEntityByValue(val); + return entity ? entity.key : val; + }), + showCheckedStrategy, + conductKeyEntities, + ); + + mergedRawValues = rawKeys.map(key => { + const entity = getEntityByKey(key); + return entity ? entity.data.value : key; + }); + } + + return getRawValueLabeled(mergedRawValues, value, getEntityByValue, getLabelProp); + }, [rawValues, value, treeConduction, showCheckedStrategy]); +} diff --git a/src/hooks/useTreeData.ts b/src/hooks/useTreeData.ts new file mode 100644 index 00000000..e3ed29fc --- /dev/null +++ b/src/hooks/useTreeData.ts @@ -0,0 +1,144 @@ +import React from 'react'; +import warning from 'rc-util/lib/warning'; +import { DataNode, InnerDataNode, SimpleModeConfig, RawValueType } from '../interface'; +import { convertChildrenToData } from '../utils/legacyUtil'; + +const MAX_WARNING_TIMES = 10; + +function parseSimpleTreeData( + treeData: DataNode[], + { id, pId, rootPId }: SimpleModeConfig, +): DataNode[] { + const keyNodes = {}; + const rootNodeList = []; + + // Fill in the map + const nodeList = treeData.map(node => { + const clone = { ...node }; + const key = clone[id]; + keyNodes[key] = clone; + clone.key = clone.key || key; + return clone; + }); + + // Connect tree + nodeList.forEach(node => { + const parentKey = node[pId]; + const parent = keyNodes[parentKey]; + + // Fill parent + if (parent) { + parent.children = parent.children || []; + parent.children.push(node); + } + + // Fill root tree node + if (parentKey === rootPId || (!parent && rootPId === null)) { + rootNodeList.push(node); + } + }); + + return rootNodeList; +} + +/** + * Format `treeData` with `value` & `key` which is used for calculation + */ +function formatTreeData( + treeData: DataNode[], + getLabelProp: (node: DataNode) => React.ReactNode, +): InnerDataNode[] { + let warningTimes = 0; + const valueSet = new Set(); + + function dig(dataNodes: DataNode[]) { + return dataNodes.map(node => { + const { key, value, children, ...rest } = node; + + const mergedValue = 'value' in node ? value : key; + + const dataNode: InnerDataNode = { + ...rest, + key: key !== null && key !== undefined ? key : mergedValue, + value: mergedValue, + title: getLabelProp(node), + }; + + // Check `key` & `value` and warning user + if (process.env.NODE_ENV !== 'production') { + if ( + key !== null && + key !== undefined && + value !== undefined && + String(key) !== String(value) && + warningTimes < MAX_WARNING_TIMES + ) { + warningTimes += 1; + warning( + false, + `\`key\` or \`value\` with TreeNode must be the same or you can remove one of them. key: ${key}, value: ${value}.`, + ); + } + + warning(!valueSet.has(value), `Same \`value\` exist in the tree: ${value}`); + valueSet.add(value); + } + + if ('children' in node) { + dataNode.children = dig(children); + } + + return dataNode; + }); + } + + return dig(treeData); +} + +/** + * Convert `treeData` or `children` into formatted `treeData`. + * Will not re-calculate if `treeData` or `children` not change. + */ +export default function useTreeData( + treeData: DataNode[], + children: React.ReactNode, + { + getLabelProp, + simpleMode, + }: { + getLabelProp: (node: DataNode) => React.ReactNode; + simpleMode: boolean | SimpleModeConfig; + }, +): InnerDataNode[] { + const cacheRef = React.useRef<{ + treeData?: DataNode[]; + children?: React.ReactNode; + formatTreeData?: InnerDataNode[]; + }>({}); + + if (treeData) { + cacheRef.current.formatTreeData = + cacheRef.current.treeData === treeData + ? cacheRef.current.formatTreeData + : formatTreeData( + simpleMode + ? parseSimpleTreeData(treeData, { + id: 'id', + pId: 'pId', + rootPId: null, + ...(simpleMode !== true ? simpleMode : {}), + }) + : treeData, + getLabelProp, + ); + + cacheRef.current.treeData = treeData; + } else { + cacheRef.current.formatTreeData = + cacheRef.current.children === children + ? cacheRef.current.formatTreeData + : formatTreeData(convertChildrenToData(children), getLabelProp); + } + + return cacheRef.current.formatTreeData; +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 49f26730..00000000 --- a/src/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import Select from './Select'; -import SelectNode from './SelectNode'; - -export { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './strategies'; -export const TreeNode = SelectNode; - -export default Select; diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 00000000..e62f1757 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,7 @@ +import TreeSelect from './TreeSelect'; +import TreeNode from './TreeNode'; +import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil'; + +export { TreeNode, SHOW_ALL, SHOW_CHILD, SHOW_PARENT }; + +export default TreeSelect; diff --git a/src/interface.ts b/src/interface.ts new file mode 100644 index 00000000..7ed7c0e3 --- /dev/null +++ b/src/interface.ts @@ -0,0 +1,82 @@ +import React from 'react'; + +export type SelectSource = 'option' | 'selection' | 'input' | 'clear'; + +export type Key = string | number; + +export type RawValueType = string | number; + +export interface LabelValueType { + key?: Key; + value?: RawValueType; + label?: React.ReactNode; + /** Only works on `treeCheckStrictly` */ + halfChecked?: boolean; +} + +export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[]; + +export interface DataNode { + value?: RawValueType; + title?: React.ReactNode; + label?: React.ReactNode; + key?: Key; + disabled?: boolean; + disableCheckbox?: boolean; + checkable?: boolean; + children?: DataNode[]; + + /** Customize data info */ + [prop: string]: any; +} + +export interface InnerDataNode extends DataNode { + key: Key; + value: RawValueType; + label?: React.ReactNode; + children?: InnerDataNode[]; +} + +export interface LegacyDataNode extends DataNode { + props: any; +} + +export interface TreeDataNode extends DataNode { + key: Key; + children?: TreeDataNode[]; +} + +export interface FlattenDataNode { + data: DataNode; + key: Key; + level: number; +} + +export interface SimpleModeConfig { + id?: Key; + pId?: Key; + rootPId?: Key; +} + +/** @deprecated This is only used for legacy compatible. Not works on new code. */ +export interface LegacyCheckedNode { + pos: string; + node: React.ReactElement; + children?: LegacyCheckedNode[]; +} + +export interface ChangeEventExtra { + /** @deprecated Please save prev value by control logic instead */ + preValue: LabelValueType[]; + triggerValue: RawValueType; + /** @deprecated Use `onSelect` or `onDeselect` instead. */ + selected?: boolean; + /** @deprecated Use `onSelect` or `onDeselect` instead. */ + checked?: boolean; + + // Not sure if exist user still use this. We have to keep but not recommend user to use + /** @deprecated This prop not work as react node anymore. */ + triggerNode: React.ReactElement; + /** @deprecated This prop not work as react node anymore. */ + allCheckedNodes: LegacyCheckedNode[]; +} diff --git a/src/propTypes.js b/src/propTypes.js deleted file mode 100644 index 08c66f47..00000000 --- a/src/propTypes.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import { isLabelInValue } from './util'; - -const internalValProp = PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, -]); - -export function genArrProps(propType) { - return PropTypes.oneOfType([ - propType, - PropTypes.arrayOf(propType), - ]); -} - -/** - * Origin code check `multiple` is true when `treeCheckStrictly` & `labelInValue`. - * But in process logic is already cover to array. - * Check array is not necessary. Let's simplify this check logic. - */ -export function valueProp(...args) { - const [props, propName, Component] = args; - - if (isLabelInValue(props)) { - const err = genArrProps(PropTypes.shape({ - label: PropTypes.node, - value: internalValProp, - }))(...args); - if (err) { - return new Error( - `Invalid prop \`${propName}\` supplied to \`${Component}\`. ` + - `You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.` - ); - } - return null; - } - - const err = genArrProps(internalValProp)(...args); - if (err) { - return new Error( - `Invalid prop \`${propName}\` supplied to \`${Component}\`. ` + - `You should use string or [string] instead.` - ); - } - return null; -} diff --git a/src/strategies.js b/src/strategies.js deleted file mode 100644 index b5cf1944..00000000 --- a/src/strategies.js +++ /dev/null @@ -1,3 +0,0 @@ -export const SHOW_ALL = 'SHOW_ALL'; -export const SHOW_PARENT = 'SHOW_PARENT'; -export const SHOW_CHILD = 'SHOW_CHILD'; diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 099763ab..00000000 --- a/src/util.js +++ /dev/null @@ -1,439 +0,0 @@ -import React from 'react'; -import warning from 'warning'; -import { - convertDataToTree as rcConvertDataToTree, - convertTreeToEntities as rcConvertTreeToEntities, - conductCheck as rcConductCheck, -} from 'rc-tree/lib/util'; -import toNodeArray from 'rc-util/lib/Children/toArray'; -import { hasClass } from 'rc-util/lib/Dom/class'; -import { SHOW_CHILD, SHOW_PARENT } from './strategies'; - -let warnDeprecatedLabel = false; - -// =================== DOM ===================== -export function findPopupContainer(node, prefixClass) { - let current = node; - while (current) { - if (hasClass(current, prefixClass)) { - return current; - } - current = current.parentNode; - } - - return null; -} - -// =================== MISC ==================== -export function toTitle(title) { - if (typeof title === 'string') { - return title; - } - return null; -} - -export function toArray(data) { - if (data === undefined || data === null) return []; - - return Array.isArray(data) ? data : [data]; -} - -// Shallow copy of React 16.3 createRef api -export function createRef() { - const func = function setRef(node) { - func.current = node; - }; - return func; -} - -// =============== Legacy =============== -export const UNSELECTABLE_STYLE = { - userSelect: 'none', - WebkitUserSelect: 'none', -}; - -export const UNSELECTABLE_ATTRIBUTE = { - unselectable: 'unselectable', -}; - -/** - * Convert position list to hierarchy structure. - * This is little hack since use '-' to split the position. - */ -export function flatToHierarchy(positionList) { - if (!positionList.length) { - return []; - } - - const entrances = {}; - - // Prepare the position map - const posMap = {}; - const parsedList = positionList.slice().map(entity => { - const clone = { - ...entity, - fields: entity.pos.split('-'), - }; - delete clone.children; - return clone; - }); - - parsedList.forEach(entity => { - posMap[entity.pos] = entity; - }); - - parsedList.sort((a, b) => { - return a.fields.length - b.fields.length; - }); - - // Create the hierarchy - parsedList.forEach(entity => { - const parentPos = entity.fields.slice(0, -1).join('-'); - const parentEntity = posMap[parentPos]; - - if (!parentEntity) { - entrances[entity.pos] = entity; - } else { - parentEntity.children = parentEntity.children || []; - parentEntity.children.push(entity); - } - - // Some time position list provide `key`, we don't need it - delete entity.key; - delete entity.fields; - }); - - return Object.keys(entrances).map(key => entrances[key]); -} - -// =============== Accessibility =============== -let ariaId = 0; - -export function resetAriaId() { - ariaId = 0; -} - -export function generateAriaId(prefix) { - ariaId += 1; - return `${prefix}_${ariaId}`; -} - -export function isLabelInValue(props) { - const { treeCheckable, treeCheckStrictly, labelInValue } = props; - if (treeCheckable && treeCheckStrictly) { - return true; - } - return labelInValue || false; -} - -// =================== Tree ==================== -export function parseSimpleTreeData(treeData, { id, pId, rootPId }) { - const keyNodes = {}; - const rootNodeList = []; - - // Fill in the map - const nodeList = treeData.map(node => { - const clone = { ...node }; - const key = clone[id]; - keyNodes[key] = clone; - clone.key = clone.key || key; - return clone; - }); - - // Connect tree - nodeList.forEach(node => { - const parentKey = node[pId]; - const parent = keyNodes[parentKey]; - - // Fill parent - if (parent) { - parent.children = parent.children || []; - parent.children.push(node); - } - - // Fill root tree node - if (parentKey === rootPId || (!parent && rootPId === null)) { - rootNodeList.push(node); - } - }); - - return rootNodeList; -} - -/** - * Detect if position has relation. - * e.g. 1-2 related with 1-2-3 - * e.g. 1-3-2 related with 1 - * e.g. 1-2 not related with 1-21 - */ -export function isPosRelated(pos1, pos2) { - const fields1 = pos1.split('-'); - const fields2 = pos2.split('-'); - - const minLen = Math.min(fields1.length, fields2.length); - for (let i = 0; i < minLen; i += 1) { - if (fields1[i] !== fields2[i]) { - return false; - } - } - return true; -} - -/** - * This function is only used on treeNode check (none treeCheckStrictly but has searchInput). - * We convert entity to { node, pos, children } format. - * This is legacy bug but we still need to do with it. - * @param entity - */ -export function cleanEntity({ node, pos, children }) { - const instance = { - node, - pos, - }; - - if (children) { - instance.children = children.map(cleanEntity); - } - - return instance; -} - -/** - * Get a filtered TreeNode list by provided treeNodes. - * [Legacy] Since `Tree` use `key` as map but `key` will changed by React, - * we have to convert `treeNodes > data > treeNodes` to keep the key. - * Such performance hungry! - * - * We pass `Component` as argument is to fix eslint issue. - */ -export function getFilterTree(treeNodes, searchValue, filterFunc, valueEntities, Component) { - if (!searchValue) { - return null; - } - - function mapFilteredNodeToData(node) { - if (!node) return null; - - let match = false; - if (filterFunc(searchValue, node)) { - match = true; - } - - const children = toNodeArray(node.props.children) - .map(mapFilteredNodeToData) - .filter(n => n); - - if (children.length || match) { - return ( - - {children} - - ); - } - - return null; - } - - return treeNodes.map(mapFilteredNodeToData).filter(node => node); -} - -// =================== Value =================== -/** - * Convert value to array format to make logic simplify. - */ -export function formatInternalValue(value, props) { - const valueList = toArray(value); - - // Parse label in value - if (isLabelInValue(props)) { - return valueList.map(val => { - if (typeof val !== 'object' || !val) { - return { - value: '', - label: '', - }; - } - - return val; - }); - } - - return valueList.map(val => ({ - value: val, - })); -} - -export function getLabel(wrappedValue, entity, treeNodeLabelProp) { - if (wrappedValue.label) { - return wrappedValue.label; - } - - if (entity && entity.node.props) { - return entity.node.props[treeNodeLabelProp]; - } - - // Since value without entity will be in missValueList. - // This code will never reached, but we still need this in case. - return wrappedValue.value; -} - -/** - * Convert internal state `valueList` to user needed value list. - * This will return an array list. You need check if is not multiple when return. - * - * `allCheckedNodes` is used for `treeCheckStrictly` - */ -export function formatSelectorValue(valueList, props, valueEntities) { - const { treeNodeLabelProp, treeCheckable, treeCheckStrictly, showCheckedStrategy } = props; - - // Will hide some value if `showCheckedStrategy` is set - if (treeCheckable && !treeCheckStrictly) { - const values = {}; - valueList.forEach(wrappedValue => { - values[wrappedValue.value] = wrappedValue; - }); - const hierarchyList = flatToHierarchy(valueList.map(({ value }) => valueEntities[value])); - - if (showCheckedStrategy === SHOW_PARENT) { - // Only get the parent checked value - return hierarchyList.map(({ node: { props: { value } } }) => ({ - label: getLabel(values[value], valueEntities[value], treeNodeLabelProp), - value, - })); - } - - if (showCheckedStrategy === SHOW_CHILD) { - // Only get the children checked value - const targetValueList = []; - - // Find the leaf children - const traverse = ({ - node: { - props: { value }, - }, - children, - }) => { - if (!children || children.length === 0) { - targetValueList.push({ - label: getLabel(values[value], valueEntities[value], treeNodeLabelProp), - value, - }); - return; - } - - children.forEach(entity => { - traverse(entity); - }); - }; - - hierarchyList.forEach(entity => { - traverse(entity); - }); - - return targetValueList; - } - } - - return valueList.map(wrappedValue => ({ - label: getLabel(wrappedValue, valueEntities[wrappedValue.value], treeNodeLabelProp), - value: wrappedValue.value, - })); -} - -/** - * Use `rc-tree` convertDataToTree to convert treeData to TreeNodes. - * This will change the label to title value - */ -function processProps(props) { - const { title, label, key, value } = props; - const cloneProps = { ...props }; - - // Warning user not to use deprecated label prop. - if (label && !title) { - if (!warnDeprecatedLabel) { - warning(false, "'label' in treeData is deprecated. Please use 'title' instead."); - warnDeprecatedLabel = true; - } - - cloneProps.title = label; - } - - if (!key) { - cloneProps.key = value; - } - - return cloneProps; -} - -export function convertDataToTree(treeData) { - return rcConvertDataToTree(treeData, { processProps }); -} - -/** - * Use `rc-tree` convertTreeToEntities for entities calculation. - * We have additional entities of `valueEntities` - */ -function initWrapper(wrapper) { - return { - ...wrapper, - valueEntities: {}, - }; -} - -function processEntity(entity, wrapper) { - const value = entity.node.props.value; - entity.value = value; - - // This should be empty, or will get error message. - const currentEntity = wrapper.valueEntities[value]; - if (currentEntity) { - warning( - false, - `Conflict! value of node '${entity.key}' (${value}) has already used by node '${ - currentEntity.key - }'.`, - ); - } - wrapper.valueEntities[value] = entity; -} - -export function convertTreeToEntities(treeNodes) { - return rcConvertTreeToEntities(treeNodes, { - initWrapper, - processEntity, - }); -} - -/** - * https://github.com/ant-design/ant-design/issues/13328 - * We need calculate the half check key when searchValue is set. - */ -// TODO: This logic may better move to rc-tree -export function getHalfCheckedKeys(valueList, valueEntities) { - const values = {}; - - // Fill checked keys - valueList.forEach(({ value }) => { - values[value] = false; - }); - - // Fill half checked keys - valueList.forEach(({ value }) => { - let current = valueEntities[value]; - - while (current && current.parent) { - const parentValue = current.parent.value; - if (parentValue in values) break; - values[parentValue] = true; - - current = current.parent; - } - }); - - // Get half keys - return Object.keys(values) - .filter(value => values[value]) - .map(value => valueEntities[value].key); -} - -export const conductCheck = rcConductCheck; diff --git a/src/utils/legacyUtil.tsx b/src/utils/legacyUtil.tsx new file mode 100644 index 00000000..db1f625f --- /dev/null +++ b/src/utils/legacyUtil.tsx @@ -0,0 +1,154 @@ +import React from 'react'; +import toArray from 'rc-util/lib/Children/toArray'; +import warning from 'rc-util/lib/warning'; +import { + DataNode, + LegacyDataNode, + ChangeEventExtra, + InnerDataNode, + RawValueType, + LegacyCheckedNode, +} from '../interface'; +import TreeNode from '../TreeNode'; + +export function convertChildrenToData(nodes: React.ReactNode): DataNode[] { + return toArray(nodes) + .map((node: React.ReactElement) => { + if (!React.isValidElement(node) || !node.type) { + return null; + } + + const { + key, + props: { children, value, ...restProps }, + } = node as React.ReactElement; + + const data = { + key, + value, + ...restProps, + }; + + const childData = convertChildrenToData(children); + if (childData.length) { + data.children = childData; + } + + return data; + }) + .filter(data => data); +} + +export function fillLegacyProps(dataNode: DataNode): LegacyDataNode { + // Skip if not dataNode exist + if (!dataNode) { + return dataNode as LegacyDataNode; + } + + const cloneNode = { ...dataNode }; + + if (!('props' in cloneNode)) { + Object.defineProperty(cloneNode, 'props', { + get() { + warning( + false, + 'New `rc-tree-select` not support return node instance as argument anymore. Please consider to remove `props` access.', + ); + return cloneNode; + }, + }); + } + + return cloneNode as LegacyDataNode; +} + +export function fillAdditionalInfo( + extra: ChangeEventExtra, + triggerValue: RawValueType, + checkedValues: RawValueType[], + treeData: InnerDataNode[], + showPosition: boolean, +) { + let triggerNode: React.ReactNode = null; + let nodeList: LegacyCheckedNode[] = null; + + function generateMap() { + function dig(list: InnerDataNode[], level = '0', parentIncluded = false) { + return list + .map((dataNode, index) => { + const pos = `${level}-${index}`; + const included = checkedValues.includes(dataNode.value); + const children = dig(dataNode.children || [], pos, included); + const node = {children.map(child => child.node)}; + + // Link with trigger node + if (triggerValue === dataNode.value) { + triggerNode = node; + } + + if (included) { + const checkedNode: LegacyCheckedNode = { + pos, + node, + children, + }; + + if (!parentIncluded) { + nodeList.push(checkedNode); + } + + return checkedNode; + } + return null; + }) + .filter(node => node); + } + + if (!nodeList) { + nodeList = []; + + dig(treeData); + + // Sort to keep the checked node length + nodeList.sort( + ( + { + node: { + props: { value: val1 }, + }, + }, + { + node: { + props: { value: val2 }, + }, + }, + ) => { + const index1 = checkedValues.indexOf(val1); + const index2 = checkedValues.indexOf(val2); + return index1 - index2; + }, + ); + } + } + + Object.defineProperty(extra, 'triggerNode', { + get() { + warning(false, '`triggerNode` is deprecated. Please consider decoupling data with node.'); + generateMap(); + + return triggerNode; + }, + }); + Object.defineProperty(extra, 'allCheckedNodes', { + get() { + warning(false, '`allCheckedNodes` is deprecated. Please consider decoupling data with node.'); + generateMap(); + + if (showPosition) { + return nodeList; + } + + return nodeList.map(({ node }) => node); + }, + }); +} diff --git a/src/utils/strategyUtil.ts b/src/utils/strategyUtil.ts new file mode 100644 index 00000000..4c5a64a1 --- /dev/null +++ b/src/utils/strategyUtil.ts @@ -0,0 +1,46 @@ +import { DataEntity } from 'rc-tree/lib/interface'; +import { RawValueType, Key, DataNode } from '../interface'; +import { isCheckDisabled } from './valueUtil'; + +export const SHOW_ALL = 'SHOW_ALL'; +export const SHOW_PARENT = 'SHOW_PARENT'; +export const SHOW_CHILD = 'SHOW_CHILD'; + +export type CheckedStrategy = typeof SHOW_ALL | typeof SHOW_PARENT | typeof SHOW_CHILD; + +export function formatStrategyKeys( + keys: Key[], + strategy: CheckedStrategy, + keyEntities: Record, +): RawValueType[] { + const keySet = new Set(keys); + + if (strategy === SHOW_CHILD) { + return keys.filter(key => { + const entity = keyEntities[key]; + + if ( + entity && + entity.children && + entity.children.every( + ({ node }) => isCheckDisabled(node) || keySet.has((node as DataNode).key), + ) + ) { + return false; + } + return true; + }); + } + if (strategy === SHOW_PARENT) { + return keys.filter(key => { + const entity = keyEntities[key]; + const parent = entity ? entity.parent : null; + + if (parent && !isCheckDisabled(parent.node) && keySet.has((parent.node as DataNode).key)) { + return false; + } + return true; + }); + } + return keys; +} diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts new file mode 100644 index 00000000..015aa111 --- /dev/null +++ b/src/utils/valueUtil.ts @@ -0,0 +1,188 @@ +import { flattenTreeData } from 'rc-tree/lib/utils/treeUtil'; +import { FlattenNode } from 'rc-tree/lib/interface'; +import { FilterFunc } from 'rc-select/lib/interface/generator'; +import { + FlattenDataNode, + Key, + RawValueType, + DataNode, + DefaultValueType, + LabelValueType, + LegacyDataNode, +} from '../interface'; +import { fillLegacyProps } from './legacyUtil'; +import { SkipType } from '../hooks/useKeyValueMapping'; + +export function toArray(value: T | T[]): T[] { + if (Array.isArray(value)) { + return value; + } + return value !== undefined ? [value] : []; +} + +export function findValueOption(values: RawValueType[], options: FlattenDataNode[]): DataNode[] { + const optionMap: Map = new Map(); + + options.forEach(flattenItem => { + const { data } = flattenItem; + optionMap.set(data.value, data); + }); + + return values.map(val => fillLegacyProps(optionMap.get(val))); +} + +export function isValueDisabled(value: RawValueType, options: FlattenDataNode[]): boolean { + const option = findValueOption([value], options)[0]; + if (option) { + return option.disabled; + } + + return false; +} + +export function isCheckDisabled(node: DataNode) { + return node.disabled || node.disableCheckbox || node.checkable === false; +} + +interface TreeDataNode { + key: Key; +} + +function getLevel({ parent }: FlattenNode): number { + let level = 0; + let current = parent; + + while (current) { + current = current.parent; + level += 1; + } + + return level; +} + +/** + * Before reuse `rc-tree` logic, we need to add key since TreeSelect use `value` instead of `key`. + */ +export function flattenOptions(options: DataNode[]): FlattenDataNode[] { + // Add missing key + function fillKey(list: DataNode[]): TreeDataNode[] { + return (list || []).map(node => { + const { value, key, children } = node; + + const clone = { + ...node, + key: 'key' in node ? key : value, + }; + + if (children) { + clone.children = fillKey(children); + } + + return clone; + }); + } + + const flattenList = flattenTreeData(fillKey(options), true); + + return flattenList.map(node => ({ + key: node.data.key, + data: node.data, + level: getLevel(node), + })); +} + +function getDefaultFilterOption(optionFilterProp: string) { + return (searchValue: string, dataNode: LegacyDataNode) => { + const value = dataNode[optionFilterProp]; + + return String(value) + .toLowerCase() + .includes(String(searchValue).toLowerCase()); + }; +} + +/** Filter options and return a new options by the search text */ +export function filterOptions( + searchValue: string, + options: DataNode[], + { + optionFilterProp, + filterOption, + }: { optionFilterProp: string; filterOption: boolean | FilterFunc }, +): DataNode[] { + if (filterOption === false) { + return options; + } + + let filterOptionFunc: FilterFunc; + if (typeof filterOption === 'function') { + filterOptionFunc = filterOption; + } else { + filterOptionFunc = getDefaultFilterOption(optionFilterProp); + } + + function dig(list: DataNode[], keepAll: boolean = false) { + return list + .map(dataNode => { + const { children } = dataNode; + + const match = keepAll || filterOptionFunc(searchValue, fillLegacyProps(dataNode)); + const childList = dig(children || [], match); + + if (match || childList.length) { + return { + ...dataNode, + children: childList, + }; + } + return null; + }) + .filter(node => node); + } + + return dig(options); +} + +export function getRawValueLabeled( + values: RawValueType[], + prevValue: DefaultValueType, + getEntityByValue: (value: RawValueType, skipType?: SkipType) => FlattenDataNode, + getLabelProp: (node: DataNode) => React.ReactNode, +): LabelValueType[] { + const valueMap = new Map(); + + toArray(prevValue).forEach(item => { + if (item && typeof item === 'object' && 'value' in item) { + valueMap.set(item.value, item); + } + }); + + return values.map(val => { + const item: LabelValueType = { value: val }; + const entity = getEntityByValue(val); + const label = entity ? getLabelProp(entity.data) : val; + + if (valueMap.has(val)) { + const labeledValue = valueMap.get(val); + item.label = 'label' in labeledValue ? labeledValue.label : label; + if ('halfChecked' in labeledValue) { + item.halfChecked = labeledValue.halfChecked; + } + } else { + item.label = label; + } + + return item; + }); +} + +export function addValue(rawValues: RawValueType[], value: RawValueType) { + const values = new Set(rawValues); + values.add(value); + return Array.from(values); +} +export function removeValue(rawValues: RawValueType[], value: RawValueType) { + const values = new Set(rawValues); + values.delete(value); + return Array.from(values); +} diff --git a/src/utils/warningPropsUtil.ts b/src/utils/warningPropsUtil.ts new file mode 100644 index 00000000..a172ab56 --- /dev/null +++ b/src/utils/warningPropsUtil.ts @@ -0,0 +1,38 @@ +import warning from 'rc-util/lib/warning'; +import { TreeSelectProps } from '../TreeSelect'; +import { toArray } from './valueUtil'; + +function warningProps(props: TreeSelectProps) { + const { + searchPlaceholder, + treeCheckStrictly, + treeCheckable, + labelInValue, + value, + multiple, + } = props; + + warning(!searchPlaceholder, '`searchPlaceholder` has been removed.'); + + if (treeCheckStrictly && labelInValue === false) { + warning(false, '`treeCheckStrictly` will force set `labelInValue` to `true`.'); + } + + if (labelInValue || treeCheckStrictly) { + warning( + toArray(value).every(val => val && typeof val === 'object' && 'value' in val), + 'Invalid prop `value` supplied to `TreeSelect`. You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.', + ); + } + + if (treeCheckStrictly || multiple || treeCheckable) { + warning( + !value || Array.isArray(value), + '`value` should be an array when `TreeSelect` is checkable or multiple.', + ); + } else { + warning(!Array.isArray(value), '`value` should not be array when `TreeSelect` is single mode.'); + } +} + +export default warningProps; diff --git a/tests/Select.SearchInput.spec.js b/tests/Select.SearchInput.spec.js index 3615f4d0..89e698c4 100644 --- a/tests/Select.SearchInput.spec.js +++ b/tests/Select.SearchInput.spec.js @@ -2,78 +2,28 @@ import React from 'react'; import { mount } from 'enzyme'; import TreeSelect, { TreeNode } from '../src'; -import { resetAriaId } from '../src/util'; describe('TreeSelect.SearchInput', () => { - beforeEach(() => { - resetAriaId(); - }); - - const createSelect = (props) => { - return mount( -
    - - - - -
    - ); - } - - describe('click placeholder to get focus', () => { - it('single', (done) => { - const wrapper = createSelect({ showSearch: true }); - - setTimeout(() => { - // Focus outside - wrapper.find('.pre-focus').instance().focus(); - - // Click placeholder - wrapper.find('.rc-tree-select-search__field__placeholder').simulate('click'); - - const $input = wrapper.find('input.rc-tree-select-search__field').instance(); - expect($input).toBe(document.activeElement); - - done(); - }, 10); - }); - - it('multiple', (done) => { - const wrapper = createSelect({ multiple: true }); - - setTimeout(() => { - // Focus outside - wrapper.find('.pre-focus').instance().focus(); - - // Click placeholder - wrapper.find('.rc-tree-select-search__field__placeholder').simulate('click'); - - const $input = wrapper.find('input.rc-tree-select-search__field').instance(); - expect($input).toBe(document.activeElement); - - done(); - }, 10); - }); - }); - it('select item will clean searchInput', () => { const onSearch = jest.fn(); const wrapper = mount( - +
    , ); - wrapper.find('.rc-tree-select-search__field').simulate('change', { target: { value: 'test' } }); - expect(onSearch).toBeCalledWith('test'); + wrapper.search('test'); + expect(onSearch).toHaveBeenCalledWith('test'); onSearch.mockReset(); - wrapper.find('.rc-tree-select-tree-node-content-wrapper').simulate('click'); - expect(onSearch).toBeCalledWith(''); + wrapper.selectNode(); + expect(onSearch).not.toHaveBeenCalled(); + expect( + wrapper + .find('input') + .first() + .props().value, + ).toBeFalsy(); }); }); diff --git a/tests/Select.checkable.spec.js b/tests/Select.checkable.spec.js index 2ccf4c1c..afcb2139 100644 --- a/tests/Select.checkable.spec.js +++ b/tests/Select.checkable.spec.js @@ -2,21 +2,8 @@ import React from 'react'; import { mount } from 'enzyme'; import TreeSelect, { SHOW_PARENT, SHOW_ALL, TreeNode } from '../src'; -import { resetAriaId } from '../src/util'; describe('TreeSelect.checkable', () => { - beforeEach(() => { - resetAriaId(); - }); - - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - it('allow clear when controlled', () => { const treeData = [ { @@ -32,7 +19,6 @@ describe('TreeSelect.checkable', () => { ], }, ]; - class App extends React.Component { state = { value: [], @@ -57,13 +43,10 @@ describe('TreeSelect.checkable', () => { } } const wrapper = mount(); - // open wrapper.openSelect(); - // select - wrapper.find('.rc-tree-select-tree-checkbox').simulate('click'); - // clear - wrapper.find('.rc-tree-select-selection__clear').simulate('click'); - expect(wrapper.find('.rc-tree-select-selection__choice')).toHaveLength(0); + wrapper.selectNode(); + wrapper.clearSelection(); + expect(wrapper.getSelection()).toHaveLength(0); }); // https://github.com/ant-design/ant-design/issues/6731 @@ -141,21 +124,19 @@ describe('TreeSelect.checkable', () => { } } const wrapper = mount(); - expect(wrapper.find('.rc-tree-select-selection__choice')).toHaveLength(1); - // open + expect(wrapper.getSelection()).toHaveLength(1); + wrapper.openSelect(); - // select - wrapper - .find('.rc-tree-select-tree-checkbox') - .at(2) - .simulate('click'); - expect(wrapper.find('.rc-tree-select-selection__choice')).toHaveLength(2); - // clear - wrapper.find('.rc-tree-select-selection__clear').simulate('click'); - expect(wrapper.find('.rc-tree-select-selection__choice')).toHaveLength(0); - // disabled + wrapper.selectNode(2); + expect(wrapper.getSelection()).toHaveLength(2); + + // Clear all + wrapper.clearAll(); + expect(wrapper.getSelection()).toHaveLength(0); + + // disabled - legacy, just keep it though it's meaningless anymore wrapper.find('#checkbox').simulate('change', { target: { checked: true } }); - expect(wrapper.find('.rc-tree-select-selection__choice')).toHaveLength(0); + expect(wrapper.getSelection()).toHaveLength(0); }); // Fix https://github.com/ant-design/ant-design/issues/7312#issuecomment-324865971 @@ -183,23 +164,12 @@ describe('TreeSelect.checkable', () => { onChange={handleChange} />, ); - // open - wrapper.find('.rc-tree-select').simulate('click'); - jest.runAllTimers(); - wrapper.update(); - // select - wrapper - .find('.rc-tree-select-tree-checkbox') - .at(0) - .simulate('click'); - expect(handleChange).toBeCalled(); - expect(wrapper.find('.rc-tree-select-selection__choice__content').length).toBe(1); - expect( - wrapper - .find('.rc-tree-select-selection__choice__content') - .at(0) - .text(), - ).toBe('1-1'); + + wrapper.openSelect(); + wrapper.selectNode(); + expect(handleChange).toHaveBeenCalled(); + expect(wrapper.getSelection()).toHaveLength(1); + expect(wrapper.getSelection(0).text()).toEqual('1-1'); }); // Fix https://github.com/ant-design/ant-design/issues/8581 @@ -227,28 +197,15 @@ describe('TreeSelect.checkable', () => { onChange={handleChange} />, ); - // open - wrapper.find('.rc-tree-select').simulate('click'); - jest.runAllTimers(); - // select - wrapper - .find('.rc-tree-select-tree-node-content-wrapper') - .at(0) - .simulate('click'); - expect(handleChange).toBeCalled(); - expect(wrapper.find('.rc-tree-select-selection__choice__content').length).toBe(1); - expect( - wrapper - .find('.rc-tree-select-selection__choice__content') - .at(0) - .text(), - ).toBe('1-1'); - // clear - wrapper - .find('.rc-tree-select-tree-node-content-wrapper') - .at(0) - .simulate('click'); - expect(wrapper.find('.rc-tree-select-selection__choice__content').length).toBe(0); + + wrapper.openSelect(); + wrapper.selectNode(); + expect(handleChange).toHaveBeenCalled(); + expect(wrapper.getSelection()).toHaveLength(1); + expect(wrapper.getSelection(0).text()).toBe('1-1'); + + wrapper.selectNode(0); + expect(wrapper.getSelection()).toHaveLength(0); }); it('clear selected value and input value', () => { @@ -270,14 +227,16 @@ describe('TreeSelect.checkable', () => { />, ); wrapper.openSelect(); - wrapper - .find('.rc-tree-select-tree-checkbox') - .at(0) - .simulate('click'); - wrapper.find('input').simulate('change', { target: { value: 'foo' } }); - wrapper.find('.rc-tree-select-selection__clear').simulate('click'); - expect(wrapper.state().valueList).toEqual([]); - expect(wrapper.state().searchValue).toBe(''); + wrapper.selectNode(0); + wrapper.search('foo'); + wrapper.clearAll(); + expect(wrapper.getSelection()).toHaveLength(0); + expect( + wrapper + .find('input') + .first() + .props().value, + ).toBe(''); }); describe('uncheck', () => { @@ -300,16 +259,11 @@ describe('TreeSelect.checkable', () => {
    , ); - describe('remove by selector', () => { it('not treeCheckStrictly', () => { const wrapper = createSelect(); expect(wrapper.render()).toMatchSnapshot(); - - wrapper - .find('.rc-tree-select-selection__choice__remove') - .at(1) - .simulate('click'); + wrapper.clearSelection(1); expect(wrapper.render()).toMatchSnapshot(); }); @@ -321,16 +275,12 @@ describe('TreeSelect.checkable', () => { defaultValue: [val('0'), val('0-0'), val('0-0-0')], onChange, }); - wrapper - .find('.rc-tree-select-selection__choice__remove') - .at(1) - .simulate('click'); - - expect(onChange.mock.calls[0][0]).toEqual([ - { label: '0', value: '0' }, - { label: '0-0-0', value: '0-0-0' }, - ]); - expect(onChange.mock.calls[0][1]).toEqual(null); + wrapper.clearSelection(1); + expect(onChange).toHaveBeenCalledWith( + [{ label: '0', value: '0' }, { label: '0-0-0', value: '0-0-0' }], + null, + expect.anything(), + ); const getProps = index => { const node = onChange.mock.calls[0][2].allCheckedNodes[index]; @@ -348,10 +298,7 @@ describe('TreeSelect.checkable', () => { const wrapper = createSelect({ searchValue: '0' }); expect(wrapper.render()).toMatchSnapshot(); - wrapper - .find('.rc-tree-select-tree-checkbox') - .at(1) - .simulate('click'); + wrapper.selectNode(1); expect(wrapper.render()).toMatchSnapshot(); }); @@ -384,24 +331,14 @@ describe('TreeSelect.checkable', () => { ], }, ]; - const wrapper = mount(); + wrapper.search('58'); + wrapper.selectNode(2); + expect(wrapper.getSelection()).toHaveLength(1); - wrapper.find('.rc-tree-select-search__field').simulate('change', { target: { value: '58' } }); - wrapper - .find(TreeNode) - .at(2) - .find('.rc-tree-select-tree-checkbox') - .simulate('click'); - expect(wrapper.state().valueList.length).toBe(3); - - wrapper.find('.rc-tree-select-search__field').simulate('change', { target: { value: '59' } }); - wrapper - .find(TreeNode) - .at(2) - .find('.rc-tree-select-tree-checkbox') - .simulate('click'); - expect(wrapper.state().valueList.length).toBe(6); + wrapper.search('59'); + wrapper.selectNode(2); + expect(wrapper.getSelection()).toHaveLength(2); }); }); @@ -457,14 +394,9 @@ describe('TreeSelect.checkable', () => { />, ); - wrapper.find('.rc-tree-select-search__field').simulate('change', { target: { value: '0-0' } }); - wrapper - .find('.rc-tree-select-tree-checkbox') - .at(0) - .simulate('click'); - const keyList = onChange.mock.calls[0][0]; - - expect(keyList.sort()).toEqual(['0-1-0', '0-1-2'].sort()); + wrapper.search('0-0'); + wrapper.selectNode(0); + expect(onChange).toHaveBeenCalledWith(['0-1-0', '0-1-2'], expect.anything(), expect.anything()); }); // https://github.com/ant-design/ant-design/issues/13328 @@ -499,21 +431,16 @@ describe('TreeSelect.checkable', () => { />, ); - wrapper - .find('.rc-tree-select-search__field') - .simulate('change', { target: { value: '0-0-1' } }); - wrapper - .find('.rc-tree-select-tree-checkbox') - .at(1) - .simulate('click'); + wrapper.search('0-0-1'); + wrapper.selectNode(1); + expect(onChange).toHaveBeenCalledWith(['0-0-1'], expect.anything(), expect.anything()); - expect(onChange.mock.calls[0][0]).toEqual(['0-0-1']); expect( wrapper .find('.rc-tree-select-tree-checkbox') .at(0) .hasClass('rc-tree-select-tree-checkbox-indeterminate'), - ).toBe(true); + ).toBeTruthy(); }); it('controlled', () => { @@ -547,15 +474,9 @@ describe('TreeSelect.checkable', () => { const wrapper = mount(); - wrapper - .find('.rc-tree-select-search__field') - .simulate('change', { target: { value: '0-0-1' } }); - wrapper - .find('.rc-tree-select-tree-checkbox') - .at(1) - .simulate('click'); - - expect(onChange).toBeCalled(); + wrapper.search('0-0-1'); + wrapper.selectNode(1); + expect(onChange).toHaveBeenCalled(); expect( wrapper @@ -575,12 +496,7 @@ describe('TreeSelect.checkable', () => { , ); - expect(wrapper.state().selectorValueList).toEqual([ - { - label: '0-0', - value: '0-0', - }, - ]); + expect(wrapper.getSelection(0).text()).toEqual('0-0'); }); it('extra.allCheckedNodes', () => { @@ -593,19 +509,19 @@ describe('TreeSelect.checkable', () => { ); // Just click - wrapper.find('.rc-tree-select-tree-checkbox').simulate('click'); + wrapper.selectNode(); expect(onChange.mock.calls[0][2].allCheckedNodes).toEqual([ expect.objectContaining({ pos: '0-0', }), ]); - wrapper.find('.rc-tree-select-selection__choice__remove').simulate('click'); + wrapper.clearSelection(0); onChange.mockReset(); // By search - wrapper.find('input').simulate('change', { target: { value: '0' } }); - wrapper.find('.rc-tree-select-tree-checkbox').simulate('click'); + wrapper.search('0'); + wrapper.selectNode(); expect(onChange.mock.calls[0][2].allCheckedNodes).toEqual([ expect.objectContaining({ pos: '0-0', @@ -666,14 +582,8 @@ describe('TreeSelect.checkable', () => { />, ); - wrapper - .find('.rc-tree-select-search__field') - .simulate('change', { target: { value: '0-0-0' } }); - wrapper - .find(TreeNode) - .at(1) - .find('.rc-tree-select-tree-checkbox') - .simulate('click'); + wrapper.search('0-0-0'); + wrapper.selectNode(1); expect(onChange.mock.calls[0][0]).toEqual([ { label: 'Node2', value: '0-1' }, @@ -693,8 +603,49 @@ describe('TreeSelect.checkable', () => { const wrapper = mount(); - const choiceNode = wrapper.find('.rc-tree-select-selection__choice'); - expect(choiceNode.length).toBeTruthy(); - expect(choiceNode.find('.rc-tree-select-selection__choice__remove').length).toBeFalsy(); + expect(wrapper.getSelection().length).toBeTruthy(); + expect(wrapper.find('.rc-tree-select-selection-item-remove').length).toBeFalsy(); + }); + + it('treeCheckStrictly can set halfChecked', () => { + const onChange = jest.fn(); + const wrapper = mount( + , + ); + + function getTreeNode(index) { + return wrapper.find('.rc-tree-select-tree-treenode').at(index); + } + + expect( + getTreeNode(0).hasClass('rc-tree-select-tree-treenode-checkbox-indeterminate'), + ).toBeTruthy(); + expect( + getTreeNode(1).hasClass('rc-tree-select-tree-treenode-checkbox-indeterminate'), + ).toBeFalsy(); + + wrapper.selectNode(1); + expect(onChange).toHaveBeenCalledWith( + [ + { + label: 'Full Check', + value: 'full', + }, + { + value: 'half', + label: 'Half Check', + halfChecked: true, + }, + ], + null, + expect.anything(), + ); }); }); diff --git a/tests/Select.multiple.spec.js b/tests/Select.multiple.spec.js index 58e1c105..70b2ed3e 100644 --- a/tests/Select.multiple.spec.js +++ b/tests/Select.multiple.spec.js @@ -1,10 +1,8 @@ /* eslint-disable no-undef */ import React from 'react'; -import { mount, render } from 'enzyme'; +import { mount } from 'enzyme'; import KeyCode from 'rc-util/lib/KeyCode'; import TreeSelect, { TreeNode } from '../src'; -import Selection from '../src/Selector/MultipleSelector/Selection'; -import { resetAriaId } from '../src/util'; import focusTest from './shared/focusTest'; describe('TreeSelect.multiple', () => { @@ -15,45 +13,30 @@ describe('TreeSelect.multiple', () => { { key: '1', value: '1', title: 'label1' }, ]; const createSelect = props => ; - const select = (wrapper, index = 0) => { - wrapper - .find('.rc-tree-select-tree-node-content-wrapper') - .at(index) - .simulate('click'); - }; - - beforeEach(() => { - resetAriaId(); - }); it('select multiple nodes', () => { const wrapper = mount(createSelect({ open: true })); - select(wrapper, 0); - select(wrapper, 1); - const result = wrapper.find('.rc-tree-select-selection__rendered'); - const choices = result.find('.rc-tree-select-selection__choice__content'); - expect(result.last().is('ul')).toBe(true); - expect(choices.at(0).prop('children')).toBe('label0'); - expect(choices.at(1).prop('children')).toBe('label1'); + wrapper.selectNode(0); + wrapper.selectNode(1); + expect(wrapper.getSelection(0).text()).toBe('label0'); + expect(wrapper.getSelection(1).text()).toBe('label1'); }); it('remove selected node', () => { const wrapper = mount(createSelect({ defaultValue: ['0', '1'] })); - wrapper - .find('.rc-tree-select-selection__choice__remove') - .first() - .simulate('click'); - const choice = wrapper.find('ul .rc-tree-select-selection__choice__content'); - expect(choice).toHaveLength(1); - expect(choice.prop('children')).toBe('label1'); + wrapper.clearSelection(); + expect(wrapper.getSelection()).toHaveLength(1); + expect(wrapper.getSelection(0).text()).toBe('label1'); }); it('remove by backspace key', () => { const wrapper = mount(createSelect({ defaultValue: ['0', '1'] })); - wrapper.find('input').simulate('keyDown', { keyCode: KeyCode.BACKSPACE }); - const choice = wrapper.find('ul .rc-tree-select-selection__choice__content'); - expect(choice).toHaveLength(1); - expect(choice.prop('children')).toBe('label0'); + wrapper + .find('input') + .first() + .simulate('keyDown', { which: KeyCode.BACKSPACE }); + expect(wrapper.getSelection()).toHaveLength(1); + expect(wrapper.getSelection(0).text()).toBe('label0'); }); // https://github.com/react-component/tree-select/issues/47 @@ -78,15 +61,17 @@ describe('TreeSelect.multiple', () => { } } const wrapper = mount(); - wrapper.find('input').simulate('keyDown', { keyCode: KeyCode.BACKSPACE }); wrapper - .find('.rc-tree-select-tree-checkbox') - .at(1) - .simulate('click'); - wrapper.find('input').simulate('keyDown', { keyCode: KeyCode.BACKSPACE }); - const choice = wrapper.find('ul .rc-tree-select-selection__choice__content'); - expect(choice).toHaveLength(1); - expect(choice.prop('children')).toBe('label0'); + .find('input') + .first() + .simulate('keyDown', { which: KeyCode.BACKSPACE }); + wrapper.selectNode(1); + wrapper + .find('input') + .first() + .simulate('keyDown', { which: KeyCode.BACKSPACE }); + expect(wrapper.getSelection()).toHaveLength(1); + expect(wrapper.getSelection(0).text()).toBe('label0'); }); // TODO: Check preVal, it's not correct @@ -107,15 +92,9 @@ describe('TreeSelect.multiple', () => { }), ); - const $remove = wrapper - .find('.rc-tree-select-selection__rendered') - .find('.rc-tree-select-selection__choice') - .find('.rc-tree-select-selection__choice__remove') - .at(1); - - $remove.simulate('click'); + wrapper.clearSelection(1); - expect(handleChange).toBeCalledWith( + expect(handleChange).toHaveBeenCalledWith( ['0'], ['label0'], expect.objectContaining({ @@ -129,94 +108,102 @@ describe('TreeSelect.multiple', () => { }); it('renders clear button', () => { - const wrapper = render(createSelect({ allowClear: true })); + const wrapper = mount(createSelect({ allowClear: true, value: ['0'] })); - expect(wrapper.find('.rc-tree-select-selection__clear')).toMatchSnapshot(); + expect(wrapper.find('.rc-tree-select-clear').length).toBeTruthy(); }); it('should focus and clear search input after select and unselect item', () => { const wrapper = mount(createSelect()); - wrapper.find('input').simulate('change', { target: { value: '0' } }); - expect(wrapper.find('input').getDOMNode().value).toBe('0'); - select(wrapper, 0); - expect(wrapper.find('input').getDOMNode().value).toBe(''); - wrapper.find('input').simulate('change', { target: { value: '0' } }); - expect(wrapper.find('input').getDOMNode().value).toBe('0'); - select(wrapper, 0); // unselect - expect(wrapper.find('input').getDOMNode().value).toBe(''); + + wrapper.search('0'); + wrapper.selectNode(0); + expect( + wrapper + .find('input') + .first() + .props().value, + ).toBe(''); + + wrapper.search('0'); + wrapper.selectNode(0); + expect( + wrapper + .find('input') + .first() + .props().value, + ).toBe(''); }); it('do not open tree when close button click', () => { const wrapper = mount(createSelect()); - wrapper.find('.rc-tree-select-selection').simulate('click'); - select(wrapper, 0); - select(wrapper, 1); - wrapper.setState({ open: false }); - wrapper - .find('.rc-tree-select-selection__choice__remove') - .at(0) - .simulate('click'); - expect(wrapper.state('open')).toBe(false); - expect(wrapper.state('valueList')).toEqual([{ label: 'label1', value: '1' }]); + wrapper.openSelect(); + wrapper.selectNode(0); + wrapper.selectNode(1); + wrapper.openSelect(); + wrapper.clearSelection(0); + expect(wrapper.isOpen()).toBeFalsy(); + expect(wrapper.getSelection()).toHaveLength(1); }); describe('maxTagCount', () => { it('legal', () => { - const wrapper = render( + const wrapper = mount( createSelect({ maxTagCount: 1, value: ['0', '1'], }), ); - expect(wrapper.find('.rc-tree-select-selection')).toMatchSnapshot(); + expect(wrapper.getSelection()).toHaveLength(2); + expect(wrapper.getSelection(1).text()).toBe('+ 1 ...'); }); it('illegal', () => { - const wrapper = render( + const wrapper = mount( createSelect({ maxTagCount: 1, value: ['0', 'not exist'], }), ); - expect(wrapper.find('.rc-tree-select-selection')).toMatchSnapshot(); + expect(wrapper.getSelection()).toHaveLength(2); + expect(wrapper.getSelection(1).text()).toBe('+ 1 ...'); }); it('zero', () => { - const wrapper = render( + const wrapper = mount( createSelect({ maxTagCount: 0, value: ['0', '1'], }), ); - expect(wrapper.find('.rc-tree-select-selection')).toMatchSnapshot(); + expect(wrapper.getSelection()).toHaveLength(1); + expect(wrapper.getSelection(0).text()).toBe('+ 2 ...'); }); describe('maxTagPlaceholder', () => { it('string', () => { - const wrapper = render( + const wrapper = mount( createSelect({ maxTagCount: 1, value: ['0', '1'], maxTagPlaceholder: 'bamboo', }), ); - - expect(wrapper.find('.rc-tree-select-selection')).toMatchSnapshot(); + expect(wrapper.getSelection(1).text()).toBe('bamboo'); }); it('function', () => { - const wrapper = render( + const wrapper = mount( createSelect({ maxTagCount: 1, value: ['0', '1'], maxTagPlaceholder: list => `${list.length} bamboo...`, }), ); - - expect(wrapper.find('.rc-tree-select-selection')).toMatchSnapshot(); + expect(wrapper.getSelection(1).text()).toBe('1 bamboo...'); }); }); }); @@ -245,17 +232,17 @@ describe('TreeSelect.multiple', () => { }), ); - select(wrapper, 0); - select(wrapper, 1); + wrapper.selectNode(0); + expect(onChange).toHaveBeenCalledWith([4, 0], expect.anything(), expect.anything()); + onChange.mockReset(); - expect(onChange.mock.calls[0][0]).toEqual([0, 4]); - expect(onChange.mock.calls[1][0]).toEqual([0, 2, 3, 4]); + wrapper.selectNode(1); + expect(onChange).toHaveBeenCalledWith([4, 0, 2, 3], expect.anything(), expect.anything()); }); // https://github.com/ant-design/ant-design/issues/12315 it('select searched node', () => { const onChange = jest.fn(); - const wrapper = mount( @@ -269,13 +256,9 @@ describe('TreeSelect.multiple', () => { , ); - wrapper.find('.rc-tree-select-search__field').simulate('change', { target: { value: 'sss' } }); - wrapper - .find('.rc-tree-select-tree-node-content-wrapper') - .at(2) - .simulate('click'); - - expect(onChange.mock.calls[0][0]).toEqual(['leaf1', 'sss']); + wrapper.search('sss'); + wrapper.selectNode(2); + expect(onChange).toHaveBeenCalledWith(['leaf1', 'sss'], expect.anything(), expect.anything()); }); it('do not crash when value has empty string', () => { @@ -285,12 +268,12 @@ describe('TreeSelect.multiple', () => { , ); - expect(wrapper.find(Selection).length).toEqual(1); + expect(wrapper.getSelection()).toHaveLength(1); }); it('can hide search box by showSearch = false', () => { - const wrapper = render(); + const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.render()).toMatchSnapshot(); }); }); diff --git a/tests/Select.props.spec.js b/tests/Select.props.spec.js index 24e83a39..99926493 100644 --- a/tests/Select.props.spec.js +++ b/tests/Select.props.spec.js @@ -1,12 +1,8 @@ /* eslint-disable no-undef, react/no-multi-comp, no-console */ import React from 'react'; -import { mount, render } from 'enzyme'; -import { renderToJson } from 'enzyme-to-json'; +import { mount } from 'enzyme'; import Tree, { TreeNode } from 'rc-tree'; -import Trigger from 'rc-trigger'; import TreeSelect, { SHOW_ALL, SHOW_CHILD, SHOW_PARENT, TreeNode as SelectNode } from '../src'; -import { resetAriaId } from '../src/util'; -import { setMock } from './__mocks__/rc-animate/lib/CSSMotionList'; // Promisify timeout to let jest catch works function timeoutPromise(delay = 0) { @@ -16,15 +12,6 @@ function timeoutPromise(delay = 0) { } describe('TreeSelect.props', () => { - beforeEach(() => { - jest.useFakeTimers(); - resetAriaId(); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - // Must wrap with `div` since enzyme will only return first child of fragment const createSelect = (props = {}) => (
    @@ -37,83 +24,17 @@ describe('TreeSelect.props', () => {
    ); - const createOpenSelect = (props = {}) => createSelect({ open: true, treeDefaultExpandAll: true, ...props }); - it('basic', () => { - const wrapper = mount(createSelect()); - expect(wrapper.render()).toMatchSnapshot(); - - wrapper.find('.rc-tree-select').simulate('click'); - expect(wrapper.render()).toMatchSnapshot(); - }); - it('className', () => { const wrapper = mount(createOpenSelect({ className: 'test-class' })); - expect(wrapper.render()).toMatchSnapshot(); + expect(wrapper.find('.rc-tree-select').hasClass('test-class')).toBeTruthy(); }); it('prefixCls', () => { const wrapper = mount(createOpenSelect({ prefixCls: 'another-cls' })); - expect(wrapper.render()).toMatchSnapshot(); - }); - - it.skip('animation', () => { - // setMock(true); - const wrapper = mount( - createSelect({ - animation: 'test-animation', - }), - ); - wrapper.find('.rc-tree-select').simulate('click'); - expect(wrapper.render()).toMatchSnapshot(); - // setMock(false); - }); - - it.skip('transitionName', () => { - // setMock(true); - const wrapper = mount( - createSelect({ - transitionName: 'test-transitionName', - }), - ); - wrapper.find('.rc-tree-select').simulate('click'); - expect(wrapper.render()).toMatchSnapshot(); - // setMock(false); - }); - - it.only('choiceTransitionName', () => { - setMock(true); - class Wrapper extends React.Component { - state = { - value: [], - }; - - doValueUpdate = () => { - this.setState({ - value: ['Value 0'], - }); - }; - - render() { - const { value } = this.state; - return ( -
    - - - -
    - ); - } - } - - const wrapper = mount(); - expect(wrapper.render()).toMatchSnapshot(); - - wrapper.instance().doValueUpdate(); - expect(wrapper.render()).toMatchSnapshot(); - setMock(false); + expect(wrapper.find('.another-cls').length).toBeTruthy(); }); describe('filterTreeNode', () => { @@ -122,29 +43,23 @@ describe('TreeSelect.props', () => { return String(child.props.title).indexOf(input) !== -1; } const wrapper = mount(createOpenSelect({ filterTreeNode })); - wrapper.find('input').simulate('change', { target: { value: 'Title 1' } }); - expect(wrapper.render()).toMatchSnapshot(); + wrapper.search('Title 1'); + expect(wrapper.find('List').props().data).toHaveLength(1); - wrapper.find('input').simulate('change', { target: { value: '0-0' } }); - expect(wrapper.render()).toMatchSnapshot(); + wrapper.search('0-0'); + expect(wrapper.find('List').props().data).toHaveLength(2); }); it('false', () => { const wrapper = mount(createOpenSelect({ filterTreeNode: false })); - wrapper.find('input').simulate('change', { target: { value: 'Title 1' } }); - expect(wrapper.render()).toMatchSnapshot(); + wrapper.search('Title 1'); + expect(wrapper.find('List').props().data).toHaveLength(4); }); }); - it('showSearch', () => { - const wrapper = mount(createOpenSelect({ showSearch: false })); - expect(wrapper.render()).toMatchSnapshot(); - }); - describe('allowClear', () => { it('functional works', () => { const handleChange = jest.fn(); - const wrapper = mount( createSelect({ allowClear: true, @@ -153,29 +68,32 @@ describe('TreeSelect.props', () => { open: true, }), ); - wrapper.find('.rc-tree-select').simulate('click'); - expect(wrapper.render()).toMatchSnapshot(); + + wrapper.openSelect(); // Click node 0-1 - const $node = wrapper.find(TreeNode).at(2); - $node.find('.rc-tree-select-tree-node-content-wrapper').simulate('click'); - - expect(wrapper.render()).toMatchSnapshot(); - expect(handleChange).toBeCalledWith('Value 0-1', ['Title 0-1'], { - preValue: [], - selected: true, - triggerValue: 'Value 0-1', - triggerNode: $node.instance(), - }); + wrapper.selectNode(2); + expect(handleChange).toHaveBeenCalledWith( + 'Value 0-1', + ['Title 0-1'], + expect.objectContaining({ + preValue: [], + selected: true, + triggerValue: 'Value 0-1', + triggerNode: expect.anything(), + }), + ); handleChange.mockReset(); // Click to clear - wrapper.find('.rc-tree-select-selection__clear').simulate('click'); - - expect(wrapper.render()).toMatchSnapshot(); - expect(handleChange).toBeCalledWith(undefined, [], { - preValue: [{ label: 'Title 0-1', value: 'Value 0-1' }], - }); + wrapper.clearAll(); + expect(handleChange).toHaveBeenCalledWith( + undefined, + [], + expect.objectContaining({ + preValue: [{ label: 'Title 0-1', value: 'Value 0-1' }], + }), + ); }); it('value not in tree should also display allow clear', () => { @@ -185,26 +103,17 @@ describe('TreeSelect.props', () => { value: 'not-exist-in-tree', }), ); - expect(wrapper.find('.rc-tree-select-selection__clear').length).toBeTruthy(); + expect(wrapper.find('.rc-tree-select-clear').length).toBeTruthy(); }); }); it('placeholder', () => { - const wrapper = render( + const wrapper = mount( createSelect({ placeholder: 'RC Component', }), ); - expect(renderToJson(wrapper)).toMatchSnapshot(); - }); - - it('searchPlaceholder', () => { - const wrapper = mount( - createOpenSelect({ - searchPlaceholder: 'RC Component', - }), - ); - expect(wrapper.render()).toMatchSnapshot(); + expect(wrapper.find('.rc-tree-select-selection-placeholder').text()).toBe('RC Component'); }); // https://github.com/ant-design/ant-design/issues/11746 @@ -216,14 +125,11 @@ describe('TreeSelect.props', () => { ); const wrapper = mount(); - - expect(wrapper.render()).toMatchSnapshot(); - + expect(wrapper.find('List').props().data).toHaveLength(1); wrapper.setProps({ treeData: [{ title: 'bbb', value: '222' }], }); - - expect(wrapper.render()).toMatchSnapshot(); + expect(wrapper.find('List').length).toBeFalsy(); }); describe('labelInValue', () => { @@ -235,17 +141,18 @@ describe('TreeSelect.props', () => { onChange: handleChange, }), ); - // Click node 0-1 - const $node = wrapper.find(TreeNode).at(2); - $node.find('.rc-tree-select-tree-node-content-wrapper').simulate('click'); - - expect(handleChange).toBeCalledWith({ label: 'Title 0-1', value: 'Value 0-1' }, null, { - preValue: [], - selected: true, - triggerValue: 'Value 0-1', - triggerNode: $node.instance(), - }); + wrapper.selectNode(2); + expect(handleChange).toHaveBeenCalledWith( + { label: 'Title 0-1', value: 'Value 0-1' }, + null, + expect.objectContaining({ + preValue: [], + selected: true, + triggerValue: 'Value 0-1', + triggerNode: expect.anything(), + }), + ); }); it('set illegal value', () => { @@ -255,11 +162,7 @@ describe('TreeSelect.props', () => { value: [null], }), ); - - expect(wrapper.find(TreeSelect).instance().state.valueList).toEqual([]); - expect(wrapper.find(TreeSelect).instance().state.missValueList).toEqual([ - { label: '', value: '' }, - ]); + expect(wrapper.getSelection(0).text()).toBe(''); }); }); @@ -271,10 +174,9 @@ describe('TreeSelect.props', () => { onClick: handleClick, }), ); - - // `onClick` depends on origin event trigger. Need't test args + // `onClick` depends on origin event trigger. Needn't test args wrapper.find('.rc-tree-select').simulate('click'); - expect(handleClick).toBeCalled(); + expect(handleClick).toHaveBeenCalled(); }); // onChange - is already test above @@ -286,19 +188,16 @@ describe('TreeSelect.props', () => { onSelect: handleSelect, }), ); - - const $paren = wrapper.find(TreeNode).at(0); - const $node = wrapper.find(TreeNode).at(2); - $node.find('.rc-tree-select-tree-node-content-wrapper').simulate('click'); + wrapper.selectNode(2); // TreeNode use cloneElement so that the node is not the same - expect(handleSelect).toBeCalledWith('Value 0-1', $node.instance(), { - event: 'select', - node: $node.instance(), - selected: true, - selectedNodes: [$paren.props().children[1]], - nativeEvent: expect.objectContaining({}), // Native event object - }); + expect(handleSelect.mock.calls[0][0]).toEqual('Value 0-1'); + expect(handleSelect.mock.calls[0][1].props).toEqual( + expect.objectContaining({ + value: 'Value 0-1', + title: 'Title 0-1', + }), + ); }); // TODO: `onDeselect` is copy from `Select` component and not implement complete. @@ -311,32 +210,13 @@ describe('TreeSelect.props', () => { onSearch: handleSearch, }), ); - - wrapper.find('input').simulate('change', { target: { value: 'Search changed' } }); - expect(handleSearch).toBeCalledWith('Search changed'); + wrapper.search('Search changed'); + expect(handleSearch).toHaveBeenCalledWith('Search changed'); }); it('showArrow', () => { const wrapper = mount(createOpenSelect({ showArrow: false })); - expect(wrapper.render()).toMatchSnapshot(); - }); - - describe('dropdownMatchSelectWidth', () => { - it('default', () => { - const wrapper = mount(createOpenSelect()); - expect(wrapper.render()).toMatchSnapshot(); - }); - - [true, false].forEach(dropdownMatchSelectWidth => { - it(String(dropdownMatchSelectWidth), () => { - const wrapper = mount( - createOpenSelect({ - dropdownMatchSelectWidth, - }), - ); - expect(wrapper.render()).toMatchSnapshot(); - }); - }); + expect(wrapper.find('.rc-tree-select-arrow').length).toBeFalsy(); }); it('dropdownClassName', () => { @@ -345,85 +225,35 @@ describe('TreeSelect.props', () => { dropdownClassName: 'test-dropdownClassName', }), ); - expect(wrapper.render()).toMatchSnapshot(); + expect(wrapper.find('.test-dropdownClassName').length).toBeTruthy(); }); it('dropdownStyle', () => { - const wrapper = mount( - createOpenSelect({ - dropdownStyle: { - background: 'red', - }, - }), - ); - expect(wrapper.render()).toMatchSnapshot(); - }); - - it('dropdownPopupAlign', () => { - const dropdownPopupAlign = { - forPassPropTest: true, + const style = { + background: 'red', }; - - const wrapper = mount(createOpenSelect({ dropdownPopupAlign })); - - expect(wrapper.find(Trigger).props().popupAlign).toBe(dropdownPopupAlign); - }); - - it('onDropdownVisibleChange', () => { - let canProcess = true; - - const handleDropdownVisibleChange = jest.fn(); const wrapper = mount( - createSelect({ - onDropdownVisibleChange: (...args) => { - handleDropdownVisibleChange(...args); - return canProcess; - }, + createOpenSelect({ + dropdownClassName: 'test-dropdownClassName', + dropdownStyle: style, }), ); - - const $select = wrapper.find('.rc-tree-select'); - - // Simulate when can process - $select.simulate('click'); - expect(handleDropdownVisibleChange).toBeCalledWith(true, { documentClickClose: false }); - handleDropdownVisibleChange.mockReset(); - - // https://github.com/ant-design/ant-design/issues/9857 - // Both use blur to hide. click not affect this. - $select.simulate('click'); - expect(handleDropdownVisibleChange).toBeCalledWith(false, { documentClickClose: true }); - handleDropdownVisibleChange.mockReset(); - - $select.simulate('blur'); - jest.runAllTimers(); - expect(handleDropdownVisibleChange).not.toBeCalled(); - handleDropdownVisibleChange.mockReset(); - - // Simulate when can't process - canProcess = false; - - $select.simulate('click'); - expect(handleDropdownVisibleChange).toBeCalledWith(true, { documentClickClose: false }); - handleDropdownVisibleChange.mockReset(); - - $select.simulate('click'); - expect(handleDropdownVisibleChange).toBeCalledWith(true, { documentClickClose: false }); - handleDropdownVisibleChange.mockReset(); - - $select.simulate('blur'); - jest.runAllTimers(); - expect(handleDropdownVisibleChange).not.toBeCalled(); + expect( + wrapper + .find('.test-dropdownClassName') + .first() + .props().style, + ).toEqual(expect.objectContaining(style)); }); it('notFoundContent', () => { const wrapper = mount( createOpenSelect({ - notFoundContent: 'Noting Matched!', + notFoundContent:
    Noting Matched!
    , treeData: [], }), ); - expect(wrapper.render()).toMatchSnapshot(); + expect(wrapper.find('.not-match').text()).toEqual('Noting Matched!'); }); describe('showCheckedStrategy', () => { @@ -432,83 +262,60 @@ describe('TreeSelect.props', () => { strategy: SHOW_ALL, arg1: ['Value 0', 'Value 0-0', 'Value 0-1'], arg2: ['Title 0', 'Title 0-0', 'Title 0-1'], - arg3: ($node, $oriNode) => { - const children = $node.props().children; - - return { - allCheckedNodes: [ - { - node: $oriNode, - pos: '0-0', - children: [ - { node: children[0], pos: '0-0-0' }, - { node: children[1], pos: '0-0-1' }, - ], - }, - ], - checked: true, - preValue: [], - triggerNode: $node.instance(), - triggerValue: 'Value 0', - }; + arg3: { + allCheckedNodes: [ + expect.objectContaining({ + node: expect.anything(), + pos: '0-0', + children: expect.anything(), + }), + ], + checked: true, + preValue: [], + triggerNode: expect.anything(), + triggerValue: 'Value 0', }, }, { strategy: SHOW_CHILD, arg1: ['Value 0-0', 'Value 0-1'], arg2: ['Title 0-0', 'Title 0-1'], - arg3: ($node, $oriNode) => { - const children = $node.props().children; - - return { - allCheckedNodes: [ - { - node: $oriNode, - pos: '0-0', - children: [ - { node: children[0], pos: '0-0-0' }, - { node: children[1], pos: '0-0-1' }, - ], - }, - ], - checked: true, - preValue: [], - triggerNode: $node.instance(), - triggerValue: 'Value 0', - }; + arg3: { + allCheckedNodes: [ + expect.objectContaining({ + node: expect.anything(), + pos: '0-0', + children: expect.anything(), + }), + ], + checked: true, + preValue: [], + triggerNode: expect.anything(), + triggerValue: 'Value 0', }, }, { strategy: SHOW_PARENT, arg1: ['Value 0'], arg2: ['Title 0'], - arg3: ($node, $oriNode) => { - const children = $node.props().children; - - return { - allCheckedNodes: [ - { - node: $oriNode, - pos: '0-0', - children: [ - { node: children[0], pos: '0-0-0' }, - { node: children[1], pos: '0-0-1' }, - ], - }, - ], - checked: true, - preValue: [], - triggerNode: $node.instance(), - triggerValue: 'Value 0', - }; + arg3: { + allCheckedNodes: [ + expect.objectContaining({ + node: expect.anything(), + pos: '0-0', + children: expect.anything(), + }), + ], + checked: true, + preValue: [], + triggerNode: expect.anything(), + triggerValue: 'Value 0', }, }, ]; - testList.forEach(({ strategy, arg1, arg2, arg3 }) => { it(strategy, () => { const handleChange = jest.fn(); - const wrapper = mount( createOpenSelect({ treeCheckable: true, @@ -516,45 +323,20 @@ describe('TreeSelect.props', () => { onChange: handleChange, }), ); - // TreeSelect will convert SelectNode to TreeNode. // Transitional node should get before click event // Since after click will render new TreeNode // [Legacy] FIXME: This is so hard to test - const $tree = wrapper.find(Tree); - const $oriNode = $tree.props().children[0]; - - const $node = wrapper.find(TreeNode).at(0); - $node - .find('.rc-tree-select-tree-checkbox') - .first() - .simulate('click'); - - expect(handleChange).toBeCalledWith(arg1, arg2, arg3($node, $oriNode)); + wrapper.selectNode(0); + expect(handleChange).toHaveBeenCalledWith(arg1, arg2, expect.anything()); + const { triggerNode, allCheckedNodes, ...rest } = handleChange.mock.calls[0][2]; + expect({ ...rest, triggerNode, allCheckedNodes }).toEqual(arg3); }); }); }); // treeCheckStrictly - already tested in Select.checkable.spec.js - it('treeIcon', () => { - const wrapper = mount( - createOpenSelect({ - treeIcon: true, - }), - ); - expect(wrapper.render()).toMatchSnapshot(); - }); - - it('treeLine', () => { - const wrapper = mount( - createOpenSelect({ - treeLine: true, - }), - ); - expect(wrapper.render()).toMatchSnapshot(); - }); - // treeDataSimpleMode - already tested in Select.spec.js it('treeDefaultExpandAll', () => { @@ -563,14 +345,14 @@ describe('TreeSelect.props', () => { treeDefaultExpandAll: true, }), ); - expect(expandWrapper.render()).toMatchSnapshot(); + expect(expandWrapper.find('List').props().data).toHaveLength(4); - const unexpandWrapper = mount( + const unExpandWrapper = mount( createOpenSelect({ treeDefaultExpandAll: false, }), ); - expect(unexpandWrapper.render()).toMatchSnapshot(); + expect(unExpandWrapper.find('List').props().data).toHaveLength(2); }); // treeCheckable - already tested in Select.checkable.spec.js @@ -586,7 +368,9 @@ describe('TreeSelect.props', () => { value: ['Value 0-0', 'Value 1', 'Value 0-1'], }), ); - expect(wrapper.render()).toMatchSnapshot(); + for (let i = 0; i < 3; i += 1) { + expect(wrapper.getSelection(0).text()).toBe('Ti...'); + } }); // disabled - already tested in Select.spec.js @@ -598,7 +382,7 @@ describe('TreeSelect.props', () => { defaultValue: 'Value 0-0', }), ); - expect(wrapper.render()).toMatchSnapshot(); + expect(wrapper.getSelection(0).text()).toBe('Title 0-0'); }); // labelInValue - already tested in Select.spec.js @@ -608,12 +392,8 @@ describe('TreeSelect.props', () => { // treeData - already tested in Select.spec.js it('loadData', () => { - jest.useRealTimers(); - let called = 0; - const handleLoadData = jest.fn(); - class Demo extends React.Component { state = { loaded: false, @@ -622,9 +402,7 @@ describe('TreeSelect.props', () => { loadData = (...args) => { called += 1; handleLoadData(...args); - this.setState({ loaded: true }); - return Promise.resolve(); }; @@ -639,33 +417,47 @@ describe('TreeSelect.props', () => { ); } } - const wrapper = mount(); + expect(handleLoadData).not.toHaveBeenCalled(); - expect(handleLoadData).not.toBeCalled(); - - const switcher = wrapper.find('.rc-tree-select-tree-switcher'); - const node = wrapper.find(TreeNode).instance(); - switcher.simulate('click'); + wrapper.find('.rc-tree-select-tree-switcher').simulate('click'); return timeoutPromise().then(() => { - expect(handleLoadData).toBeCalledWith(node); + expect(handleLoadData).toHaveBeenCalledWith(expect.objectContaining({ value: '0-0' })); expect(called).toBe(1); - expect(wrapper.render()).toMatchSnapshot(); + expect(wrapper.find('List').props().data).toHaveLength(2); }); }); + it('treeLoadedKeys', () => { + const loadData = jest.fn(() => Promise.resolve()); + mount( + , + ); + + expect(loadData).toHaveBeenCalledTimes(1); + expect(loadData).toHaveBeenCalledWith(expect.objectContaining({ value: '0-1' })); + }); + it('getPopupContainer', () => { const getPopupContainer = trigger => trigger.parentNode; - const wrapper = mount(createOpenSelect({ getPopupContainer })); - - expect(wrapper.find(Trigger).props().getPopupContainer).toBe(getPopupContainer); + expect( + wrapper + .find('Trigger') + .first() + .props().getPopupContainer, + ).toBe(getPopupContainer); }); it('set value not in the Tree', () => { const onChange = jest.fn(); - const wrapper = mount(
    @@ -673,37 +465,23 @@ describe('TreeSelect.props', () => {
    , ); - wrapper.find('.rc-tree-select-tree-checkbox').simulate('click'); - const valueList = onChange.mock.calls[0][0]; expect(valueList).toEqual(['not exist', 'exist']); }); - it('warning if use label', () => { - const spy = jest.spyOn(global.console, 'error'); - console.log(">>> Follow Warning is for test purpose. Don't be scared :)"); - render(); - expect(spy).toHaveBeenCalledWith( - "Warning: 'label' in treeData is deprecated. Please use 'title' instead.", - ); - spy.mockRestore(); - }); - describe('onDeselect trigger', () => { const nodeProps1 = { title: 'bamboo', value: 'smart', customize: 'beautiful', }; - const nodeProps2 = { title: 'day', value: 'light', customize: 'well', }; const propList = [nodeProps1, nodeProps2]; - const createDeselectWrapper = props => mount( @@ -711,7 +489,6 @@ describe('TreeSelect.props', () => { , ); - const nodeMatcher = index => expect.objectContaining({ props: expect.objectContaining(propList[index]), @@ -722,20 +499,9 @@ describe('TreeSelect.props', () => { const onSelect = jest.fn(); const onDeselect = jest.fn(); const wrapper = createDeselectWrapper({ onSelect, onDeselect, defaultValue: 'smart' }); - wrapper - .find('.rc-tree-select-tree-node-content-wrapper') - .at(0) - .simulate('click'); - expect(onSelect).not.toBeCalled(); - expect(onDeselect.mock.calls[0][0]).toEqual('smart'); - expect(onDeselect.mock.calls[0][1]).toEqual(nodeMatcher(0)); - expect(onDeselect.mock.calls[0][2]).toEqual({ - event: 'select', - selected: false, - nativeEvent: expect.objectContaining({}), // Not check native event - node: nodeMatcher(0), - selectedNodes: [], - }); + wrapper.selectNode(0); + expect(onDeselect).not.toHaveBeenCalled(); + expect(onSelect).toHaveBeenCalledWith('smart', nodeMatcher(0)); }); }); @@ -766,18 +532,10 @@ describe('TreeSelect.props', () => { defaultValue: ['smart', 'light'], ...props, }); - wrapper - .find('.rc-tree-select-tree-node-content-wrapper') - .at(0) - .simulate('click'); - expect(onSelect).not.toBeCalled(); - expect(onDeselect.mock.calls[0][0]).toEqual('smart'); - expect(onDeselect.mock.calls[0][1]).toEqual(nodeMatcher(0)); - expect(onDeselect.mock.calls[0][2]).toEqual({ - nativeEvent: expect.objectContaining({}), // Not check native event - node: nodeMatcher(0), - ...match, - }); + + wrapper.selectNode(0); + expect(onSelect).not.toHaveBeenCalled(); + expect(onDeselect).toHaveBeenCalledWith('smart', nodeMatcher(0)); }); it('click on selector', () => { @@ -789,26 +547,10 @@ describe('TreeSelect.props', () => { defaultValue: ['smart', 'light'], ...props, }); - wrapper - .find('.rc-tree-select-selection__choice__remove') - .at(0) - .simulate('click'); - expect(onSelect).not.toBeCalled(); - expect(onDeselect.mock.calls[0][0]).toEqual('smart'); - expect(onDeselect.mock.calls[0][1]).toEqual(nodeMatcher(0)); - - const tgtArg3 = { - nativeEvent: expect.objectContaining({}), // Not check native event - node: nodeMatcher(0), - ...match, - }; - - Object.keys(tgtArg3).forEach(key => { - if (selectorSkip.includes(key)) return; - - const tgtVal = tgtArg3[key]; - expect(onDeselect.mock.calls[0][2][key]).toEqual(tgtVal); - }); + + wrapper.clearSelection(0); + expect(onSelect).not.toHaveBeenCalled(); + expect(onDeselect).toHaveBeenCalledWith('smart', nodeMatcher(0)); }); }); }); diff --git a/tests/Select.spec.js b/tests/Select.spec.js index f3c50c5f..a22f7deb 100644 --- a/tests/Select.spec.js +++ b/tests/Select.spec.js @@ -1,15 +1,12 @@ /* eslint-disable no-undef react/no-multi-comp */ import React from 'react'; -import { render, mount } from 'enzyme'; +import { mount } from 'enzyme'; import KeyCode from 'rc-util/lib/KeyCode'; -import raf from 'raf'; import TreeSelect, { TreeNode } from '../src'; -import { resetAriaId, getLabel } from '../src/util'; import focusTest from './shared/focusTest'; describe('TreeSelect.basic', () => { beforeEach(() => { - resetAriaId(); jest.useFakeTimers(); }); @@ -24,7 +21,7 @@ describe('TreeSelect.basic', () => { focusTest('single'); describe('render', () => { - let treeData = [ + const treeData = [ { key: '0', value: '0', title: '0 label' }, { key: '1', @@ -38,7 +35,7 @@ describe('TreeSelect.basic', () => { ]; it('renders correctly', () => { - const wrapper = render( + const wrapper = mount( { treeData={treeData} />, ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.render()).toMatchSnapshot(); }); it('renders tree correctly', () => { - const wrapper = render( + const wrapper = mount( { treeData={treeData} />, ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.render()).toMatchSnapshot(); }); it('not crash if no children', () => { - render(); + mount(); }); it('renders disabled correctly', () => { - const wrapper = render(); - expect(wrapper).toMatchSnapshot(); + const wrapper = mount(); + expect(wrapper.render()).toMatchSnapshot(); }); it('renders TreeNode correctly', () => { - const wrapper = render( - + const wrapper = mount( + @@ -85,12 +82,12 @@ describe('TreeSelect.basic', () => { , ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.render()).toMatchSnapshot(); }); it('renders TreeNode correctly with falsy child', () => { - const wrapper = render( - + const wrapper = mount( + @@ -99,29 +96,32 @@ describe('TreeSelect.basic', () => { , ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.render()).toMatchSnapshot(); }); it('renders treeDataSimpleMode correctly', () => { - treeData = [ - { id: '0', value: '0', title: 'label0' }, - { id: '1', value: '1', title: 'label1', pId: '0' }, - ]; - const wrapper = render( + const wrapper = mount(
    - +
    , ); - expect(wrapper).toMatchSnapshot(); + expect(wrapper.render()).toMatchSnapshot(); }); }); it('sets default value', () => { - const treeData = [{ key: '0', value: '0', title: 'label0' }]; - const wrapper = mount(); - expect(wrapper.find('.rc-tree-select-selection__rendered > span').props().children).toBe( - 'label0', + const wrapper = mount( + , ); + expect(wrapper.getSelection(0).text()).toEqual('label0'); }); it('select value twice', () => { @@ -133,17 +133,14 @@ describe('TreeSelect.basic', () => { , ); wrapper.openSelect(); - wrapper - .find('.rc-tree-select-tree-title') - .at(0) - .simulate('click'); + wrapper.selectNode(); expect(onChange.mock.calls[0][0]).toEqual('0'); + expect(onChange).toHaveBeenCalledWith('0', expect.anything(), expect.anything()); + + onChange.mockReset(); wrapper.openSelect(); - wrapper - .find('.rc-tree-select-tree-title') - .at(1) - .simulate('click'); - expect(onChange.mock.calls[1][0]).toEqual('1'); + wrapper.selectNode(1); + expect(onChange).toHaveBeenCalledWith('1', expect.anything(), expect.anything()); }); it('can be controlled by value', () => { @@ -152,11 +149,10 @@ describe('TreeSelect.basic', () => { { key: '1', value: '1', title: 'label1' }, ]; const wrapper = mount(); - let choice = wrapper.find('.rc-tree-select-selection__rendered > span'); - expect(choice.prop('children')).toBe('label0'); + expect(wrapper.getSelection(0).text()).toEqual('label0'); + wrapper.setProps({ value: '1' }); - choice = wrapper.find('.rc-tree-select-selection__rendered > span'); - expect(choice.prop('children')).toBe('label1'); + expect(wrapper.getSelection(0).text()).toEqual('label1'); }); describe('select', () => { @@ -170,31 +166,32 @@ describe('TreeSelect.basic', () => { const onChange = jest.fn(); const onSelect = jest.fn(); const wrapper = mount(createSelect({ onChange, onSelect })); - wrapper.selectNode(0); - const selectedNode = wrapper - .find('TreeNode') - .first() - .instance(); - - expect(onChange).toBeCalledWith('0', ['label0'], { - preValue: [], - selected: true, - triggerNode: selectedNode, - triggerValue: '0', - }); + wrapper.selectNode(); + expect(onChange).toHaveBeenCalledWith( + '0', + ['label0'], + expect.objectContaining({ + preValue: [], + selected: true, + triggerValue: '0', + triggerNode: expect.anything(), + }), + ); - const args = onSelect.mock.calls[0]; - expect(args[1]).toBe(selectedNode); - expect(args[2]).toMatchObject({ - event: 'select', - selected: true, - }); + expect(onSelect).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + value: '0', + key: '0', + title: 'label0', + }), + ); }); it('render result by treeNodeLabelProp', () => { const wrapper = mount(createSelect({ treeNodeLabelProp: 'value' })); - wrapper.selectNode(0); - expect(wrapper.find('.rc-tree-select-selection__rendered > span').prop('children')).toBe('0'); + wrapper.selectNode(); + expect(wrapper.getSelection(0).text()).toEqual('0'); }); }); @@ -206,15 +203,15 @@ describe('TreeSelect.basic', () => { const createSelect = props => ; it('renders search input', () => { - const wrapper = render(createSelect()); - expect(wrapper).toMatchSnapshot(); + const wrapper = mount(createSelect()); + expect(wrapper.render()).toMatchSnapshot(); }); it('fires search event', () => { const onSearch = jest.fn(); const wrapper = mount(createSelect({ onSearch })); - wrapper.find('input').simulate('change', { target: { value: 'a' } }); - expect(onSearch).toBeCalledWith('a'); + wrapper.search('a'); + expect(onSearch).toHaveBeenCalledWith('a'); }); it('check tree changed by filter', () => { @@ -228,14 +225,14 @@ describe('TreeSelect.basic', () => { it('search nodes by filterTreeNode', () => { const filter = (value, node) => node.props.value.toLowerCase() === value.toLowerCase(); const wrapper = mount(createSelect({ filterTreeNode: filter })); - wrapper.find('input').simulate('change', { target: { value: 'A' } }); + wrapper.search('A'); expect(wrapper.find('TreeNode')).toHaveLength(1); expect(wrapper.find('TreeNode').prop('value')).toBe('a'); }); it('search nodes by treeNodeFilterProp', () => { const wrapper = mount(createSelect({ treeNodeFilterProp: 'title' })); - wrapper.find('input').simulate('change', { target: { value: 'labela' } }); + wrapper.search('labela'); expect(wrapper.find('TreeNode')).toHaveLength(1); expect(wrapper.find('TreeNode').prop('value')).toBe('a'); }); @@ -262,7 +259,7 @@ describe('TreeSelect.basic', () => { , ); wrapper.openSelect(); - expect(wrapper.state('open')).toBe(true); + expect(wrapper.isOpen()).toBeTruthy(); }); it('close tree when press ESC', () => { @@ -271,17 +268,21 @@ describe('TreeSelect.basic', () => { , ); - wrapper.setState({ open: true }); - wrapper.find('.rc-tree-select-search__field').simulate('keyDown', { keyCode: KeyCode.ESC }); - expect(wrapper.state('open')).toBe(false); + wrapper.openSelect(); + wrapper + .find('input') + .first() + .simulate('keyDown', { which: KeyCode.ESC }); + expect(wrapper.isOpen()).toBeFalsy(); }); // https://github.com/ant-design/ant-design/issues/4084 it('checks node correctly after treeData updated', () => { - const wrapper = mount(); + const onChange = jest.fn(); + const wrapper = mount(); wrapper.setProps({ treeData: [{ key: '0', value: '0', title: 'label0' }] }); wrapper.find('.rc-tree-select-tree-checkbox').simulate('click'); - expect(wrapper.state().valueList).toEqual([{ value: '0', label: 'label0' }]); + expect(onChange).toHaveBeenCalledWith(['0'], expect.anything(), expect.anything()); }); it('expands tree nodes by treeDefaultExpandedKeys', () => { @@ -295,8 +296,12 @@ describe('TreeSelect.basic', () => { , ); - const node = wrapper.find('.rc-tree-select-tree-node-content-wrapper').at(1); - expect(node.hasClass('rc-tree-select-tree-node-content-wrapper-open')).toBe(true); + expect( + wrapper + .find('.rc-tree-select-tree-treenode') + .at(1) + .hasClass('rc-tree-select-tree-treenode-switcher-open'), + ).toBeTruthy(); }); describe('allowClear', () => { @@ -308,9 +313,9 @@ describe('TreeSelect.basic', () => { ); wrapper.openSelect(); - wrapper.find('.rc-tree-select-tree-title').simulate('click'); - wrapper.find('.rc-tree-select-selection__clear').simulate('click'); - expect(wrapper.state().valueList).toEqual([]); + wrapper.selectNode(); + wrapper.clearAll(); + expect(wrapper.find('Select').props().value).toHaveLength(0); }); it('has inputValue prop', () => { @@ -336,21 +341,12 @@ describe('TreeSelect.basic', () => { } const wrapper = mount(); wrapper.openSelect(); - wrapper.selectNode(0); - wrapper.find('.rc-tree-select-selection__clear').simulate('click'); - expect(wrapper.find(TreeSelect).instance().state.valueList).toEqual([]); + wrapper.selectNode(); + wrapper.clearAll(); + expect(wrapper.find('Select').props().value).toHaveLength(0); }); }); - it('check title when label is a object', () => { - const wrapper = render( - - Do not show} value="0" key="0" /> - , - ); - expect(wrapper).toMatchSnapshot(); - }); - describe('keyCode', () => { [KeyCode.ENTER, KeyCode.DOWN].forEach(code => { it('open', () => { @@ -362,52 +358,80 @@ describe('TreeSelect.basic', () => { , ); - wrapper.find('.rc-tree-select').simulate('keyDown', { keyCode: code }); - expect(wrapper.state('open')).toBe(true); + wrapper + .find('input') + .first() + .simulate('keyDown', { which: code }); + expect(wrapper.isOpen()).toBeTruthy(); }); }); }); - describe('util', () => { - it('getLabel never reach', () => { - expect(getLabel({ value: 'newValue' })).toBe('newValue'); - }); - }); - - describe('forceAlign', () => { - it('onChoiceAnimationLeave trigger', done => { + describe('scroll to view', () => { + it('single mode should trigger scroll', () => { const wrapper = mount( - + , ); - const instance = wrapper.instance(); - instance.forcePopupAlign = jest.fn(); + wrapper.openSelect(); + wrapper.openSelect(); + expect(wrapper.isOpen()).toBeFalsy(); - instance.onChoiceAnimationLeave(); + const scrollTo = jest.fn(); + wrapper.find('List').instance().scrollTo = scrollTo; - raf(() => { - expect(instance.forcePopupAlign).toBeCalled(); - done(); - }); + wrapper.openSelect(); + expect(scrollTo).toHaveBeenCalled(); }); }); - describe('scroll to view', () => { - it('single mode should trigger scroll', done => { + describe('accessibility', () => { + it('key operation', () => { + const onChange = jest.fn(); const wrapper = mount( - - - , + , ); + function keyDown(code) { + wrapper + .find('input') + .first() + .simulate('keyDown', { which: code }); + wrapper.update(); + } + + function matchValue(value) { + expect(onChange).toHaveBeenCalledWith(value, expect.anything(), expect.anything()); + onChange.mockReset(); + } + wrapper.openSelect(); - raf(() => { - done(); - }); - jest.runAllTimers(); + keyDown(KeyCode.DOWN); + keyDown(KeyCode.ENTER); + matchValue(['parent']); + + keyDown(KeyCode.UP); + keyDown(KeyCode.ENTER); + matchValue(['parent', 'child']); }); }); + + it('click in list should preventDefault', () => { + const wrapper = mount(); + + const preventDefault = jest.fn(); + wrapper.find('.rc-tree-select-tree-node-content-wrapper').simulate('mouseDown', { + preventDefault, + }); + + expect(preventDefault).toHaveBeenCalled(); + }); }); diff --git a/tests/Select.tree.spec.js b/tests/Select.tree.spec.js index 44cdab3d..0c159074 100644 --- a/tests/Select.tree.spec.js +++ b/tests/Select.tree.spec.js @@ -1,22 +1,10 @@ /* eslint-disable no-undef, react/no-multi-comp, no-console */ import React from 'react'; -import { mount, render } from 'enzyme'; +import { mount } from 'enzyme'; +import { resetWarned } from 'rc-util/lib/warning'; import TreeSelect, { TreeNode as SelectNode } from '../src'; -import { resetAriaId } from '../src/util'; -// import { setMock } from './__mocks__/rc-animate'; describe('TreeSelect.tree', () => { - beforeEach(() => { - jest.useFakeTimers(); - resetAriaId(); - // setMock(true); - }); - - afterEach(() => { - jest.useRealTimers(); - // setMock(false); - }); - const createSelect = props => ( @@ -69,33 +57,35 @@ describe('TreeSelect.tree', () => { const wrapper = mount(); - wrapper.find('.rc-tree-select-tree-switcher').simulate('click'); - expect(wrapper.state().treeExpandedKeys).toEqual(['0-0']); + wrapper.switchNode(); + expect(wrapper.find('Tree').props().expandedKeys).toEqual(['0-0']); - wrapper - .find('.rc-tree-select-tree-switcher') - .at(2) - .simulate('click'); - expect(wrapper.state().treeExpandedKeys).toEqual(['0-0', '0-0-1']); + wrapper.switchNode(2); + expect(wrapper.find('Tree').props().expandedKeys).toEqual(['0-0', '0-0-1']); wrapper.find('button.reset').simulate('click'); - expect(wrapper.find('Tree').instance().state.expandedKeys).toEqual([]); + expect(wrapper.find('Tree').props().expandedKeys).toEqual([]); + }); + + it('warning if node key are not same as value', () => { + resetWarned(); + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mount(); + expect(spy).toHaveBeenCalledWith( + 'Warning: `key` or `value` with TreeNode must be the same or you can remove one of them. key: little, value: ttt.', + ); + spy.mockRestore(); }); it('warning if node has same value', () => { - const spy = jest.spyOn(global.console, 'error'); - console.log(">>> Follow Warning is for test purpose. Don't be scared :)"); - render( + resetWarned(); + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mount( , ); - expect(spy).toHaveBeenCalledWith( - "Warning: Conflict! value of node 'bamboo' (ttt) has already used by node 'little'.", - ); + expect(spy).toHaveBeenCalledWith('Warning: Same `value` exist in the tree: ttt'); spy.mockRestore(); }); @@ -107,6 +97,6 @@ describe('TreeSelect.tree', () => { , ); - expect(wrapper.render()).toMatchSnapshot(); + expect(wrapper.getSelection(0).text()).toEqual('empty string'); }); }); diff --git a/tests/Select.warning.spec.js b/tests/Select.warning.spec.js new file mode 100644 index 00000000..7bbde15e --- /dev/null +++ b/tests/Select.warning.spec.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { resetWarned } from 'rc-util/lib/warning'; +import TreeSelect from '../src'; + +describe('TreeSelect.warning', () => { + let spy = null; + + beforeAll(() => { + spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + beforeEach(() => { + resetWarned(); + spy.mockReset(); + }); + + afterAll(() => { + spy.mockRestore(); + }); + + it('warns on invalid value when labelInValue', () => { + mount(); + + expect(spy).toHaveBeenCalledWith( + 'Warning: Invalid prop `value` supplied to `TreeSelect`. You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.', + ); + }); + + it('warns on invalid value when treeCheckable and treeCheckStrictly', () => { + mount(); + + expect(spy).toHaveBeenCalledWith( + 'Warning: Invalid prop `value` supplied to `TreeSelect`. You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.', + ); + }); + + it('warns on invalid value when single', () => { + mount(); + expect(spy).toHaveBeenCalledWith( + 'Warning: `value` should not be array when `TreeSelect` is single mode.', + ); + }); + + it('warns on invalid value when multiple', () => { + mount(); + expect(spy).toHaveBeenCalledWith( + 'Warning: `value` should be an array when `TreeSelect` is checkable or multiple.', + ); + }); + + it('treeCheckStrictly but not labelInValue', () => { + mount(); + expect(spy).toHaveBeenCalledWith( + 'Warning: `treeCheckStrictly` will force set `labelInValue` to `true`.', + ); + }); + + it('documentClickClose should removed', () => { + const wrapper = mount( + { + expect(documentClickClose).toBeFalsy(); + }} + />, + ); + + wrapper.openSelect(); + + expect(spy).toHaveBeenCalledWith( + 'Warning: Second param of `onDropdownVisibleChange` has been removed.', + ); + }); +}); diff --git a/tests/__mocks__/rc-virtual-list.js b/tests/__mocks__/rc-virtual-list.js new file mode 100644 index 00000000..21b6e92a --- /dev/null +++ b/tests/__mocks__/rc-virtual-list.js @@ -0,0 +1,3 @@ +import List from 'rc-virtual-list/lib/mock'; + +export default List; diff --git a/tests/__snapshots__/Select.checkable.spec.js.snap b/tests/__snapshots__/Select.checkable.spec.js.snap index 3ce59a73..67ab8511 100644 --- a/tests/__snapshots__/Select.checkable.spec.js.snap +++ b/tests/__snapshots__/Select.checkable.spec.js.snap @@ -2,166 +2,194 @@ exports[`TreeSelect.checkable uncheck remove by selector not treeCheckStrictly 1`] = `
    - - - - - -
    -
    +   + + +
    +
    -
      -
    • - - - +
      +
      - - 0 - - -
        +
      +
      -
    • - - - +
      - - 0-0 - - -
        -
      • + + + + + 0 + + +
      +
      +
      +
      +
    • -
    - - - - +
    +
    +
    +
    + + @@ -192,118 +223,134 @@ exports[`TreeSelect.checkable uncheck remove by selector not treeCheckStrictly 1 exports[`TreeSelect.checkable uncheck remove by selector not treeCheckStrictly 2`] = `
    - - - - - -
    -
    +   + + + +
    +
    -
      -
    • - - - +
      +
      - - 0 - - -
        +
      +
      -
    • - - - +
      - - 0-0 - - -
        -
      • + + + + + 0 + + +
      +
      +
      +
      +
    • -
    - - - - +
    +
    +
    +
    + + @@ -334,166 +384,194 @@ exports[`TreeSelect.checkable uncheck remove by selector not treeCheckStrictly 2 exports[`TreeSelect.checkable uncheck remove by tree check 1`] = `
    - - - - - -
    -
    + 0  + + +
    +
    -
      -
    • - - - +
      +
      - - 0 - - -
        +
      +
      -
    • - - - +
      - - 0-0 - - -
        -
      • + + + + + 0 + + +
      +
      +
      +
      +
    • -
    - - - - +
    +
    +
    +
    + + @@ -524,118 +605,131 @@ exports[`TreeSelect.checkable uncheck remove by tree check 1`] = ` exports[`TreeSelect.checkable uncheck remove by tree check 2`] = `
    - - - - - -
    -
    + 0  + + +
    +
    -
      -
    • - - - +
      +
      - - 0 - - -
        +
      +
      -
    • - - - +
      - - 0-0 - - -
        -
      • + + + + + 0 + + +
      +
      +
      +
      +
    • -
    - - - - +
    +
    +
    +
    + + diff --git a/tests/__snapshots__/Select.multiple.spec.js.snap b/tests/__snapshots__/Select.multiple.spec.js.snap index 8050e784..d92bf666 100644 --- a/tests/__snapshots__/Select.multiple.spec.js.snap +++ b/tests/__snapshots__/Select.multiple.spec.js.snap @@ -1,314 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TreeSelect.multiple can hide search box by showSearch = false 1`] = ` - - - + + + + + label0 + + + +
    +
    + + + + + +