diff --git a/.gitignore b/.gitignore index b34f71df..5e8b3291 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ /.css-compare /examples/dist -/lib /node_modules package-lock.json yarn.lock diff --git a/README.md b/README.md index 9a569230..165182a8 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ class Widget extends React.Component { render() { return ( ( gulp.task('build-style', () => ( gulp.src('./src/scss/**/*.scss') - .pipe(scsslint()) - .pipe(scsslint.failReporter()) + // .pipe(scsslint()) + // .pipe(scsslint.failReporter()) .pipe(sass({ outputStyle: 'expanded', }).on('error', sass.logError)) @@ -98,8 +98,8 @@ function buildExamplesScript(mode = 'development') { function buildExamplesStyle(minifyStyles = false) { let stream = gulp.src('./examples/src/scss/**/*.scss') - .pipe(scsslint()) - .pipe(scsslint.failReporter()) + // .pipe(scsslint()) + // .pipe(scsslint.failReporter()) .pipe(sass({ outputStyle: 'expanded', }).on('error', sass.logError)) diff --git a/lib/index.browser.js b/lib/index.browser.js new file mode 100644 index 00000000..0e208248 --- /dev/null +++ b/lib/index.browser.js @@ -0,0 +1,13 @@ +/*! react-checkbox-tree - v1.5.1 | 2019 */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.ReactCheckboxTree=t(require("react")):e.ReactCheckboxTree=t(e.React)}(window,function(e){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=27)}([function(e,t,n){e.exports=n(96)()},function(t,n){t.exports=e},function(e,t,n){var r=n(16),o="object"==typeof self&&self&&self.Object===Object&&self,a=r||o||Function("return this")();e.exports=a},function(e,t,n){var r=n(41),o=n(47);e.exports=function(e,t){var n=o(e,t);return r(n)?n:void 0}},function(e,t,n){var r=n(31),o=n(32),a=n(33),i=n(34),c=n(35);function s(e){var t=-1,n=null==e?0:e.length;for(this.clear();++tf))return!1;var h=u.get(e);if(h&&u.get(t))return h==t;var b=-1,y=!0,v=n&c?new r:void 0;for(u.set(e,t),u.set(t,e);++b-1&&e%1==0&&e<=n}},function(e,t,n){var r=n(28);e.exports=function(e,t){return r(e,t)}},function(e,t,n){var r=self.crypto||self.msCrypto;e.exports=function(e){e=e||21;for(var t="",n=r.getRandomValues(new Uint8Array(e));0-1}},function(e,t,n){var r=n(5);e.exports=function(e,t){var n=this.__data__,o=r(n,e);return o<0?(++this.size,n.push([e,t])):n[o][1]=t,this}},function(e,t,n){var r=n(4);e.exports=function(){this.__data__=new r,this.size=0}},function(e,t){e.exports=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n}},function(e,t){e.exports=function(e){return this.__data__.get(e)}},function(e,t){e.exports=function(e){return this.__data__.has(e)}},function(e,t,n){var r=n(4),o=n(11),a=n(19),i=200;e.exports=function(e,t){var n=this.__data__;if(n instanceof r){var c=n.__data__;if(!o||c.length-1&&e%1==0&&e=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function y(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{};!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.props=t,this.flatNodes=n}var t,n,r;return t=e,(n=[{key:"setProps",value:function(e){this.props=e}},{key:"clone",value:function(){var t=this,n={};return Object.keys(this.flatNodes).forEach(function(e){var r=t.flatNodes[e];n[e]=function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{},r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if(Array.isArray(e)&&0!==e.length){var o=this.props,a=o.disabled,i=o.noCascade;e.forEach(function(e,o){var c=t.nodeHasChildren(e);t.flatNodes[e.value]={label:e.label,value:e.value,children:e.children,parent:n,isParent:c,isLeaf:!c,showCheckbox:void 0===e.showCheckbox||e.showCheckbox,disabled:t.getDisabledState(e,n,a,i),treeDepth:r,index:o},t.flattenNodes(e.children,e,r+1)})}}},{key:"nodeHasChildren",value:function(e){return Array.isArray(e.children)&&e.children.length>0}},{key:"getDisabledState",value:function(e,t,n,r){return!!n||(!(r||!t.disabled)||Boolean(e.disabled))}},{key:"deserializeLists",value:function(e){var t=this,n=["checked","expanded"];Object.keys(this.flatNodes).forEach(function(e){n.forEach(function(n){t.flatNodes[e][n]=!1})}),n.forEach(function(n){e[n].forEach(function(e){void 0!==t.flatNodes[e]&&(t.flatNodes[e][n]=!0)})})}},{key:"serializeList",value:function(e){var t=this,n=[];return Object.keys(this.flatNodes).forEach(function(r){t.flatNodes[r][e]&&n.push(r)}),n}},{key:"expandAllNodes",value:function(e){var t=this;return Object.keys(this.flatNodes).forEach(function(n){t.flatNodes[n].isParent&&(t.flatNodes[n].expanded=e)}),this}},{key:"toggleChecked",value:function(e,t,n){var r=this,o=this.flatNodes[e.value];if(o.isLeaf||n){if(e.disabled)return this;this.toggleNode(e.value,"checked",t)}else o.children.forEach(function(e){r.toggleChecked(e,t,n)});return this}},{key:"toggleNode",value:function(e,t,n){return this.flatNodes[e][t]=n,this}}])&&w(t.prototype,n),r&&w(t,r),e}();function P(e){return(P="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function E(){return(E=Object.assign||function(e){for(var t=1;t0&&void 0!==arguments[0])||arguments[0],t=this.props.onExpand;t(this.state.model.clone().expandAllNodes(e).serializeList("expanded"))}},{key:"determineShallowCheckState",value:function(e,t){var n=this.state.model.getNode(e.value);return n.isLeaf||t?n.checked?1:0:this.isEveryChildChecked(e)?1:this.isSomeChildChecked(e)?2:0}},{key:"isEveryChildChecked",value:function(e){var t=this;return e.children.every(function(e){return 1===t.state.model.getNode(e.value).checkState})}},{key:"isSomeChildChecked",value:function(e){var t=this;return e.children.some(function(e){return t.state.model.getNode(e.value).checkState>0})}},{key:"renderTreeNodes",value:function(e){var n=this,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=this.props,a=o.expandDisabled,i=o.expandOnClick,c=o.icons,s=o.lang,l=o.noCascade,u=o.onClick,p=o.onlyLeafCheckboxes,d=o.optimisticToggle,h=o.showNodeTitle,b=o.showNodeIcon,y=this.state,v=y.focusedNodeIndex,x=y.id,m=y.model,g=t.defaultProps.icons,k=e.map(function(e){var t=!r.value||m.getNode(r.value).expanded;t&&n.visibleNodes.push(e.value);var o=e.value,y=m.getNode(e.value),k=y.isParent?n.renderTreeNodes(e.children,e):null;y.checkState=n.determineShallowCheckState(e,l);var O=p?y.isLeaf:y.showCheckbox;return t?f.a.createElement(V,{key:o,checked:y.checkState,className:e.className,disabled:y.disabled,expandDisabled:a,expandOnClick:i,expanded:y.expanded,hasFocus:n.visibleNodes[v]===e.value,icon:e.icon,icons:ee({},g,{},c),label:e.label,lang:s,optimisticToggle:d,isLeaf:y.isLeaf,isParent:y.isParent,showCheckbox:O,showNodeIcon:b,title:h?e.title||e.label:e.title,treeId:x,value:e.value,onCheck:n.onCheck,onClick:u&&n.onNodeClick,onExpand:n.onExpand},k):null});return f.a.createElement("ol",{role:"presentation"},k)}},{key:"renderExpandAll",value:function(){var e=this.props,t=e.icons,n=t.expandAll,r=t.collapseAll,o=e.lang;return e.showExpandAll?f.a.createElement("div",{className:"rct-options"},f.a.createElement(O,{className:"rct-option rct-option-expand-all",title:o.expandAll,onClick:this.onExpandAll},n),f.a.createElement(O,{className:"rct-option rct-option-collapse-all",title:o.collapseAll,onClick:this.onCollapseAll},r)):null}},{key:"renderHiddenInput",value:function(){var e=this.props,t=e.name,n=e.nameAsArray;return void 0===t?null:n?this.renderArrayHiddenInput():this.renderJoinedHiddenInput()}},{key:"renderArrayHiddenInput",value:function(){var e=this.props,t=e.checked,n=e.name;return t.map(function(e){var t="".concat(n,"[]");return f.a.createElement("input",{key:e,name:t,type:"hidden",value:e})})}},{key:"renderJoinedHiddenInput",value:function(){var e=this.props,t=e.checked,n=e.name,r=t.join(",");return f.a.createElement("input",{name:n,type:"hidden",value:r})}},{key:"render",value:function(){var e=this.props,t=e.disabled,n=e.nodes,r=e.nativeCheckboxes,a=null===this.state.focusedNodeIndex;this.visibleNodes=[];var i=this.renderTreeNodes(n),c=o()({"react-checkbox-tree":!0,"rct-disabled":t,"rct-native-display":r});return f.a.createElement("div",{className:c},this.renderExpandAll(),this.renderHiddenInput(),f.a.createElement("div",{"aria-label":this.props["aria-label"],onFocus:this.onFocus,onKeyDown:this.onKeyDown,role:"tree",tabIndex:a?0:-1},i))}}])&&te(n.prototype,r),a&&te(n,a),t}();ae(ce,"propTypes",{nodes:u.a.arrayOf(Q).isRequired,"aria-label":u.a.string,checked:$,disabled:u.a.bool,expandDisabled:u.a.bool,expandOnClick:u.a.bool,expanded:$,icons:z,id:u.a.string,lang:q,name:u.a.string,nameAsArray:u.a.bool,nativeCheckboxes:u.a.bool,noCascade:u.a.bool,onlyLeafCheckboxes:u.a.bool,optimisticToggle:u.a.bool,showExpandAll:u.a.bool,showNodeIcon:u.a.bool,showNodeTitle:u.a.bool,onCheck:u.a.func,onClick:u.a.func,onExpand:u.a.func}),ae(ce,"defaultProps",{"aria-label":null,checked:[],disabled:!1,expandDisabled:!1,expandOnClick:!1,expanded:[],icons:{check:f.a.createElement("span",{className:"rct-icon rct-icon-check"}),uncheck:f.a.createElement("span",{className:"rct-icon rct-icon-uncheck"}),halfCheck:f.a.createElement("span",{className:"rct-icon rct-icon-half-check"}),expandClose:f.a.createElement("span",{className:"rct-icon rct-icon-expand-close"}),expandOpen:f.a.createElement("span",{className:"rct-icon rct-icon-expand-open"}),expandAll:f.a.createElement("span",{className:"rct-icon rct-icon-expand-all"}),collapseAll:f.a.createElement("span",{className:"rct-icon rct-icon-collapse-all"}),parentClose:f.a.createElement("span",{className:"rct-icon rct-icon-parent-close"}),parentOpen:f.a.createElement("span",{className:"rct-icon rct-icon-parent-open"}),leaf:f.a.createElement("span",{className:"rct-icon rct-icon-leaf"})},id:null,lang:{collapseAll:"Collapse all",expandAll:"Expand all",toggle:"Toggle"},name:void 0,nameAsArray:!1,nativeCheckboxes:!1,noCascade:!1,onlyLeafCheckboxes:!1,optimisticToggle:!0,showExpandAll:!1,showNodeIcon:!0,showNodeTitle:!1,onCheck:function(){},onClick:null,onExpand:function(){}});t.default=ce}])}); \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 00000000..2e391489 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,13 @@ +/*! react-checkbox-tree - v1.5.1 | 2019 */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.ReactCheckboxTree=t(require("react")):e.ReactCheckboxTree=t(e.React)}(global,function(e){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=27)}([function(e,t,n){e.exports=n(95)()},function(t,n){t.exports=e},function(e,t,n){var r=n(16),o="object"==typeof self&&self&&self.Object===Object&&self,a=r||o||Function("return this")();e.exports=a},function(e,t,n){var r=n(41),o=n(46);e.exports=function(e,t){var n=o(e,t);return r(n)?n:void 0}},function(e,t,n){var r=n(31),o=n(32),a=n(33),i=n(34),c=n(35);function s(e){var t=-1,n=null==e?0:e.length;for(this.clear();++tf))return!1;var h=u.get(e);if(h&&u.get(t))return h==t;var b=-1,y=!0,v=n&c?new r:void 0;for(u.set(e,t),u.set(t,e);++b-1&&e%1==0&&e<=n}},function(e,t,n){var r=n(28);e.exports=function(e,t){return r(e,t)}},function(e,t,n){var r=n(97),o=n(99);e.exports=function(e){for(var t=r(e=e||21),n="";0-1}},function(e,t,n){var r=n(5);e.exports=function(e,t){var n=this.__data__,o=r(n,e);return o<0?(++this.size,n.push([e,t])):n[o][1]=t,this}},function(e,t,n){var r=n(4);e.exports=function(){this.__data__=new r,this.size=0}},function(e,t){e.exports=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n}},function(e,t){e.exports=function(e){return this.__data__.get(e)}},function(e,t){e.exports=function(e){return this.__data__.has(e)}},function(e,t,n){var r=n(4),o=n(11),a=n(19),i=200;e.exports=function(e,t){var n=this.__data__;if(n instanceof r){var c=n.__data__;if(!o||c.length-1&&e%1==0&&e=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function y(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{};!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.props=t,this.flatNodes=n}var t,n,r;return t=e,(n=[{key:"setProps",value:function(e){this.props=e}},{key:"clone",value:function(){var t=this,n={};return Object.keys(this.flatNodes).forEach(function(e){var r=t.flatNodes[e];n[e]=function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{},r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if(Array.isArray(e)&&0!==e.length){var o=this.props,a=o.disabled,i=o.noCascade;e.forEach(function(e,o){var c=t.nodeHasChildren(e);t.flatNodes[e.value]={label:e.label,value:e.value,children:e.children,parent:n,isParent:c,isLeaf:!c,showCheckbox:void 0===e.showCheckbox||e.showCheckbox,disabled:t.getDisabledState(e,n,a,i),treeDepth:r,index:o},t.flattenNodes(e.children,e,r+1)})}}},{key:"nodeHasChildren",value:function(e){return Array.isArray(e.children)&&e.children.length>0}},{key:"getDisabledState",value:function(e,t,n,r){return!!n||(!(r||!t.disabled)||Boolean(e.disabled))}},{key:"deserializeLists",value:function(e){var t=this,n=["checked","expanded"];Object.keys(this.flatNodes).forEach(function(e){n.forEach(function(n){t.flatNodes[e][n]=!1})}),n.forEach(function(n){e[n].forEach(function(e){void 0!==t.flatNodes[e]&&(t.flatNodes[e][n]=!0)})})}},{key:"serializeList",value:function(e){var t=this,n=[];return Object.keys(this.flatNodes).forEach(function(r){t.flatNodes[r][e]&&n.push(r)}),n}},{key:"expandAllNodes",value:function(e){var t=this;return Object.keys(this.flatNodes).forEach(function(n){t.flatNodes[n].isParent&&(t.flatNodes[n].expanded=e)}),this}},{key:"toggleChecked",value:function(e,t,n){var r=this,o=this.flatNodes[e.value];if(o.isLeaf||n){if(e.disabled)return this;this.toggleNode(e.value,"checked",t)}else o.children.forEach(function(e){r.toggleChecked(e,t,n)});return this}},{key:"toggleNode",value:function(e,t,n){return this.flatNodes[e][t]=n,this}}])&&w(t.prototype,n),r&&w(t,r),e}();function P(e){return(P="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function E(){return(E=Object.assign||function(e){for(var t=1;t0&&void 0!==arguments[0])||arguments[0],t=this.props.onExpand;t(this.state.model.clone().expandAllNodes(e).serializeList("expanded"))}},{key:"determineShallowCheckState",value:function(e,t){var n=this.state.model.getNode(e.value);return n.isLeaf||t?n.checked?1:0:this.isEveryChildChecked(e)?1:this.isSomeChildChecked(e)?2:0}},{key:"isEveryChildChecked",value:function(e){var t=this;return e.children.every(function(e){return 1===t.state.model.getNode(e.value).checkState})}},{key:"isSomeChildChecked",value:function(e){var t=this;return e.children.some(function(e){return t.state.model.getNode(e.value).checkState>0})}},{key:"renderTreeNodes",value:function(e){var n=this,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=this.props,a=o.expandDisabled,i=o.expandOnClick,c=o.icons,s=o.lang,l=o.noCascade,u=o.onClick,p=o.onlyLeafCheckboxes,d=o.optimisticToggle,h=o.showNodeTitle,b=o.showNodeIcon,y=this.state,v=y.focusedNodeIndex,x=y.id,g=y.model,m=t.defaultProps.icons,k=e.map(function(e){var t=!r.value||g.getNode(r.value).expanded;t&&n.visibleNodes.push(e.value);var o=e.value,y=g.getNode(e.value),k=y.isParent?n.renderTreeNodes(e.children,e):null;y.checkState=n.determineShallowCheckState(e,l);var O=p?y.isLeaf:y.showCheckbox;return t?f.a.createElement(V,{key:o,checked:y.checkState,className:e.className,disabled:y.disabled,expandDisabled:a,expandOnClick:i,expanded:y.expanded,hasFocus:n.visibleNodes[v]===e.value,icon:e.icon,icons:ee({},m,{},c),label:e.label,lang:s,optimisticToggle:d,isLeaf:y.isLeaf,isParent:y.isParent,showCheckbox:O,showNodeIcon:b,title:h?e.title||e.label:e.title,treeId:x,value:e.value,onCheck:n.onCheck,onClick:u&&n.onNodeClick,onExpand:n.onExpand},k):null});return f.a.createElement("ol",{role:"presentation"},k)}},{key:"renderExpandAll",value:function(){var e=this.props,t=e.icons,n=t.expandAll,r=t.collapseAll,o=e.lang;return e.showExpandAll?f.a.createElement("div",{className:"rct-options"},f.a.createElement(O,{className:"rct-option rct-option-expand-all",title:o.expandAll,onClick:this.onExpandAll},n),f.a.createElement(O,{className:"rct-option rct-option-collapse-all",title:o.collapseAll,onClick:this.onCollapseAll},r)):null}},{key:"renderHiddenInput",value:function(){var e=this.props,t=e.name,n=e.nameAsArray;return void 0===t?null:n?this.renderArrayHiddenInput():this.renderJoinedHiddenInput()}},{key:"renderArrayHiddenInput",value:function(){var e=this.props,t=e.checked,n=e.name;return t.map(function(e){var t="".concat(n,"[]");return f.a.createElement("input",{key:e,name:t,type:"hidden",value:e})})}},{key:"renderJoinedHiddenInput",value:function(){var e=this.props,t=e.checked,n=e.name,r=t.join(",");return f.a.createElement("input",{name:n,type:"hidden",value:r})}},{key:"render",value:function(){var e=this.props,t=e.disabled,n=e.nodes,r=e.nativeCheckboxes,a=null===this.state.focusedNodeIndex;this.visibleNodes=[];var i=this.renderTreeNodes(n),c=o()({"react-checkbox-tree":!0,"rct-disabled":t,"rct-native-display":r});return f.a.createElement("div",{className:c},this.renderExpandAll(),this.renderHiddenInput(),f.a.createElement("div",{"aria-label":this.props["aria-label"],onFocus:this.onFocus,onKeyDown:this.onKeyDown,role:"tree",tabIndex:a?0:-1},i))}}])&&te(n.prototype,r),a&&te(n,a),t}();ae(ce,"propTypes",{nodes:u.a.arrayOf(Q).isRequired,"aria-label":u.a.string,checked:$,disabled:u.a.bool,expandDisabled:u.a.bool,expandOnClick:u.a.bool,expanded:$,icons:z,id:u.a.string,lang:q,name:u.a.string,nameAsArray:u.a.bool,nativeCheckboxes:u.a.bool,noCascade:u.a.bool,onlyLeafCheckboxes:u.a.bool,optimisticToggle:u.a.bool,showExpandAll:u.a.bool,showNodeIcon:u.a.bool,showNodeTitle:u.a.bool,onCheck:u.a.func,onClick:u.a.func,onExpand:u.a.func}),ae(ce,"defaultProps",{"aria-label":null,checked:[],disabled:!1,expandDisabled:!1,expandOnClick:!1,expanded:[],icons:{check:f.a.createElement("span",{className:"rct-icon rct-icon-check"}),uncheck:f.a.createElement("span",{className:"rct-icon rct-icon-uncheck"}),halfCheck:f.a.createElement("span",{className:"rct-icon rct-icon-half-check"}),expandClose:f.a.createElement("span",{className:"rct-icon rct-icon-expand-close"}),expandOpen:f.a.createElement("span",{className:"rct-icon rct-icon-expand-open"}),expandAll:f.a.createElement("span",{className:"rct-icon rct-icon-expand-all"}),collapseAll:f.a.createElement("span",{className:"rct-icon rct-icon-collapse-all"}),parentClose:f.a.createElement("span",{className:"rct-icon rct-icon-parent-close"}),parentOpen:f.a.createElement("span",{className:"rct-icon rct-icon-parent-open"}),leaf:f.a.createElement("span",{className:"rct-icon rct-icon-leaf"})},id:null,lang:{collapseAll:"Collapse all",expandAll:"Expand all",toggle:"Toggle"},name:void 0,nameAsArray:!1,nativeCheckboxes:!1,noCascade:!1,onlyLeafCheckboxes:!1,optimisticToggle:!0,showExpandAll:!1,showNodeIcon:!0,showNodeTitle:!1,onCheck:function(){},onClick:null,onExpand:function(){}});t.default=ce}])}); \ No newline at end of file diff --git a/lib/react-checkbox-tree.css b/lib/react-checkbox-tree.css new file mode 100644 index 00000000..652c8785 --- /dev/null +++ b/lib/react-checkbox-tree.css @@ -0,0 +1,213 @@ +.react-checkbox-tree { + /* + display: flex; + flex-direction: row-reverse; + */ + font-size: 16px; +} + +.react-checkbox-tree > ol { + -webkit-box-flex: 1; + -ms-flex: 1 1 auto; + flex: 1 1 auto; +} + +.react-checkbox-tree ol { + margin: 0; + padding-left: 0; + list-style-type: none; +} + +.react-checkbox-tree ol ol { + padding-left: 24px; +} + +.react-checkbox-tree button { + line-height: normal; + color: inherit; +} + +.react-checkbox-tree button:focus { + outline: none; +} + +.react-checkbox-tree button:disabled { + cursor: not-allowed; +} + +.react-checkbox-tree .rct-bare-label { + cursor: default; +} + +.react-checkbox-tree label { + margin-bottom: 0; + cursor: pointer; +} + +.react-checkbox-tree label:hover { + background: rgba(51, 51, 204, 0.1); +} + +.react-checkbox-tree label:active { + background: rgba(51, 51, 204, 0.15); +} + +.react-checkbox-tree:not(.rct-native-display) input { + display: none; +} + +.react-checkbox-tree.rct-native-display input { + margin: 0 5px; +} + +.react-checkbox-tree .rct-icon { + font-family: "FontAwesome"; + font-style: normal; +} + +.rct-disabled > .rct-text > label { + opacity: .75; + cursor: not-allowed; +} + +.rct-disabled > .rct-text > label:hover { + background: transparent; +} + +.rct-disabled > .rct-text > label:active { + background: transparent; +} + +.rct-text { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.rct-options { + -webkit-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + margin-left: .5rem; + text-align: right; +} + +.rct-option { + opacity: .75; + border: 0; + background: none; + cursor: pointer; + padding: 0 4px; + font-size: 18px; +} + +.rct-option:hover { + opacity: 1; +} + +.rct-option + .rct-option { + margin-left: 2px; +} + +.rct-collapse, +.rct-checkbox, +.rct-node-icon { + padding: 0 5px; +} + +.rct-collapse *, +.rct-checkbox *, +.rct-node-icon * { + display: inline-block; + margin: 0; + width: 14px; +} + +.rct-collapse { + border: 0; + background: none; + line-height: normal; + color: inherit; + font-size: 12px; +} + +.rct-collapse.rct-collapse-btn { + cursor: pointer; +} + +.rct-collapse > .rct-icon-expand-close { + opacity: .5; +} + +.rct-collapse > .rct-icon-expand-close:hover { + opacity: 1; +} + +.rct-native-display .rct-checkbox { + display: none; +} + +.rct-node-clickable { + cursor: pointer; +} + +.rct-node-clickable:hover { + background: rgba(51, 51, 204, 0.1); +} + +.rct-node-clickable:focus { + outline: 0; + background: rgba(51, 51, 204, 0.2); +} + +.rct-node-icon { + color: #33c; +} + +.rct-title { + padding: 0 5px; +} + +.rct-icon-expand-close::before { + content: "\f054"; +} + +.rct-icon-expand-open::before { + content: "\f078"; +} + +.rct-icon-uncheck::before { + content: "\f096"; +} + +.rct-icon-check::before { + content: "\f046"; +} + +.rct-icon-half-check::before { + opacity: .5; + content: "\f046"; +} + +.rct-icon-leaf::before { + content: "\f016"; +} + +.rct-icon-parent-open::before { + content: "\f115"; +} + +.rct-icon-parent-close::before { + content: "\f114"; +} + +.rct-icon-expand-all::before { + content: "\f0fe"; +} + +.rct-icon-collapse-all::before { + content: "\f146"; +} diff --git a/src/js/CheckboxTree.js b/src/js/CheckboxTree.js index 21f52144..fe7e1a61 100644 --- a/src/js/CheckboxTree.js +++ b/src/js/CheckboxTree.js @@ -12,10 +12,25 @@ import languageShape from './shapes/languageShape'; import listShape from './shapes/listShape'; import nodeShape from './shapes/nodeShape'; +const SUPPORTED_KEYS = [ + 'ArrowUp', + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight', + 'End', + 'Home', + 'Enter', + ' ', +]; + +// Clamp a number so that it is within the range [min, max] +const clamp = (n, min, max) => Math.min(Math.max(n, min), max); + class CheckboxTree extends React.Component { static propTypes = { nodes: PropTypes.arrayOf(nodeShape).isRequired, + 'aria-label': PropTypes.string, checked: listShape, disabled: PropTypes.bool, expandDisabled: PropTypes.bool, @@ -39,6 +54,7 @@ class CheckboxTree extends React.Component { }; static defaultProps = { + 'aria-label': null, checked: [], disabled: false, expandDisabled: false, @@ -87,6 +103,7 @@ class CheckboxTree extends React.Component { }); this.state = { + focusedNodeIndex: null, id: props.id || `rct-${nanoid(7)}`, model, prevProps: props, @@ -97,6 +114,8 @@ class CheckboxTree extends React.Component { this.onNodeClick = this.onNodeClick.bind(this); this.onExpandAll = this.onExpandAll.bind(this); this.onCollapseAll = this.onCollapseAll.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); } // eslint-disable-next-line react/sort-comp @@ -158,6 +177,92 @@ class CheckboxTree extends React.Component { this.expandAllNodes(false); } + onFocus() { + const isFirstFocus = this.state.focusedNodeIndex === null; + if (isFirstFocus) { + this.setState({ focusedNodeIndex: 0 }); + } + } + + onKeyDown(e) { + const keyEligibleForFirstLetterNavigation = e.key.length === 1 && + !e.ctrlKey && !e.metaKey && !e.altKey; + // abort early so that we don't try to intercept common browser keystrokes like alt+d + if (!SUPPORTED_KEYS.includes(e.key) && !keyEligibleForFirstLetterNavigation) { + return; + } + + const { focusedNodeIndex, model } = this.state; + const currentlyFocusedNode = model.getNode(this.visibleNodes[focusedNodeIndex || 0]); + let newFocusedNodeIndex = focusedNodeIndex || 0; + const isExpandingEnabled = !this.props.expandDisabled && !this.props.disabled; + + e.preventDefault(); // disable built-in scrolling + switch (e.key) { + case 'ArrowDown': + newFocusedNodeIndex += 1; + break; + case 'ArrowUp': + newFocusedNodeIndex -= 1; + break; + case 'Home': + newFocusedNodeIndex = 0; + break; + case 'End': + newFocusedNodeIndex = this.visibleNodes.length - 1; + break; + case 'ArrowRight': + if (currentlyFocusedNode && currentlyFocusedNode.isParent) { + if (currentlyFocusedNode.expanded) { + // we can increment focused index to get the first child + // because visibleNodes is an pre-order traversal of the tree + newFocusedNodeIndex += 1; + } else if (isExpandingEnabled) { + // expand the currently focused node + this.onExpand({ value: currentlyFocusedNode.value, expanded: true }); + } + } + break; + case 'ArrowLeft': + if (!currentlyFocusedNode) { + return; + } + if (currentlyFocusedNode.isParent && currentlyFocusedNode.expanded && + isExpandingEnabled) { + // collapse the currently focused node + this.onExpand({ value: currentlyFocusedNode.value, expanded: false }); + } else { + // Move focus to the parent of the current node, if any + // parent is the first element to the left of the currently focused element + // with a lower tree depth since visibleNodes is an pre-order traversal + const parent = this.visibleNodes.slice(0, focusedNodeIndex) + .reverse() + .find(val => model.getNode(val).treeDepth < currentlyFocusedNode.treeDepth); + if (parent) { + newFocusedNodeIndex = this.visibleNodes.indexOf(parent); + } + } + break; + default: + if (keyEligibleForFirstLetterNavigation) { + const next = this.visibleNodes.slice((focusedNodeIndex || 0) + 1) + .find((val) => { + const { label } = model.getNode(val); + // for now, we only support first-letter nav to + // nodes with string labels, not jsx elements + return label.startsWith ? label.startsWith(e.key) : false; + }); + if (next) { + newFocusedNodeIndex = this.visibleNodes.indexOf(next); + } + } + break; + } + + newFocusedNodeIndex = clamp(newFocusedNodeIndex, 0, this.visibleNodes.length - 1); + this.setState({ focusedNodeIndex: newFocusedNodeIndex }); + } + expandAllNodes(expand = true) { const { onExpand } = this.props; @@ -207,10 +312,15 @@ class CheckboxTree extends React.Component { showNodeTitle, showNodeIcon, } = this.props; - const { id, model } = this.state; + const { focusedNodeIndex, id, model } = this.state; const { icons: defaultIcons } = CheckboxTree.defaultProps; const treeNodes = nodes.map((node) => { + const parentExpanded = parent.value ? model.getNode(parent.value).expanded : true; + if (parentExpanded) { + // visible only if parent is expanded or if there is no root parent + this.visibleNodes.push(node.value); + } const key = node.value; const flatNode = model.getNode(node.value); const children = flatNode.isParent ? this.renderTreeNodes(node.children, node) : null; @@ -224,8 +334,6 @@ class CheckboxTree extends React.Component { const showCheckbox = onlyLeafCheckboxes ? flatNode.isLeaf : flatNode.showCheckbox; // Render only if parent is expanded or if there is no root parent - const parentExpanded = parent.value ? model.getNode(parent.value).expanded : true; - if (!parentExpanded) { return null; } @@ -239,6 +347,7 @@ class CheckboxTree extends React.Component { expandDisabled={expandDisabled} expandOnClick={expandOnClick} expanded={flatNode.expanded} + hasFocus={this.visibleNodes[focusedNodeIndex] === node.value} icon={node.icon} icons={{ ...defaultIcons, ...icons }} label={node.label} @@ -261,7 +370,7 @@ class CheckboxTree extends React.Component { }); return ( -
    +
      {treeNodes}
    ); @@ -327,6 +436,9 @@ class CheckboxTree extends React.Component { render() { const { disabled, nodes, nativeCheckboxes } = this.props; + const { focusedNodeIndex } = this.state; + const isFirstFocus = focusedNodeIndex === null; + this.visibleNodes = []; // an pre-order traversal of the tree for keyboard support const treeNodes = this.renderTreeNodes(nodes); const className = classNames({ @@ -339,7 +451,16 @@ class CheckboxTree extends React.Component {
    {this.renderExpandAll()} {this.renderHiddenInput()} - {treeNodes} +
    + {treeNodes} +
    ); } diff --git a/src/js/NativeCheckbox.js b/src/js/NativeCheckbox.js index 3a78561c..f6563300 100644 --- a/src/js/NativeCheckbox.js +++ b/src/js/NativeCheckbox.js @@ -30,7 +30,10 @@ class NativeCheckbox extends React.PureComponent { // Remove property that does not exist in HTML delete props.indeterminate; - return { this.checkbox = c; }} type="checkbox" />; + // Since we already implement space toggling selection, + // the native checkbox no longer needs to be in the accessibility tree and in tab order + // I.e, this is purely for visual rendering + return { this.checkbox = c; }} type="checkbox" aria-hidden tabIndex={-1} />; } } diff --git a/src/js/TreeNode.js b/src/js/TreeNode.js index 71ba66d7..7d2e1fd8 100644 --- a/src/js/TreeNode.js +++ b/src/js/TreeNode.js @@ -13,6 +13,7 @@ class TreeNode extends React.Component { disabled: PropTypes.bool.isRequired, expandDisabled: PropTypes.bool.isRequired, expanded: PropTypes.bool.isRequired, + hasFocus: PropTypes.bool.isRequired, icons: iconsShape.isRequired, isLeaf: PropTypes.bool.isRequired, isParent: PropTypes.bool.isRequired, @@ -50,11 +51,23 @@ class TreeNode extends React.Component { constructor(props) { super(props); + this.nodeRef = React.createRef(); + + this.componentDidUpdate = this.componentDidUpdate.bind(this); this.onCheck = this.onCheck.bind(this); this.onClick = this.onClick.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); this.onExpand = this.onExpand.bind(this); } + componentDidUpdate(prevProps) { + // Move focus for keyboard users + const isReceivingFocus = this.props.hasFocus && !prevProps.hasFocus; + if (isReceivingFocus) { + this.nodeRef.current.focus(); + } + } + onCheck() { const { value, onCheck } = this.props; @@ -77,6 +90,16 @@ class TreeNode extends React.Component { onClick({ value, checked: this.getCheckState({ toggle: false }) }); } + onKeyDown(e) { + if (e.key === ' ') { + e.preventDefault(); // prevent scrolling + e.stopPropagation(); // prevent parent nodes from toggling their checked state + if (!this.props.disabled) { + this.onCheck(); + } + } + } + onExpand() { const { expanded, value, onExpand } = this.props; @@ -117,10 +140,13 @@ class TreeNode extends React.Component { return ( @@ -178,21 +204,24 @@ class TreeNode extends React.Component { const { onClick, title } = this.props; const clickable = onClick !== null; + // Disable the lints about this control not being accessible + // We already provide full keyboard control, so this is clickable for mouse users + // eslint-disable-next-line max-len + /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ return ( {clickable ? ( {children} ) : children} ); + // eslint-disable-next-line max-len + /* eslint-enable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ } renderCheckboxLabel(children) { @@ -226,13 +255,13 @@ class TreeNode extends React.Component { if (clickable) { render.push(( + // We can disable the lint here, since keyboard functionality is already provided + // eslint-disable-next-line jsx-a11y/no-static-element-interactions {children} @@ -267,11 +296,18 @@ class TreeNode extends React.Component { return null; } - return this.props.children; + return this.props.isParent ? ( +
    + {this.props.children} +
    + ) : ( + this.props.children + ); } render() { const { + checked, className, disabled, expanded, @@ -285,9 +321,22 @@ class TreeNode extends React.Component { 'rct-node-collapsed': !isLeaf && !expanded, 'rct-disabled': disabled, }, className); + let ariaChecked = checked === 1 ? 'true' : 'false'; + if (checked === 2) { + ariaChecked = 'mixed'; + } return ( -
  1. +
  2. {this.renderCollapseButton()} {this.renderLabel()} diff --git a/src/scss/react-checkbox-tree.scss b/src/scss/react-checkbox-tree.scss index 39ff93f0..2bfa6323 100644 --- a/src/scss/react-checkbox-tree.scss +++ b/src/scss/react-checkbox-tree.scss @@ -5,8 +5,12 @@ $rct-clickable-hover: rgba($rct-icon-color, .1) !default; $rct-clickable-focus: rgba($rct-icon-color, .2) !default; .react-checkbox-tree { + // Unsure why these 2 lines cause the tree items to not render visually, once I added a div with role="tree" to wrap the tree + // would appreciate help to fix + /* display: flex; flex-direction: row-reverse; + */ font-size: 16px; > ol { diff --git a/test/CheckboxTree.js b/test/CheckboxTree.js index 17ba31a2..8f98cd6e 100644 --- a/test/CheckboxTree.js +++ b/test/CheckboxTree.js @@ -178,7 +178,7 @@ describe('', () => { assert.deepEqual( wrapper.find(TreeNode).prop('children').props, - { children: [null, null] }, + { children: [null, null], role: 'presentation' }, ); }); });