From 85b07eaecec0ab9b8860c4baa236b8f87066cde0 Mon Sep 17 00:00:00 2001 From: ellinge Date: Thu, 18 Apr 2019 21:47:47 +0200 Subject: [PATCH] feat: Added support for single select in tree dropdown (#217) ## What does it do? Adds support for single select in the tree dropdown. Simple select ignores any children so this is a hybrid between the two. Also ignores clicks for simple select when labels are disabled (only checked readonly) The middle dropdown is radioSelect and the last one a simpleSelect https://ellinge.github.io/react-dropdown-tree-select-test/#DevelopTemp-checkeddefault Fixes #119 ## Type of change - [x] Bug fix - [x] New feature --- .codeclimate.yml | 6 + README.md | 50 ++++++-- __snapshots__/src/checkbox/index.test.js.md | 2 - __snapshots__/src/checkbox/index.test.js.snap | Bin 305 -> 252 bytes __snapshots__/src/index.test.js.md | 115 ++++++++++++++++++ __snapshots__/src/index.test.js.snap | Bin 2558 -> 3066 bytes __snapshots__/src/radio/index.test.js.md | 25 ++++ __snapshots__/src/radio/index.test.js.snap | Bin 0 -> 257 bytes docs/src/stories/Options/index.js | 14 +++ src/checkbox/index.js | 11 +- src/checkbox/index.test.js | 14 --- src/index.js | 54 ++++---- src/index.test.js | 15 ++- src/radio/index.js | 37 ++++++ src/radio/index.test.js | 22 ++++ src/tag/index.css | 3 +- src/tree-manager/flatten-tree.js | 57 ++++++--- src/tree-manager/index.js | 48 ++++++-- src/tree-manager/tests/radioSelect.test.js | 72 +++++++++++ src/tree-node/index.css | 3 +- src/tree-node/index.js | 8 ++ src/tree-node/node-label.js | 41 ++++--- src/tree-node/node-label.test.js | 62 ++++++---- src/tree/index.js | 6 + types/react-dropdown-tree-select.d.ts | 8 ++ 25 files changed, 538 insertions(+), 135 deletions(-) create mode 100644 __snapshots__/src/radio/index.test.js.md create mode 100644 __snapshots__/src/radio/index.test.js.snap create mode 100644 src/radio/index.js create mode 100644 src/radio/index.test.js create mode 100644 src/tree-manager/tests/radioSelect.test.js diff --git a/.codeclimate.yml b/.codeclimate.yml index a99d6e90..46a008a5 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -34,6 +34,12 @@ plugins: eslint: enabled: true channel: 'eslint-5' + duplication: + enabled: true + config: + languages: + javascript: + mass_threshold : 50 exclude_patterns: - 'docs/' - 'snapshots/' diff --git a/README.md b/README.md index 882dfd23..bf95e1f7 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,9 @@ A lightweight and fast control to render a select component that can display hie - [noMatchesText](#nomatchestext) - [keepTreeOnSearch](#keeptreeonsearch) - [keepChildrenOnSearch](#keepchildrenonsearch) + - [keepOpenOnSelect](#keepopenonselect) - [simpleSelect](#simpleselect) + - [radioSelect](#radioSelect) - [showPartiallySelected](#showpartiallyselected) - [showDropdown](#showDropdown) - [form states (disabled|readOnly)](#formstates) @@ -142,11 +144,11 @@ const data = { children: [ { label: 'No one can get me', - value: 'anonymous' - } - ] - } - ] + value: 'anonymous', + }, + ], + }, + ], } const onChange = (currentNode, selectedNodes) => { @@ -159,7 +161,10 @@ const onNodeToggle = currentNode => { console.log('onNodeToggle::', currentNode) } -ReactDOM.render(, document.body) // in real world, you'd want to render to an element, instead of body. +ReactDOM.render( + , + document.body +) // in real world, you'd want to render to an element, instead of body. ``` ## Props @@ -219,7 +224,7 @@ Type: `function` Fires when a action is triggered. Example: ```jsx -function onAction({action, id}) { +function onAction({ action, id }) { console.log(`onAction:: [${action}]`, id) } @@ -298,7 +303,15 @@ Type: `bool` Displays children of found nodes to allow searching for a parent node on then selecting any child node of the found node. Defaults to `false` -*NOTE* this works only in combination with `keepTreeOnSearch` +_NOTE_ this works only in combination with `keepTreeOnSearch` + +### keepOpenOnSelect + +Type: `bool` (default: 'false') + +Keeps single selects open after selection. Defaults to `false` + +_NOTE_ this works only in combination with `simpleSelect` or `radioSelect` ### simpleSelect @@ -306,6 +319,16 @@ Type: `bool` (default: `false`) Turns the dropdown into a simple, single select dropdown. If you pass tree data, only immediate children are picked, grandchildren nodes are ignored. Defaults to `false`. +_NOTE_ if multiple nodes in data are selected, `checked` or `isDefaultValue`, only the first visited node is selected + +### radioSelect + +Type: `bool` (default: `false`) + +Turns the dropdown into radio select dropdown. Similar to simpleSelect but keeps tree/children. Defaults to `false`. + +_NOTE_ if multiple nodes in data are selected, `checked` or `isDefaultValue`, only the first visited node is selected + ### showPartiallySelected Type: `bool` (default: `false`) @@ -353,12 +376,12 @@ module: { fallback: 'style-loader', use: [ { - loader: 'css-loader' - } - ] + loader: 'css-loader', + }, + ], }), - include: /node_modules[/\\]react-dropdown-tree-select/ - } + include: /node_modules[/\\]react-dropdown-tree-select/, + }, ] } ``` @@ -512,6 +535,7 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds | [BaarishRain
BaarishRain](https://github.com/BaarishRain)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3ABaarishRain "Bug reports") | [Kovacs Alexandru Robert
Kovacs Alexandru Robert](http://kovacsalexandrurobert.ro)
[πŸ€”](#ideas-akovacspentalog "Ideas, Planning, & Feedback") | [Alexis Mondragon
Alexis Mondragon](https://github.com/amondragon)
[πŸ€”](#ideas-amondragon "Ideas, Planning, & Feedback") | [Charlie91
Charlie91](https://github.com/Charlie91)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3ACharlie91 "Bug reports") | [Dhirendrasinh
Dhirendrasinh](https://github.com/dhirendrarathod2000)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3Adhirendrarathod2000 "Bug reports") | [JKapostins
JKapostins](https://github.com/JKapostins)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3AJKapostins "Bug reports") | [josvegit
josvegit](https://github.com/josvegit)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3Ajosvegit "Bug reports") | | [Luis Locon
Luis Locon](https://twitter.com/LoconLuis)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3Aloconluis "Bug reports") | [Mikdat DOĞRU
Mikdat DOĞRU](https://github.com/mikdatdogru)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3Amikdatdogru "Bug reports") | [Will Izard
Will Izard](https://github.com/will-izard)
[πŸ€”](#ideas-will-izard "Ideas, Planning, & Feedback") | [Nikola Peric
Nikola Peric](https://gitlab.com/nikperic)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3Anikolap "Bug reports") | [RamΓ³n Alejandro Reyes Fajardo
RamΓ³n Alejandro Reyes Fajardo](https://github.com/ramonrf)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3Aramonrf "Bug reports") | [Sarada Cherukupalli
Sarada Cherukupalli](https://github.com/sarada-Cheukupalli)
[πŸ€”](#ideas-sarada-Cheukupalli "Ideas, Planning, & Feedback") | [Dilip Gavara
Dilip Gavara](https://github.com/dilip025)
[πŸ’»](https://github.com/dowjones/react-dropdown-tree-select/commits?author=dilip025 "Code") | | [Lutz Lengemann
Lutz Lengemann](http://www.dealzeit.de)
[πŸ’»](https://github.com/dowjones/react-dropdown-tree-select/commits?author=mobilutz "Code") | [Akshay Dipta
Akshay Dipta](https://github.com/Eainde)
[πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3AEainde "Bug reports") | [Ian Langworth ☠
Ian Langworth ☠](https://langworth.com/)
[πŸ€”](#ideas-statico "Ideas, Planning, & Feedback") | [Stoyan Berov
Stoyan Berov](https://github.com/stoberov)
[πŸ’»](https://github.com/dowjones/react-dropdown-tree-select/commits?author=stoberov "Code") [πŸ›](https://github.com/dowjones/react-dropdown-tree-select/issues?q=author%3Astoberov "Bug reports") | [ellinge
ellinge](https://github.com/ellinge)
[πŸ’»](https://github.com/dowjones/react-dropdown-tree-select/commits?author=ellinge "Code") [πŸ€”](#ideas-ellinge "Ideas, Planning, & Feedback") [🚧](#maintenance-ellinge "Maintenance") | + This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! diff --git a/__snapshots__/src/checkbox/index.test.js.md b/__snapshots__/src/checkbox/index.test.js.md index 3a2030f2..ac19e9ef 100644 --- a/__snapshots__/src/checkbox/index.test.js.md +++ b/__snapshots__/src/checkbox/index.test.js.md @@ -10,7 +10,6 @@ Generated by [AVA](https://ava.li). @@ -20,6 +19,5 @@ Generated by [AVA](https://ava.li). diff --git a/__snapshots__/src/checkbox/index.test.js.snap b/__snapshots__/src/checkbox/index.test.js.snap index 9fb5e85cf1f47ae0bee5948f2477acccc33c33fd..8120bd31f8454897a613660ba1062a5269149d60 100644 GIT binary patch delta 226 zcmV<803H9a0{j6YK~_N^Q*L2!b7*gLAa*ed0RWqh_jHEe2}xr2`!H{}vxbo(B!6HH z5dY}i&~%aihkZ-PhD>W|o8OFJQGZ5|E?FRpkx__IAh9e_w;-o9Ju^?YC^a#;gb@Uo z7?~r13gUsd9f(C3S%er_GxG{cO9TW(7}C zKs`*NjLZxmot()ziN(c!iMgpT5jVEt#N2|MR5W!gC6xtmB^=2asma+%`4uF%wT^PP ca-?JyCnn{jrho+)1&DGY0G2OOn27-Z0CL`5{Qv*} delta 279 zcmV+y0qFkx0kHxiK~_N^Q*L2!b7*gLAa*ed0RZ~6AWMBW|o8OFJ(T9v6U9vzHBcl+bKw?>!`kOnwYP*9NRe?0>u4+A4J z10x?$50fZ}1=PuzoRe5w?3b9E3TzW$D^AQU$Vp{j dnZ=1oIjJdN0Y(9^Uk}p5uK;QlI361U005SAYqS6W diff --git a/__snapshots__/src/index.test.js.md b/__snapshots__/src/index.test.js.md index ae98e2b9..0b9b76f1 100644 --- a/__snapshots__/src/index.test.js.md +++ b/__snapshots__/src/index.test.js.md @@ -32,6 +32,120 @@ Generated by [AVA](https://ava.li). +## renders default radio select state + +> Snapshot 1 + + + + + ## renders default state > Snapshot 1 @@ -173,6 +287,7 @@ Generated by [AVA](https://ava.li). className="dropdown-content" > { diff --git a/__snapshots__/src/index.test.js.snap b/__snapshots__/src/index.test.js.snap index bb4db0bb636e8149e31acf902211ff73805acd25..30da685cb898c01e790943f11b87751a15ebb3b9 100644 GIT binary patch literal 3066 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 I+ZQ|l0LgaK5&!@I literal 2558 zcmVP!wTwkzYioqsIC@5*5qV+8y*eWqLgeX>&7*W86CP;dMw5eFzh~If;XKv=%XLe_M zhkt03PrLIxzvuV7{eHh^p5Js(5Clp1$BIRt6F)hA&6#_)|FC`NtsWxie^$o?Vddn} zgP(fpmGh_fc0F+Z$gYP7E&bc&f{@y`<;eygC&4elYv6s*L3|04xUQrXv)b*2!IG-3 zSMzG75{q|`K!W&h8o5o?wOK#o@tF9*T>=TAUx9=PiP1Tbh(Kh4v=Xuo&z(dPbj7}` zQc(obhHgZ&Ow47i>Qc5+rj5RAYvk7opUF>@X!-YT)cvoFv<1F{g>OIZue@=7`KTgfe-hTwbe; zt7gf3YB>J>@Fuc#0L7RPqy6M}GR0vUT&b3+pEBp_{bfLMPF~d*B>^ zTMO69)jcB$vtvG<1AQLi!yqR+9jKjkAjnoAvmM;W$j)bFtzCLh_HNTXHfzp}j;f_z zrBu>x?;X)96>XfZ=9{u3ko4zsnXNX1Zu6T4eXU=hL$Im}R@GyE%hp3L;rH*s>)-_V zH|Pq$5W!loHBenk`G!Jqf(^8C&3YQ{FR*SOCT$h1T7m9??O54wt8UQ>gUaYSbzGY?Dt~3G^i`D6vdt+^hMINC0QLQ3 zeeVw%-k(fy4W6s8zMq>4)3&j9hc9UCX-{KMdmnq+)7T+poGRC2*aKiA*c`5Ul{k@S zT*~j_QZtss`c{PFTTyDB_`Ai`C0kt&?;6oIp7%Q9Uz8Ior!dK{f=9y6SlL-C>w6qB z2fz=&VMcy|YixZX+{6h84@l;7<%%sN$}MI{>}#>2r9=x9tLnjO6@DJpe~QL-zKh>y zzz0B%nA@4pG20p7oj6M&|8a0jgb6ZTypVJsq;`QrtaLX|`j3!$1Nd7R=^mc+5=dPQ zHnGx|@WSc6klG2J057sFE_Dx(r&^obC*8d~xM8RolNxw-*o;b^eA`l5-@9j&roAdn zdmL%HnUSVFjx?QgM$v29nvJ4XcYPka>!Y3$xD`F+!NcGnI11hXk*N7k8t1UUvM zCefrX>I@IPYb_)1{?>cfvn%M!F)r*^ zhwLSKYGBGy@;shic9cYLLFx(HONQ}Oa+Dmz({nQ=J^?z#}cpD?qd+vgoRUhAKl@@HsSk9{d^n3tWPW%5@+EcCgo*I9FlVRA{L5KZ9mJ z0xyGqf?nJ;u4nHi?G03=s45N3Ccs|sEI1B=Z4I+h_Guc5mZ8ydFbKARN5L;xMV(wl z#?n(Yfw$4*T@c0_*A?IfpudyujyAkD&WtHlUHb2#(T~9~@D51e748#YBlsfwI#-)a zn^~`vwe*jp*#U4AOzSG*@ZmV4YJLM%#)@UoEC;5*cfl**JgcfUVdqqprW0M15QL9{ z)gS|QfZR?1uo(1%t>Ek6NPELOvwa(kiT)Lh-T{#gL0ATc zz?~fpuhP~DxW8lOyb2Aw+}r%aS+!UhGma{Osg#CiUO_EKcb;{oT3X<5o6Bzk0XmCc zH$97*w=?@RY(d5$6BsTk&|8_q>0Uq@RIWQO_665IFGsAaJ2STxn3H_TTugxNah4Nk zE;mzu0$wbu(eU|}HS2`8;qzxd1fORo)V$Ao`aoy%`Nf?dg3qTs`uwb>djX#hX6rI3 z&puzf0&|jioQnx~^Lft`XfDfX7~O{Ejn3QL*ui+6TT_PDe8$zo-br=l$GrH|ouT_$ z%tfAWPU3d0(R+&yv-#LZ^Q2#b+1(4i+3Czx$2gzKegc`FfcqUaPUos1v#NZln*LiXG1LKbfcYwb8sZ?(JYvvpH&GxNAuh-vXV9 z={;!rFnAPv8%&!cXqcF`^&f5`wS2itYB`%LD@ Us!8bnU*|>t0|4e1s$44o02a3VNB{r; diff --git a/__snapshots__/src/radio/index.test.js.md b/__snapshots__/src/radio/index.test.js.md new file mode 100644 index 00000000..b99f147a --- /dev/null +++ b/__snapshots__/src/radio/index.test.js.md @@ -0,0 +1,25 @@ +# Snapshot report for `src\radio\index.test.js` + +The actual snapshot is saved in `index.test.js.snap`. + +Generated by [AVA](https://ava.li). + +## Radio component + +> Snapshot 1 + + + +## renders disabled state + +> Snapshot 1 + + diff --git a/__snapshots__/src/radio/index.test.js.snap b/__snapshots__/src/radio/index.test.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..921ee379ccb5621752ece75551743d00f029b2cf GIT binary patch literal 257 zcmV+c0sj6$RzV%HI00000000WJ zVqjokVhC6{bLR36WvN4bH%_ulxhBZS00I|)_($)Cri=VP>{~)MWLitx{AL7;PGlqk%7#Nuu82NyDm_$J=pia)@oW$Z{zr@^Bm + +
{ - e.stopPropagation() - e.nativeEvent.stopImmediatePropagation() - this.props.onChange(e) - } - render() { const { checked, indeterminate = false, onChange, disabled, readOnly, ...rest } = this.props @@ -35,7 +26,7 @@ class Checkbox extends PureComponent { diff --git a/src/checkbox/index.test.js b/src/checkbox/index.test.js index acd3b5db..1db42f37 100644 --- a/src/checkbox/index.test.js +++ b/src/checkbox/index.test.js @@ -2,7 +2,6 @@ import { shallow } from 'enzyme' import React from 'react' import test from 'ava' import toJson from 'enzyme-to-json' -import { spy } from 'sinon' import Checkbox, { refUpdater } from './index' @@ -27,16 +26,3 @@ test('renders disabled state', t => { const tree = toJson(shallow()) t.snapshot(tree) }) - -test('call stopPropagation and stopImmediatePropagation when clicked', t => { - const onChange = spy() - const wrapper = shallow() - const event = { - stopPropagation: spy(), - nativeEvent: { stopImmediatePropagation: spy() }, - } - wrapper.simulate('change', event) - t.true(onChange.called) - t.true(event.stopPropagation.called) - t.true(event.nativeEvent.stopImmediatePropagation.called) -}) diff --git a/src/index.js b/src/index.js index 3424d6e0..3be305c2 100644 --- a/src/index.js +++ b/src/index.js @@ -25,6 +25,7 @@ class DropdownTreeSelect extends Component { clearSearchOnChange: PropTypes.bool, keepTreeOnSearch: PropTypes.bool, keepChildrenOnSearch: PropTypes.bool, + keepOpenOnSelect: PropTypes.bool, placeholderText: PropTypes.string, showDropdown: PropTypes.bool, className: PropTypes.string, @@ -34,6 +35,7 @@ class DropdownTreeSelect extends Component { onFocus: PropTypes.func, onBlur: PropTypes.func, simpleSelect: PropTypes.bool, + radioSelect: PropTypes.bool, noMatchesText: PropTypes.string, showPartiallySelected: PropTypes.bool, disabled: PropTypes.bool, @@ -57,15 +59,16 @@ class DropdownTreeSelect extends Component { this.clientId = props.id || clientIdGenerator.get(this) } - createList = ({ data, simpleSelect, showPartiallySelected, hierarchical }) => { + initNewProps = ({ data, simpleSelect, radioSelect, showPartiallySelected, hierarchical }) => { this.treeManager = new TreeManager({ data, simpleSelect, + radioSelect, showPartiallySelected, hierarchical, rootPrefixId: this.clientId, }) - return this.treeManager.tree + this.setState(this.treeManager.getTreeAndTags()) } resetSearchState = () => { @@ -79,10 +82,8 @@ class DropdownTreeSelect extends Component { } componentWillMount() { - const { data, simpleSelect, showPartiallySelected, hierarchical } = this.props - const tree = this.createList({ data, simpleSelect, showPartiallySelected, hierarchical }) - const tags = this.treeManager.getTags() - this.setState({ tree, tags }) + const { data, hierarchical } = this.props + this.initNewProps({ data, hierarchical, ...this.props }) } componentWillUnmount() { @@ -90,10 +91,7 @@ class DropdownTreeSelect extends Component { } componentWillReceiveProps(nextProps) { - const { data, simpleSelect, showPartiallySelected, hierarchical } = nextProps - const tree = this.createList({ data, simpleSelect, showPartiallySelected, hierarchical }) - const tags = this.treeManager.getTags() - this.setState({ tree, tags }) + this.initNewProps(nextProps) } handleClick = () => { @@ -152,9 +150,11 @@ class DropdownTreeSelect extends Component { } onCheckboxChange = (id, checked) => { + const { simpleSelect, radioSelect, keepOpenOnSelect } = this.props this.treeManager.setNodeCheckedState(id, checked) let tags = this.treeManager.getTags() - const showDropdown = this.props.simpleSelect ? false : this.state.showDropdown + const isSingleSelect = simpleSelect || radioSelect + const showDropdown = isSingleSelect && !keepOpenOnSelect ? false : this.state.showDropdown if (!tags.length) { this.treeManager.restoreDefaultValues() @@ -168,11 +168,11 @@ class DropdownTreeSelect extends Component { showDropdown, } - if (this.props.simpleSelect || this.props.clearSearchOnChange) { + if ((isSingleSelect && !showDropdown) || this.props.clearSearchOnChange) { Object.assign(nextState, this.resetSearchState()) } - if (this.props.simpleSelect) { + if (isSingleSelect && !showDropdown) { document.removeEventListener('click', this.handleOutsideClick, false) } @@ -193,13 +193,15 @@ class DropdownTreeSelect extends Component { } render() { + const { disabled, readOnly, simpleSelect, radioSelect } = this.props + const { showDropdown } = this.state const dropdownTriggerClassname = cx({ 'dropdown-trigger': true, arrow: true, - disabled: this.props.disabled, - readOnly: this.props.readOnly, - top: this.state.showDropdown, - bottom: !this.state.showDropdown, + disabled, + readOnly, + top: showDropdown, + bottom: !showDropdown, }) return ( @@ -210,8 +212,8 @@ class DropdownTreeSelect extends Component { this.node = node }} > -
- +
+ { this.searchInput = el @@ -222,12 +224,12 @@ class DropdownTreeSelect extends Component { onFocus={this.onInputFocus} onBlur={this.onInputBlur} onTagRemove={this.onTagRemove} - disabled={this.props.disabled} - readOnly={this.props.readOnly} + disabled={disabled} + readOnly={readOnly} /> - {this.state.showDropdown && ( -
+ {showDropdown && ( +
{this.state.allNodesHidden ? ( {this.props.noMatchesText || 'No matches found'} ) : ( @@ -239,9 +241,11 @@ class DropdownTreeSelect extends Component { onAction={this.onAction} onCheckboxChange={this.onCheckboxChange} onNodeToggle={this.onNodeToggle} - simpleSelect={this.props.simpleSelect} + simpleSelect={simpleSelect} + radioSelect={radioSelect} showPartiallySelected={this.props.showPartiallySelected} - readOnly={this.props.readOnly} + readOnly={readOnly} + clientId={this.clientId} /> )}
diff --git a/src/index.test.js b/src/index.test.js index 87ee8248..3e86b94d 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -60,6 +60,12 @@ test('renders default state', t => { t.snapshot(toJson(wrapper)) }) +test('renders default radio select state', t => { + const { tree } = t.context + const wrapper = mount() + t.snapshot(toJson(wrapper)) +}) + test('shows dropdown', t => { const { tree } = t.context const wrapper = shallow() @@ -107,13 +113,20 @@ test('sets search mode on input change', t => { test('hides dropdown onChange for simpleSelect', t => { const { tree } = t.context - const wrapper = mount() + const wrapper = mount() wrapper.instance().onCheckboxChange(node0._id, true) t.false(wrapper.state().searchModeOn) t.false(wrapper.state().allNodesHidden) t.false(wrapper.state().showDropdown) }) +test('keeps dropdown open onChange for simpleSelect and keepOpenOnSelect', t => { + const { tree } = t.context + const wrapper = mount() + wrapper.instance().onCheckboxChange(node0._id, true) + t.true(wrapper.state().showDropdown) +}) + test('clears input onChange for clearSearchOnChange', t => { const { tree } = t.context const wrapper = mount() diff --git a/src/radio/index.js b/src/radio/index.js new file mode 100644 index 00000000..e150fcef --- /dev/null +++ b/src/radio/index.js @@ -0,0 +1,37 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' + +export const refUpdater = ({ checked }) => input => { + if (input) { + input.checked = checked + } +} + +class RadioButton extends PureComponent { + static propTypes = { + name: PropTypes.string.isRequired, + checked: PropTypes.bool, + onChange: PropTypes.func, + disabled: PropTypes.bool, + readOnly: PropTypes.bool, + } + + render() { + const { name, checked, onChange, disabled, readOnly, ...rest } = this.props + + const isDisabled = disabled || readOnly + + return ( + + ) + } +} + +export default RadioButton diff --git a/src/radio/index.test.js b/src/radio/index.test.js new file mode 100644 index 00000000..152052e1 --- /dev/null +++ b/src/radio/index.test.js @@ -0,0 +1,22 @@ +import { shallow } from 'enzyme' +import React from 'react' +import test from 'ava' +import toJson from 'enzyme-to-json' + +import RadioButton, { refUpdater } from './index' + +test('Radio component', t => { + const input = toJson(shallow()) + t.snapshot(input) +}) + +test('renders checked state', t => { + const input = {} + refUpdater({ checked: true })(input) + t.true(input.checked) +}) + +test('renders disabled state', t => { + const input = toJson(shallow()) + t.snapshot(input) +}) diff --git a/src/tag/index.css b/src/tag/index.css index 2ca1d4e9..98395e88 100644 --- a/src/tag/index.css +++ b/src/tag/index.css @@ -15,7 +15,8 @@ border: none; outline: none; - &.readOnly { + &.readOnly, + &.disabled { cursor: not-allowed; } } diff --git a/src/tree-manager/flatten-tree.js b/src/tree-manager/flatten-tree.js index 68518a3e..de4f840d 100644 --- a/src/tree-manager/flatten-tree.js +++ b/src/tree-manager/flatten-tree.js @@ -92,33 +92,35 @@ const tree = [ } ``` * @param {[type]} tree The incoming tree object - * @param {[bool]} simple Whether its in Single slect mode (simple dropdown) + * @param {[bool]} simple Whether its in Single select mode (simple dropdown) + * @param {[bool]} radio Whether its in Radio select mode (radio dropdown) * @param {[bool]} showPartialState Whether to show partially checked state * @param {[string]} rootPrefixId The prefix to use when setting root node ids * @return {object} The flattened list */ -function flattenTree({ tree, simple, showPartialState, hierarchical, rootPrefixId }) { +function flattenTree({ tree, simple, radio, showPartialState, hierarchical, rootPrefixId }) { const forest = Array.isArray(tree) ? tree : [tree] // eslint-disable-next-line no-use-before-define - const { list, defaultValues } = walkNodes({ + return walkNodes({ nodes: forest, simple, + radio, showPartialState, hierarchical, rootPrefixId, }) - return { list, defaultValues } } /** * If the node didn't specify anything on its own * figure out the initial state based on parent - * @param {object} node [current node] - * @param {object} parent [node's immediate parent] + * @param {object} node [current node] + * @param {object} parent [node's immediate parent] + * @param {bool} inheritChecked [if checked should be inherited] */ -function setInitialStateProps(node, parent = {}) { - const stateProps = ['checked', 'disabled'] +function setInitialStateProps(node, parent = {}, inheritChecked = true) { + const stateProps = inheritChecked ? ['checked', 'disabled'] : ['disabled'] for (let index = 0; index < stateProps.length; index++) { const prop = stateProps[index] @@ -131,15 +133,16 @@ function setInitialStateProps(node, parent = {}) { function walkNodes({ nodes, - list = new Map(), parent, depth = 0, simple, + radio, showPartialState, - defaultValues = [], hierarchical, rootPrefixId, + _rv = { list: new Map(), defaultValues: [], singleSelectedNode: null }, }) { + const single = simple || radio nodes.forEach((node, i) => { node._depth = depth @@ -151,31 +154,48 @@ function walkNodes({ node._id = node.id || `${rootPrefixId ? `${rootPrefixId}-${i}` : i}` } - if (node.isDefaultValue) { - defaultValues.push(node._id) + if (single && node.checked) { + if (_rv.singleSelectedNode) { + node.checked = false + } else { + _rv.singleSelectedNode = node + } + } + + if (single && node.isDefaultValue && _rv.singleSelectedNode && !_rv.singleSelectedNode.isDefaultValue) { + // Default value has precedence, uncheck previous value + _rv.singleSelectedNode.checked = false + _rv.singleSelectedNode = null + } + + if (node.isDefaultValue && (!single || _rv.defaultValues.length === 0)) { + _rv.defaultValues.push(node._id) node.checked = true + if (single) { + _rv.singleSelectedNode = node + } } - if (!hierarchical) setInitialStateProps(node, parent) + if (!hierarchical || radio) setInitialStateProps(node, parent, !radio) - list.set(node._id, node) + _rv.list.set(node._id, node) if (!simple && node.children) { node._children = [] walkNodes({ nodes: node.children, - list, parent: node, depth: depth + 1, + radio, showPartialState, - defaultValues, hierarchical, + _rv, }) if (showPartialState && !node.checked) { node.partial = getPartialState(node) // re-check if all children are checked. if so, check thyself - if (!isEmpty(node.children) && node.children.every(c => c.checked)) { + if (!single && !isEmpty(node.children) && node.children.every(c => c.checked)) { node.checked = true } } @@ -183,7 +203,8 @@ function walkNodes({ node.children = undefined } }) - return { list, defaultValues } + + return _rv } export default flattenTree diff --git a/src/tree-manager/index.js b/src/tree-manager/index.js index fc6c1e5f..14f99a80 100644 --- a/src/tree-manager/index.js +++ b/src/tree-manager/index.js @@ -4,11 +4,12 @@ import { isEmpty } from '../utils' import flattenTree from './flatten-tree' class TreeManager { - constructor({ data, simpleSelect, showPartiallySelected, hierarchical, rootPrefixId }) { + constructor({ data, simpleSelect, radioSelect, showPartiallySelected, hierarchical, rootPrefixId }) { this._src = data - const { list, defaultValues } = flattenTree({ + const { list, defaultValues, singleSelectedNode } = flattenTree({ tree: JSON.parse(JSON.stringify(data)), simple: simpleSelect, + radio: radioSelect, showPartialState: showPartiallySelected, hierarchical, rootPrefixId, @@ -16,9 +17,14 @@ class TreeManager { this.tree = list this.defaultValues = defaultValues this.simpleSelect = simpleSelect + this.radioSelect = radioSelect this.showPartialState = !hierarchical && showPartiallySelected this.searchMaps = new Map() this.hierarchical = hierarchical + if ((simpleSelect || radioSelect) && singleSelectedNode) { + // Remembers initial check on single select dropdowns + this.currentChecked = singleSelectedNode._id + } } getNodeById(id) { @@ -130,14 +136,14 @@ class TreeManager { return this.tree } - togglePreviousChecked(id) { + togglePreviousChecked(id, checked) { const prevChecked = this.currentChecked // if id is same as previously selected node, then do nothing (since it's state is already set correctly by setNodeCheckedState) // but if they ar not same, then toggle the previous one if (prevChecked && prevChecked !== id) this.getNodeById(prevChecked).checked = false - this.currentChecked = id + this.currentChecked = checked ? id : null } setNodeCheckedState(id, checked) { @@ -150,7 +156,15 @@ class TreeManager { } if (this.simpleSelect) { - this.togglePreviousChecked(id) + this.togglePreviousChecked(id, checked) + } else if (this.radioSelect) { + this.togglePreviousChecked(id, checked) + if (this.showPartialState) { + this.partialCheckParents(node) + } + if (!checked) { + this.unCheckParents(node) + } } else { if (!this.hierarchical) this.toggleChildren(id, checked) @@ -222,6 +236,10 @@ class TreeManager { } getTags() { + if (this.radioSelect || this.simpleSelect) { + return this._getTagsForSingleSelect() + } + const tags = [] const visited = {} const markSubTreeVisited = node => { @@ -234,17 +252,27 @@ class TreeManager { if (node.checked) { tags.push(node) - - if (!this.hierarchical) { - // Parent node, so no need to walk children - markSubTreeVisited(node) - } } else { visited[key] = true } + if (node.checked && !this.hierarchical) { + // Parent node, so no need to walk children + markSubTreeVisited(node) + } }) return tags } + + getTreeAndTags() { + return { tree: this.tree, tags: this.getTags() } + } + + _getTagsForSingleSelect() { + if (this.currentChecked) { + return [this.getNodeById(this.currentChecked)] + } + return [] + } } export default TreeManager diff --git a/src/tree-manager/tests/radioSelect.test.js b/src/tree-manager/tests/radioSelect.test.js new file mode 100644 index 00000000..cfdf1a86 --- /dev/null +++ b/src/tree-manager/tests/radioSelect.test.js @@ -0,0 +1,72 @@ +import test from 'ava' +import React from 'react' +import { mount } from 'enzyme' +import TreeManager from '..' +import DropdownTreeSelect from '../../' + +const dropdownId = 'rdts' +const tree = ['nodeA', 'nodeB', 'nodeC'].map(nv => ({ id: nv, label: nv, value: nv })) + +test('should render radio inputs with shared name', t => { + const wrapper = mount() + + const inputs = wrapper.find('.dropdown-content').find(`input[type="radio"][name="${dropdownId}"]`) + t.deepEqual(inputs.length, 3) +}) + +test('hides dropdown onChange for radioSelect', t => { + const wrapper = mount() + wrapper.instance().onCheckboxChange('nodeA', true) + t.false(wrapper.state().searchModeOn) + t.false(wrapper.state().allNodesHidden) + t.false(wrapper.state().showDropdown) +}) + +test('keeps dropdown open onChange for radioSelect and keepOpenOnSelect', t => { + const wrapper = mount() + wrapper.instance().onCheckboxChange('nodeA', true) + t.true(wrapper.state().showDropdown) +}) + +test('should deselect previous node', t => { + const manager = new TreeManager({ data: tree, radioSelect: true }) + + // first select a node + manager.setNodeCheckedState('nodeA', true) + + // then select another node + manager.setNodeCheckedState('nodeB', true) + + t.false(manager.getNodeById('nodeA').checked) + t.true(manager.getNodeById('nodeB').checked) +}) + +test('should only select single first checked node on init', t => { + const data = tree.map(n => ({ ...n, checked: true })) + + const manager = new TreeManager({ data, radioSelect: true }) + + t.true(manager.getNodeById('nodeA').checked) + t.false(manager.getNodeById('nodeB').checked) + t.false(manager.getNodeById('nodeC').checked) +}) + +test('should only select single first default value node on init', t => { + const data = tree.map(n => ({ ...n, isDefaultValue: true })) + + const manager = new TreeManager({ data, radioSelect: true }) + + t.true(manager.getNodeById('nodeA').checked) + t.falsy(manager.getNodeById('nodeB').checked) + t.falsy(manager.getNodeById('nodeC').checked) +}) + +test('should select single first default node and ignore any checked', t => { + const data = [{ id: 'nodeA', checked: true }, { id: 'nodeB', isDefaultValue: true }, { id: 'nodeC', checked: true }] + + const manager = new TreeManager({ data, radioSelect: true }) + + t.false(manager.getNodeById('nodeA').checked) + t.true(manager.getNodeById('nodeB').checked) + t.false(manager.getNodeById('nodeC').checked) +}) diff --git a/src/tree-node/index.css b/src/tree-node/index.css index 784bf20a..4f5c7bb8 100644 --- a/src/tree-node/index.css +++ b/src/tree-node/index.css @@ -45,7 +45,8 @@ display: none; } -.checkbox-item { +.checkbox-item, +.radio-item { vertical-align: middle; margin: 0 4px 0 0; diff --git a/src/tree-node/index.js b/src/tree-node/index.js index 2998b572..324fce7b 100644 --- a/src/tree-node/index.js +++ b/src/tree-node/index.js @@ -26,6 +26,7 @@ const getNodeCx = props => { className, showPartiallySelected, readOnly, + checked, } = props return cx( @@ -39,6 +40,7 @@ const getNodeCx = props => { 'match-in-parent': keepTreeOnSearch && keepChildrenOnSearch && matchInParent, partial: showPartiallySelected && partial, readOnly, + checked, }, className ) @@ -66,13 +68,16 @@ class TreeNode extends PureComponent { onAction: PropTypes.func, onCheckboxChange: PropTypes.func, simpleSelect: PropTypes.bool, + radioSelect: PropTypes.bool, showPartiallySelected: PropTypes.bool, readOnly: PropTypes.bool, + clientId: PropTypes.string, } render() { const { simpleSelect, + radioSelect, keepTreeOnSearch, _id, _children, @@ -92,6 +97,7 @@ class TreeNode extends PureComponent { onCheckboxChange, showPartiallySelected, readOnly, + clientId, } = this.props const liCx = getNodeCx(this.props) const style = keepTreeOnSearch || !searchModeOn ? { paddingLeft: `${(_depth || 0) * 20}px` } : {} @@ -108,9 +114,11 @@ class TreeNode extends PureComponent { value={value} disabled={disabled} simpleSelect={simpleSelect} + radioSelect={radioSelect} onCheckboxChange={onCheckboxChange} showPartiallySelected={showPartiallySelected} readOnly={readOnly} + clientId={clientId} /> diff --git a/src/tree-node/node-label.js b/src/tree-node/node-label.js index 90b200fd..89bac4be 100644 --- a/src/tree-node/node-label.js +++ b/src/tree-node/node-label.js @@ -2,6 +2,7 @@ import cn from 'classnames/bind' import PropTypes from 'prop-types' import React, { PureComponent } from 'react' import Checkbox from '../checkbox' +import RadioButton from '../radio' import styles from './index.css' @@ -20,15 +21,17 @@ class NodeLabel extends PureComponent { disabled: PropTypes.bool, dataset: PropTypes.object, simpleSelect: PropTypes.bool, + radioSelect: PropTypes.bool, showPartiallySelected: PropTypes.bool, onCheckboxChange: PropTypes.func, readOnly: PropTypes.bool, + clientId: PropTypes.string, } handleCheckboxChange = e => { - const { simpleSelect, id, onCheckboxChange } = this.props + const { simpleSelect, radioSelect, id, onCheckboxChange } = this.props - if (simpleSelect) { + if (simpleSelect || radioSelect) { onCheckboxChange(id, true) } else { const { @@ -36,34 +39,38 @@ class NodeLabel extends PureComponent { } = e onCheckboxChange(id, checked) } + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() } render() { - const { simpleSelect, title, label, id, partial, checked } = this.props - const { value, disabled, showPartiallySelected, readOnly } = this.props + const { simpleSelect, radioSelect, title, label, id, partial, checked } = this.props + const { value, disabled, showPartiallySelected, readOnly, clientId } = this.props const nodeLabelProps = { className: 'node-label' } // in case of simple select mode, there is no checkbox, so we need to handle the click via the node label - // but not if the control is in readOnly state - const shouldRegisterClickHandler = simpleSelect && !readOnly + // but not if the control is in readOnly or disabled state + const shouldRegisterClickHandler = simpleSelect && !readOnly && !disabled if (shouldRegisterClickHandler) { nodeLabelProps.onClick = this.handleCheckboxChange } + const sharedProps = { id, value, checked, disabled, readOnly } + return ( ) diff --git a/src/tree-node/node-label.test.js b/src/tree-node/node-label.test.js index 4a854b8e..e5c97e40 100644 --- a/src/tree-node/node-label.test.js +++ b/src/tree-node/node-label.test.js @@ -1,4 +1,4 @@ -import { shallow } from 'enzyme' +import { shallow, mount } from 'enzyme' import { spy } from 'sinon' import React from 'react' import test from 'ava' @@ -6,13 +6,22 @@ import toJson from 'enzyme-to-json' import NodeLabel from './node-label' +const mockEvent = { + target: { checked: true }, + stopPropagation: () => undefined, + nativeEvent: { stopImmediatePropagation: () => undefined }, +} +const baseNode = { + id: '0-0-0', + _parent: '0-0', + label: 'item0-0-0', + value: 'value0-0-0', + className: 'cn0-0-0', +} + test('renders node label', t => { const node = { - id: '0-0-0', - _parent: '0-0', - label: 'item0-0-0', - value: 'value0-0-0', - className: 'cn0-0-0', + ...baseNode, actions: [ { id: 'NOT', @@ -29,29 +38,21 @@ test('renders node label', t => { test('notifies checkbox changes', t => { const node = { - id: '0-0-0', - _parent: '0-0', - label: 'item0-0-0', - value: 'value0-0-0', - className: 'cn0-0-0', + ...baseNode, checked: false, } const onChange = spy() const wrapper = shallow() - wrapper.find('.checkbox-item').simulate('change', { target: { checked: true } }) + wrapper.find('.checkbox-item').simulate('change', mockEvent) t.true(onChange.calledWith('0-0-0', true)) }) test('disable checkbox if the node has disabled status', t => { const node = { - id: '0-0-0', - _parent: '0-0', + ...baseNode, disabled: true, - label: 'item0-0-0', - value: 'value0-0-0', - className: 'cn0-0-0', } const wrapper = shallow() @@ -61,17 +62,32 @@ test('disable checkbox if the node has disabled status', t => { test('notifies clicks in simple mode', t => { const node = { - id: '0-0-0', - _parent: '0-0', - label: 'item0-0-0', - value: 'value0-0-0', - className: 'cn0-0-0', + ...baseNode, checked: false, } const onChange = spy() const wrapper = shallow() - wrapper.find('.node-label').simulate('click') + wrapper.find('.node-label').simulate('click', mockEvent) t.true(onChange.calledWith('0-0-0', true)) }) + +test('call stopPropagation and stopImmediatePropagation when label is clicked', t => { + const node = { + ...baseNode, + checked: false, + } + + const onChange = spy() + + const wrapper = mount() + const event = { + type: 'click', + stopPropagation: spy(), + nativeEvent: { stopImmediatePropagation: spy() }, + } + wrapper.find('input').prop('onChange')(event) + t.true(event.stopPropagation.called) + t.true(event.nativeEvent.stopImmediatePropagation.called) +}) diff --git a/src/tree/index.js b/src/tree/index.js index b254774c..52fb3283 100644 --- a/src/tree/index.js +++ b/src/tree/index.js @@ -24,9 +24,11 @@ class Tree extends Component { onAction: PropTypes.func, onCheckboxChange: PropTypes.func, simpleSelect: PropTypes.bool, + radioSelect: PropTypes.bool, showPartiallySelected: PropTypes.bool, pageSize: PropTypes.number, readOnly: PropTypes.bool, + clientId: PropTypes.string, } static defaultProps = { @@ -65,12 +67,14 @@ class Tree extends Component { keepChildrenOnSearch, searchModeOn, simpleSelect, + radioSelect, showPartiallySelected, readOnly, onAction, onChange, onCheckboxChange, onNodeToggle, + clientId, } = props const items = [] data.forEach(node => { @@ -87,8 +91,10 @@ class Tree extends Component { onNodeToggle={onNodeToggle} onAction={onAction} simpleSelect={simpleSelect} + radioSelect={radioSelect} showPartiallySelected={showPartiallySelected} readOnly={readOnly} + clientId={clientId} /> ) } diff --git a/types/react-dropdown-tree-select.d.ts b/types/react-dropdown-tree-select.d.ts index 76f9eafd..f5a21005 100644 --- a/types/react-dropdown-tree-select.d.ts +++ b/types/react-dropdown-tree-select.d.ts @@ -15,6 +15,10 @@ declare module "react-dropdown-tree-select" { * NOTE this works only in combination with keepTreeOnSearch */ keepChildrenOnSearch?: boolean; + /** Keeps single selects open after selection. Defaults to `false` + * NOTE this works only in combination with simpleSelect or radioSelect + */ + keepOpenOnSelect?: boolean; /** The text to display as placeholder on the search box. Defaults to Choose... */ placeholderText?: string; /** If set to true, shows the dropdown when rendered. @@ -51,6 +55,10 @@ declare module "react-dropdown-tree-select" { * Defaults to false */ simpleSelect?: boolean; + /** Turns the dropdown into radio select dropdown. Similar to simpleSelect but keeps tree/children. Defaults to `false`. + * *NOTE* if multiple nodes in data are selected, checked or isDefaultValue, only the first visited node is selected + */ + radioSelect?: boolean; /** The text to display when the search does not find results in the content list. Defaults to No matches found */ noMatchesText?: string; /** If set to true, shows checkboxes in a partial state when one, but not all of their children are selected.