diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d47f36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bower_components* +bower-* diff --git a/bower.json b/bower.json index a36fab4..97090ca 100644 --- a/bower.json +++ b/bower.json @@ -2,15 +2,38 @@ "name": "hc-tree-view", "description": "An optimized tree-view based on a flat data structure.", "main": "hc-tree-view.html", + "version": "v0.5.0", "dependencies": { - "polymer": "Polymer/polymer#^1.7.0", - "iron-list": "admwx7/iron-list#fix/issue-300", - "paper-tristate-checkbox": "Hackception/paper-tristate-checkbox#^0.2.0" + "polymer": "Polymer/polymer#^1.9 || ^2.0", + "iron-list": "admwx7/iron-list#fix/data-propagation", + "paper-tristate-checkbox": "Hackception/paper-tristate-checkbox#^0.3.0" }, "devDependencies": { - "iron-component-page": "PolymerElements/iron-component-page#^1.0.0", - "iron-demo-helpers": "PolymerElements/iron-demo-helpers#^1.0.0", - "web-component-tester": "^4.0.0", - "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0" + "iron-component-page": "PolymerElements/iron-component-page#^1.0 || ^2.0", + "iron-demo-helpers": "PolymerElements/iron-demo-helpers#^1.0 || ^2.0", + "web-component-tester": "Polymer/web-component-tester#^6.0.0", + "webcomponentsjs": "webcomponents/webcomponentsjs#^1.0 || ^2.0" + }, + "variants": { + "1.x": { + "dependencies": { + "polymer": "Polymer/polymer#^1.9", + "iron-list": "admwx7/iron-list#fix/issue-300", + "paper-tristate-checkbox": "Hackception/paper-tristate-checkbox#^0.2.0" + }, + "devDependencies": { + "iron-component-page": "PolymerElements/iron-component-page#^1.0.0", + "iron-demo-helpers": "PolymerElements/iron-demo-helpers#^1.0.0", + "web-component-tester": "Polymer/web-component-tester#^4.0.0", + "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0" + }, + "resolutions": { + "iron-list": "fix/issue-300", + "webcomponentsjs": "^0.7.0" + } + } + }, + "resolutions": { + "webcomponentsjs": "^1.0.0" } } diff --git a/demo/hc-nested-list.html b/demo/hc-nested-list.html index f4b2e89..1882529 100644 --- a/demo/hc-nested-list.html +++ b/demo/hc-nested-list.html @@ -62,7 +62,7 @@

hc-nested-list demo

items: { type: Array, // This function is provided as part of test/utils.html, used for testing - value: () => buildItems(100, 100, 100) + value: () => buildItems(10, 10, 10) } } }); diff --git a/demo/index.html b/demo/index.html index 428b33d..3043de2 100644 --- a/demo/index.html +++ b/demo/index.html @@ -38,39 +38,43 @@

Basic hc-tree-view demo

> diff --git a/hc-nested-list-behavior.html b/hc-nested-list-behavior.html index ba3f66f..ed75a55 100644 --- a/hc-nested-list-behavior.html +++ b/hc-nested-list-behavior.html @@ -17,17 +17,17 @@ isLoading: { type: Boolean, value: false, - notify: true + notify: true, }, /* The property name for the id on each item. */ itemIdName: { type: String, - value: 'id' + value: 'id', }, /* The property name for the parentId on each item. */ itemParentIdName: { type: String, - value: 'parentId' + value: 'parentId', }, /** * The flat array of items to build the nested list structure from, requires an id and @@ -44,7 +44,7 @@ */ parts: { type: Object, - notify: true + notify: true, }, /** * The currently visible subset of items from the items property @@ -54,12 +54,25 @@ _items: { type: Array, readOnly: true, - computed: '_computeItems(parts)' + computed: '_computeItems(parts)', + observer: '__updateList', + }, + }, + observers: [ + '_computeParts(items, itemIdName, itemParentIdName)', + ], + ready() { + this.itemOpenChanged = this.itemOpenChanged.bind(this); + }, + attached() { + this.addEventListener('item-open-changed', this.itemOpenChanged); + this.$.list._propagateUpdates = (item, instance) => { + instance.notifyPath('item.state', item.state); + instance.notifyPath('item.open', item.open); } }, - observers: ['_computeParts(items, itemIdName, itemParentIdName)'], - listeners: { - 'item-open-changed': 'itemOpenChanged' + detached() { + this.removeEventListener('item-open-changed', this.itemOpenChanged); }, /** * Called when an item's open status is changed. Update parts with children if lazy-loading then call update @@ -110,12 +123,14 @@ * @param {Object} detail * @param {String} [idName=this.itemIdName] * @param {Object} [parts = this.parts] + * @return {Promise} */ - itemOpenChanged(event, detail, idName = this.itemIdName, parts = this.parts) { + itemOpenChanged(event, idName = this.itemIdName, parts = this.parts) { // Block interactions while loading items if (this.isLoading) return; this.isLoading = true; + const detail = event.detail; const items = parts[this._getParentId(detail.item)]; const id = detail.item[idName]; let item; @@ -132,7 +147,7 @@ } else items[i].open = false; // If it's not the item we want, ensure it's closed } - (new Promise((res) => { + return (new Promise((res) => { if (item) { // fetch children and update when done, when there's an item const ids = Object.keys(parts); @@ -147,7 +162,7 @@ res(parts); }); } else res(parts); - })).then(() => { + })).then((parts) => { this.set('parts', Object.assign({}, parts)); this.isLoading = false; }); @@ -161,7 +176,7 @@ * @param {Object} parts * @param {Object[]} [items=parts.root] * @param {String} [idName=this.itemIdName] - * @returns {Object[]} + * @return {Object[]} */ _computeItems(parts, items = parts.root, idName = this.itemIdName) { // Can't calculate items without parts or the name of the id property. @@ -216,7 +231,7 @@ * @param {Object} item * @param {Object} parts * @param {String} [parentId = this._getParentId(item)] - * @returns {Number|undefined} + * @return {Number|undefined} */ _getItemDepth(item, parts, parentId = this._getParentId(item)) { const parent = this.getParent(item, parts); @@ -241,7 +256,7 @@ * @protected * @param {Object} item * @param {String} [parentIdName=this.itemParentIdName] - * @returns {String|Number} + * @return {String|Number} */ _getParentId(item, parentIdName = this.itemParentIdName) { return (item[parentIdName] || item[parentIdName] === 0) ? @@ -262,9 +277,27 @@ return Object.assign({ open: false, state: parentState, - depth: this._getItemDepth(item, parts, parentId) + depth: this._getItemDepth(item, parts, parentId), }, item); - } + }, + /** + * This is a hack to make iron-list hide it's additional physical items since that's not actually part of it's + * functionality. + * @private + */ + __updateList() { + this.debounce('parts-updating', () => { + const list = this.$.list; + const lastIndex = this._items.length - 1; + (list._physicalItems || []).forEach((item, index) => { + if (index > lastIndex) { + item.setAttribute('hidden', ''); + } else { + item.removeAttribute('hidden'); + } + }); + }, 100); + }, }; /** diff --git a/hc-nested-list-item.html b/hc-nested-list-item.html index cefba90..7cdae2a 100644 --- a/hc-nested-list-item.html +++ b/hc-nested-list-item.html @@ -31,10 +31,16 @@ is: 'hc-nested-list-item', properties: { /* The item holding all data */ - item: Object + item: Object, }, - listeners: { - tap: 'fireOpenEvent' + ready() { + this.fireOpenEvent = this.fireOpenEvent.bind(this); + }, + attached() { + this.addEventListener('click', this.fireOpenEvent); + }, + detached() { + this.removeEventListener('click', this.fireOpenEvent); }, /** * Fires the toggle event required by hc-nested-list to open/close content sections. @@ -47,11 +53,16 @@ event.stopPropagation(); // Trigger an event to toggle open state instead. - this.fire('item-open-changed', { - item: this.item, - open: !this.item.open - }); - } + this.dispatchEvent(new CustomEvent('item-open-changed', { + detail: { + item: this.item, + open: !this.item.open, + }, + bubbles: true, + composed: true, + cancelable: true, + })); + }, }); diff --git a/hc-nested-list.html b/hc-nested-list.html index 2ddc9d3..06f0e78 100644 --- a/hc-nested-list.html +++ b/hc-nested-list.html @@ -1,6 +1,6 @@ - + +
+ +
-
- -
+ as="item">
@@ -57,7 +57,7 @@ Polymer({ /* global hc */ is: 'hc-nested-list', - behaviors: [hc.NestedListBehavior] + behaviors: [hc.NestedListBehavior], }); diff --git a/hc-tree-node.html b/hc-tree-node.html index 6632e7a..10100ba 100644 --- a/hc-tree-node.html +++ b/hc-tree-node.html @@ -71,7 +71,8 @@ checked="[[checked]]" unchecked="[[unchecked]]" indeterminate="[[indeterminate]]" - on-state-changed="_stateChanged" + on-click="__userInteraction" + disable-indeterminate > @@ -87,44 +88,35 @@ /* The value to use when state="on" */ checked: { type: Boolean, - value: true + value: true, }, /* The icon to use when the item is closed */ closedIcon: { type: String, - value: 'icons:chevron-right' + value: 'icons:chevron-right', }, /* The value to use when state=null */ indeterminate: { type: Boolean, - value: false + value: false, }, /* This node's data */ item: Object, /* The icon to use when the item is open */ openIcon: { type: String, - value: 'icons:expand-more' + value: 'icons:expand-more', }, /* The value to use when state="off" */ unchecked: { type: Boolean, - value: false + value: false, }, /* Holds the value that determines selected status for the item */ value: { type: Boolean, - observer: '_valueChanged' + observer: '_valueChanged', }, - /** - * Used to determine when this element is in initialization state to prevent extra events - * - * @private - */ - __init: { - type: Boolean, - value: true - } }, observers: ['_updateDepth(item.depth)'], ready() { @@ -133,70 +125,81 @@ * and to prevent event bubbling. This is using an annonymous function to keep the correct scope * that is provided by the checkbox element instead of an arrow function which will use the local * scope. - * * @protected + * * @param {Event} event */ - this.$.checkbox._regularTap = function (event) { - event.stopPropagation(); + this.$.checkbox._regularTap = function(event) { this.state = this.state === 'on' ? 'off' : 'on'; }; }, /** * Returns the openIcon when open is true, otherwise it returns the closedIcon. + * @protected * - * @private * @param {Boolean} open - * @returns {String} + * @return {String} */ _getIcon(open) { return open ? this.openIcon : this.closedIcon; }, /** - * Watches for changes to the value for the checkbox then notifies the parent of selection changes. + * Updates the depth of this node to display as if it is nested + * @protected * - * @private - * @fires hc-tree-view-behavior#item-state-changed - * @param {CustomEvent} event - * @param {Object} detail + * @param {Number} depth */ - _stateChanged(event, detail) { - // Prevent firing events if we don't have the necessary data - if (!this.item) return; - - // When a change event happens, inform the list - this.fire('item-state-changed', { - item: this.item, - state: detail.value - }); - }, _updateDepth(depth) { this.updateStyles({ - '--hc-tree-node-depth': String(depth) + '--hc-tree-node-depth': String(depth), }); }, /** * Watches for changes to the value for the checkbox then notifies the parent of selection changes. + * @protected * - * @private * @fires hc-tree-view-behavior#item-selected-changed * @param {Boolean} value */ _valueChanged(value) { - // Prevent firing events if we don't have the necessary data + // Prevent firing events if we don't have the necessary data or if it's still initializing if (!this.item) return; - // Prevent the initial event that happens upon loading - else if (this.__init) { - this.__init = false; + if (!this.__initComplete) { + this.__initComplete = true; return; } - // When a change event happens, inform the list - this.fire('item-selected-changed', { - item: this.item, - selected: value - }); - } + this.dispatchEvent(new CustomEvent('item-selected-changed', { + detail: { + item: this.item, + selected: value, + state: this.item.state, + }, + bubbles: true, + composed: true, + })); + }, + /** + * Used to trigger interactions with the tree based on the user. + * @private + * @fires hc-tree-view-behavior#item-state-changed + * + * @param {ClickEvent} event + */ + __userInteraction(event) { + event.stopPropagation(); + // Resort to the 1.x methods + if (!Polymer.element) { + this.dispatchEvent(new CustomEvent('item-state-changed', { + detail: { + item: this.item, + state: this.item.state, + }, + bubbles: true, + composed: true, + })); + } + }, }); diff --git a/hc-tree-view-behavior.html b/hc-tree-view-behavior.html index 243fdc6..ab4299a 100644 --- a/hc-tree-view-behavior.html +++ b/hc-tree-view-behavior.html @@ -16,31 +16,44 @@ /* Used to allow changing what the tree node's id is */ itemIdName: { type: String, - value: 'id' + value: 'id', }, /* Used to allow changing what the tree node's parent id is */ itemParentIdName: { type: String, - value: 'parentId' + value: 'parentId', }, /* The list of items to display in the tree, using a flat structure */ items: Array, + /* The hc-nested-list element to use for rendering */ + list: { + type: Object, + notify: true, + }, /* The list of all selected items without duplicates */ selectedItems: { type: Array, value: () => [], - notify: true + notify: true, }, /** * The parts object used by the list element * * @private */ - _parts: Object + _parts: Object, + }, + ready() { + this.itemSelectedChanged = this.itemSelectedChanged.bind(this); + this.itemStateChanged = this.itemStateChanged.bind(this); + }, + attached() { + this.addEventListener('item-selected-changed', this.itemSelectedChanged); + this.addEventListener('item-state-changed', this.itemStateChanged); }, - listeners: { - 'item-selected-changed': 'itemSelectedChanged', - 'item-state-changed': 'itemStateChanged' + detached() { + this.removeEventListener('item-selected-changed', this.itemSelectedChanged); + this.removeEventListener('item-state-changed', this.itemStateChanged); }, /** * Collapses the tree while maintaining the current selection states @@ -70,74 +83,95 @@ }); }, /** - * Takes an array of one or more items and removes them from the selectedItems array. + * Takes an array of one or more items and removes them from the selectedItems array. If no items are passed + * then all items will be deslected. * - * @param {Array.} [items = this.selectedItems] + * @param {Array.} [items] * @param {Boolean} [preventUpdate] - blocks the call to _updateState to block template updating */ - deselectItems(items = this.selectedItems, preventUpdate) { - let selectedItems = []; - - const getNodeId = (i) => i[this.itemIdName]; - - // Map to Ids for perf - selectedItems = this.selectedItems.map(getNodeId); - const passedItems = items.map(getNodeId); - - // Filter by Ids for perf - selectedItems = selectedItems.filter((i) => passedItems.indexOf(i) === -1); + deselectItems(items, preventUpdate) { + if (!items) { + items = this.selectedItems; + this.set('selectedItems', []); + } else { + const getNodeId = (i) => i[this.itemIdName]; + const itemsToUpdate = []; - // Update selectedItems if there was an item removed - if (this.selectedItems.length !== selectedItems.length) { - const getItemById = (id) => this.selectedItems.find((i) => getNodeId(i) === id); - - // Re-map ids to items if not empty - if (selectedItems.length > 0) selectedItems = selectedItems.map(getItemById); + items.forEach((item) => { + const index = this.selectedItems.findIndex((i) => { + const match = getNodeId(i) === getNodeId(item); + if (match) { + itemsToUpdate.push(i); + } + return match; + }); + if (index >= 0) { + this.selectedItems.splice(index, 1); + } + }); - this.set('selectedItems', selectedItems); - if (!preventUpdate) this._updateState(items, 'off'); + items = itemsToUpdate; + if (items.length) { + this.set('selectedItems', Object.assign([], this.selectedItems)); + } } + if (!preventUpdate) this._updateState(items, 'off'); }, /** * Fetches the parent for the given item. * * @param {Object} item - * @returns {Object|undefined} + * @return {Object|undefined} */ getParent(item) { - return this.$.list.getParent(item); + return this.list.getParent(item); }, /** * Makes calls to select or deselect the item based on the selected in the event. * * @listens hc-tree-view-behavior#item-selected-changed - * @param {CustomEvent} [event] - * @param {Object} detail + * @param {CustomEvent} event */ - itemSelectedChanged(event, detail) { - this[detail.selected ? 'selectItems' : 'deselectItems']([detail.item], true); + itemSelectedChanged({detail}) { + if (detail.selected) { + this.selectItems([detail.item], true); + } else { + this.deselectItems([detail.item], true); + } + if (detail.state !== undefined && Polymer.Element) { + this._updateState([detail.item], detail.state) + } }, /** * Updates the parents and children of this item that it's state was changed, update thiers as well if needed. * * @listens hc-tree-view-behavior#item-state-changed * @param {CustomEvent} event - * @param {Object} detail */ - itemStateChanged(event, detail) { + itemStateChanged({detail}) { const {item, state} = detail; - // Reject duplicated events - if (item.state === state) return; - item.state = state; + if (item.state !== state) { + item.state = state; + } // Change selected status for parents up the chain const parent = this._updateParents(item, state); - if (parent) - this[parent.state === 'on' ? 'selectItems' : 'deselectItems']([parent], true); + if (parent) { + if (parent.state === 'on') { + this.selectItems([parent], true); + } else { + this.deselectItems([parent], true); + } + } // Change selected status for children, even if they are hidden. - this[state === 'on' ? 'selectItems' : 'deselectItems'](this._updateChildren(item, state), true); + if (state === 'on') { + this.selectItems(this._updateChildren(item, state), true); + } else { + this.deselectItems(this._updateChildren(item, state), true); + } + this.debounce('itemStateChanged', () => { this.set('_parts', Object.assign({}, this._parts)); }); @@ -173,7 +207,7 @@ * @param {Object} item * @param {Boolean} state * @param {Object} [parts = this._parts] - * @returns {Array.} + * @return {Array.} */ _updateChildren(item, state, parts = this._parts) { const children = parts[item[this.itemIdName]]; @@ -227,26 +261,33 @@ * @private * @param {Array.} items * @param {String} state - * @param {Object} [parts = this._parts] */ - _updateState(items, state, parts = this._parts) { - if (!items || !parts) return; - // Filter items to remove children that have a parent in the array - items.filter((item) => !items.some((i) => i[this.itemIdName] === item[this.itemParentIdName])). + _updateState(items, state) { + if (!items || !this._parts) return; + this.debounce('_updateState', () => { + // Filter items to remove children that have a parent in the array + items = items.filter((item) => { + item.state = state; + return !items.some((i) => i[this.itemIdName] === item[this.itemParentIdName]); + }); // Pass the filtered items into _updateChildren - forEach((item) => { + items.forEach((item) => { // Find the item in the parts object - const parentId = this.$.list._getParentId(item); - const partItem = parts[parentId].find((i) => i[this.itemIdName] === item[this.itemIdName]); + const parentId = this.list._getParentId(item); + const partItem = this._parts[parentId].find((i) => { + return i[this.itemIdName] === item[this.itemIdName]; + }); // Trigger a state change that will propagate - this.itemStateChanged(null, { - item: partItem, - state + this.itemStateChanged({ + detail: { + item: partItem, + state, + }, }); }); - } + }); + }, }; - /** * Event for changing the selection status of a tree node * @@ -254,13 +295,15 @@ * @type {Object} * @property {Object} item - the item to change selection status for * @property {Boolean} selection - the updated selection status + * @property {String} [state] - triggers a state update, can be 'on', 'off', or null */ /** * Event for changing the state of a tree node * + * @deprecated * @event hc-tree-view-behavior#item-state-changed * @type {Object} * @property {Object} item - the item to change state for - * @property {Boolean} state - the updated state + * @property {String} state - the updated state, can be 'on', 'off', or null */ diff --git a/hc-tree-view.html b/hc-tree-view.html index eb6d14d..42bf056 100644 --- a/hc-tree-view.html +++ b/hc-tree-view.html @@ -27,29 +27,77 @@ .item { @apply --hc-tree-view-item; } + [hidden] { + display: none; + } - - - + + + diff --git a/test/hc-nested-list-item_test.html b/test/hc-nested-list-item_test.html index 7614e1b..45c3f92 100644 --- a/test/hc-nested-list-item_test.html +++ b/test/hc-nested-list-item_test.html @@ -36,7 +36,7 @@ element.item = {test: 'test'}; event.stopPropagation = sinon.spy(); element.set = sinon.spy(); - fireSpy = sinon.spy(element, 'fire'); + fireSpy = sinon.spy(element, 'dispatchEvent'); }); it('should block event propagation', () => { @@ -45,12 +45,22 @@ }); it('should fire a item-open-changed event providing the item data', () => { element.fireOpenEvent(event); - expect(fireSpy.called).to.equal(true); - expect(fireSpy.calledWithMatch('item-open-changed', {item: element.item, open: !element.item.open})). - to.equal(true); + const e = new CustomEvent('item-open-changed', { + detail: { + item: element.item, + open: !element.item.open, + }, + bubbles: true, + composed: true, + cancelable: true, + }); + expect(fireSpy.calledWithMatch(e)).to.equal(true); }); it('should be bound to a click event', done => { element.fireOpenEvent = sinon.spy(); + const parent = element.parentNode; + parent.removeChild(element); + parent.appendChild(element); element.querySelector('#header').click(); flush(() => { diff --git a/test/hc-nested-list_test.html b/test/hc-nested-list_test.html index 61e2736..27ae94c 100644 --- a/test/hc-nested-list_test.html +++ b/test/hc-nested-list_test.html @@ -66,35 +66,35 @@ it('should return the parent from parts if it exists', () => expect(element.getParent(parts[parts.root[0].id][0])).to.deep.equal(parts.root[0])); }); - describe('itemOpenChanged(event, detail, idName)', () => { + describe('itemOpenChanged(event, idName, parts)', () => { const event = {}; let defaultPartSpy, fetchChildrenSpy; beforeEach(() => { event.detail = { item: Object.assign({}, parts.root[0]), - open: true + open: true, }; defaultPartSpy = sinon.spy(element, '__setDefaultPart'); fetchChildrenSpy = sinon.spy(element, 'fetchChildren'); - element._computeItems = () => {}; + element._computeItems = sinon.spy(); }); - it('should update the open state for the item', (done) => { + it('should update the open state for the item', () => { element.parts = parts; - element.itemOpenChanged(event, event.detail); - flush(() => { - expect(element.set.calledOnce).to.equal(true); - expect(element.set.calledWith('parts')).to.equal(true); - expect(element.set.args[0][1].root[0].open).to.equal(event.detail.open); - expect(fetchChildrenSpy.calledWithMatch(element.parts.root[0], element.parts)).to.equal(true); - done(); - }); + return element.itemOpenChanged(event).then( + () => { + expect(element.set.calledOnce).to.equal(true); + expect(element.set.calledWith('parts')).to.equal(true); + expect(element.set.args[0][1].root[0].open).to.equal(event.detail.open); + expect(fetchChildrenSpy.calledWithMatch(element.parts.root[0], element.parts)).to.equal(true); + } + ); }); it('should set open to false for all other items', (done) => { parts.root[1].open = true; element.parts = parts; - element.itemOpenChanged(event, event.detail); + element.itemOpenChanged(event); flush(() => { expect(element.set.calledOnce).to.equal(true); expect(element.set.calledWith('parts')).to.equal(true); @@ -105,14 +105,19 @@ }); it('should not run if isLoading is true', () => { element.isLoading = true; - element.itemOpenChanged(event, event.detail); + element.itemOpenChanged(event); expect(fetchChildrenSpy.called).to.equal(false); }); it('should run when the item-open-changed event is fired', (done) => { - element.itemOpenChanged = sinon.spy(); - element.fire('item-open-changed'); + const spy = sinon.spy(element, 'itemOpenChanged'); + const parent = element.parentNode; + parent.removeChild(element); + parent.appendChild(element); + element.isLoading = true; + const event = new CustomEvent('item-open-changed'); + element.dispatchEvent(event); flush(() => { - expect(element.itemOpenChanged.calledOnce).to.equal(true); + expect(spy.calledOnce).to.equal(true); done(); }); }); @@ -128,7 +133,7 @@ update(parts); }; element.parts = parts; - element.itemOpenChanged(event, event.detail); + element.itemOpenChanged(event); expect(defaultPartSpy.calledWithMatch(bar, parts, 'foo', 'off')).to.equal(true); }); it('should inherit on state from opened item', () => { @@ -145,7 +150,7 @@ parts.root[0].state = 'on'; element.parts = parts; - element.itemOpenChanged(event, event.detail); + element.itemOpenChanged(event); expect(defaultPartSpy.calledWithMatch(bar, parts, 'foo', 'on')).to.equal(true); }); }); diff --git a/test/hc-tree-node_test.html b/test/hc-tree-node_test.html index b44a0e7..114b9eb 100644 --- a/test/hc-tree-node_test.html +++ b/test/hc-tree-node_test.html @@ -25,7 +25,7 @@ beforeEach(() => { element = fixture('basic'); - fireSpy = sinon.spy(element, 'fire'); + fireSpy = sinon.spy(element, 'dispatchEvent'); }); // Lifecycle Functions @@ -63,20 +63,6 @@ element.$.checkbox.click(); expect(element.item.state).to.equal('on'); }); - it('should prevent bubbling when _regularTap', done => { - const spy = sinon.spy(); - element.item = { - state: 'off' - }; - element.addEventListener('tap', () => { - spy(); - }); - element.$.checkbox.click(); - flush(() => { - expect(spy.called).to.equal(false); - done(); - }) - }); }); // Private Functions describe('_getIcon(open)', () => { @@ -85,50 +71,63 @@ it('should return closedIcon when open is false', () => expect(element._getIcon(false)).to.equal(element.closedIcon)); }); - describe('_stateChanged(event, detail)', () => { - const event = {}; - - it('should do nothing when there is no item', () => { - element._stateChanged(); - expect(fireSpy.called).to.equal(false); - }); - it('should fire a item-state-changed event', () => { - element.item = {test: 'test'}; - event.detail = { - value: 'off' - }; - element._stateChanged(event, event.detail); - expect(fireSpy.calledWith('item-state-changed', { - item: element.item, - state: event.detail.value - })).to.equal(true); - }); - }); describe('_valueChanged(value)', () => { it('should do nothing when there is no item', () => { element._valueChanged(); expect(fireSpy.called).to.equal(false); }); - it('should reject the first run after item is set, while __init is true', () => { + it('should reject the first run after item is set, while __initComplete is false', () => { element._valueChanged(); expect(fireSpy.called).to.equal(false); - expect(element.__init).to.equal(true); + expect(element.__initComplete).not.to.be.ok; element.item = {test: 'test'}; fireSpy.reset(); element._valueChanged(); - expect(element.__init).to.equal(false); + expect(element.__initComplete).to.be.ok; expect(fireSpy.called).to.equal(false); }); it('should fire the item-selected-changed event', () => { element.item = {test: 'test'}; + const e = new CustomEvent('item-selected-changed', { + detail: { + item: element.item, + selected: true, + }, + bubbles: true, + composed: true, + }); // Call twice since the first one is rejected element._valueChanged(true); element._valueChanged(true); - expect(fireSpy.calledWith('item-selected-changed', { - item: element.item, - selected: true - })).to.equal(true); + expect(fireSpy.calledWith(e)).to.equal(true); + }); + }); + describe('__userInteraction(event)', () => { + let event; + + beforeEach(() => { + element.item = { + state: 'on', + }; + event = new CustomEvent('item-state-changed', { + detail: { + item: element.item, + state: element.item.state, + }, + bubbles: true, + composed: true, + }); + }); + + it('should stop event propagation', () => { + event.stopPropagation = sinon.spy(); + element.__userInteraction(event); + expect(event.stopPropagation).to.have.been.called; + }); + it('should dispatch an item-state-changed event', () => { + element.__userInteraction(event); + expect(fireSpy).to.have.been.calledWithMatch(event); }); }); }); diff --git a/test/hc-tree-view_test.html b/test/hc-tree-view_test.html index f7ba62e..ff688e4 100644 --- a/test/hc-tree-view_test.html +++ b/test/hc-tree-view_test.html @@ -35,6 +35,60 @@ }); // Public Functions + describe('collapse()', () => { + beforeEach(() => { + sinon.spy(element, 'debounce'); + element.items = items; + Object.keys(parts).forEach((k) => { + parts[k].forEach((itemPart) => { + itemPart.open = true; + }); + }); + }); + it('should collapse all nodes and maintain their state', (done) => { + element.collapse(undefined, parts); + expect(element.debounce.callCount).to.equal(1); + flush(() => { + const result = Object.keys(element._parts).some((k) => { + return element._parts[k].some((itemPart) => { + return itemPart.open; + }); + }); + expect(result).to.equal(false); + done(); + }); + }); + it('should not collapse any nodes when empty array is passed', (done) => { + element.collapse([], parts); + expect(element.debounce.callCount).to.equal(0); + flush(() => { + const result = Object.keys(element._parts).every((k) => { + return element._parts[k].every((itemPart) => itemPart.open); + }); + expect(result).to.equal(true); + done(); + }); + }); + it('should collapse the passed items and maintain their state', (done) => { + const item = items[0]; + element.collapse([item], parts); + expect(element.debounce.callCount).to.equal(1); + flush(() => { + const itemId = item.id; + const parentId = item.parentId; + let collapsedResult = ( + !element._parts[itemId].every((i) => i.open) && + !element._parts[parentId].find((i) => i.id === itemId).open + ); + const untouchedResult = Object.keys(element._parts).some((k) => { + return element._parts[k].some((itemPart) => itemPart.open); + }); + expect(collapsedResult).to.equal(true); + expect(untouchedResult).to.equal(true); + done(); + }); + }); + }); describe('deselectItems(items)', () => { beforeEach(() => element._updateState = sinon.spy()); @@ -76,7 +130,7 @@ expect(element.$.list.getParent.calledWithMatch(item)).to.equal(true); }); }); - describe('itemSelectedChanged(event, detail)', () => { + describe('itemSelectedChanged(event)', () => { const item = {id: 'test'}; beforeEach(() => { element.selectItems = sinon.spy(); @@ -84,12 +138,12 @@ }); it('should call selectItems with the item when detail.selected', () => { - element.itemSelectedChanged(null, {item, selected: true}); + element.itemSelectedChanged({detail: { item, selected: true, }}); expect(element.selectItems.calledWithMatch([item], true)).to.equal(true); expect(element.deselectItems.calledWithMatch([item], true)).to.equal(false); }); it('should call deselectItems with the item when detail.selected', () => { - element.itemSelectedChanged(null, {item, selected: false}); + element.itemSelectedChanged({detail: { item, selected: false, }}); expect(element.selectItems.calledWithMatch([item], true)).to.equal(false); expect(element.deselectItems.calledWithMatch([item], true)).to.equal(true); }); @@ -116,34 +170,34 @@ }); it('should update item state', () => { const state = 'on'; - element.itemStateChanged(null, {item, state}); + element.itemStateChanged({ detail: {item, state} }); expect(item.state).to.equal(state); }); it('should call _updateParents', () => { const state = 'on'; - element.itemStateChanged(null, {item, state}); + element.itemStateChanged({ detail: {item, state} }); expect(element._updateParents.calledWithMatch(item, state)).to.equal(true); }); it('should call selectItems when state is changed to "on"', () => { const state = 'on'; - element.itemStateChanged(null, {item, state}); + element.itemStateChanged({ detail: {item, state} }); expect(element.selectItems.calledWithMatch([{id: 'test'}], true)).to.equal(true); }); it('should call deselectItems when state is changed to !"on"', () => { const state = 'off'; item.state = 'on'; - element.itemStateChanged(null, {item, state}); + element.itemStateChanged({ detail: {item, state} }); expect(element.deselectItems.calledWithMatch([{id: 'test'}], true)).to.equal(true); }); it('should debounce', () => { const state = 'on'; - element.itemStateChanged(null, {item, state}); + element.itemStateChanged({ detail: {item, state} }); expect(element.debounce.calledOnce).to.equal(true); }) it('should set _parts', done => { const state = 'on'; element._parts = parts; - element.itemStateChanged(null, {item, state}); + element.itemStateChanged({ detail: {item, state} }); flush(() => { expect(setSpy.calledWithMatch('_parts', parts)).to.equal(true); done(); @@ -163,61 +217,6 @@ expect(setSpy.calledWithMatch('items', items)).to.equal(true); }); }); - describe('collapse()', () => { - beforeEach(() => { - sinon.spy(element, 'debounce'); - element.items = items; - Object.keys(parts).forEach((k) => { - parts[k].forEach((itemPart) => { - itemPart.open = true; - }); - }); - }); - it('should collapse all nodes and maintain their state', (done) => { - element.collapse(undefined, parts); - expect(element.debounce.callCount).to.equal(1); - flush(() => { - const result = Object.keys(element._parts).some((k) => { - return element._parts[k].some((itemPart) => { - console.log(itemPart) - return itemPart.open; - }); - }); - expect(result).to.equal(false); - done(); - }); - }); - it('should not collapse any nodes when empty array is passed', (done) => { - element.collapse([], parts); - expect(element.debounce.callCount).to.equal(0); - flush(() => { - const result = Object.keys(element._parts).every((k) => { - return element._parts[k].every((itemPart) => itemPart.open); - }); - expect(result).to.equal(true); - done(); - }); - }); - it('should collapse the passed items and maintain their state', (done) => { - const item = items[0]; - element.collapse([item], parts); - expect(element.debounce.callCount).to.equal(1); - flush(() => { - const itemId = item.id; - const parentId = item.parentId; - let collapsedResult = ( - !element._parts[itemId].every((i) => i.open) && - !element._parts[parentId].find((i) => i.id === itemId).open - ); - const untouchedResult = Object.keys(element._parts).some((k) => { - return element._parts[k].some((itemPart) => itemPart.open); - }); - expect(collapsedResult).to.equal(true); - expect(untouchedResult).to.equal(true); - done(); - }); - }); - }) describe('selectItems(items)', () => { beforeEach(() => element._updateState = sinon.spy()); @@ -335,14 +334,22 @@ element.$.list.parts = parts; }); - it('should call _updateChildren all items that do not have a parent in items', () => { - const subItems = [parts.root[0], parts[parts.root[0].id][0], parts[parts.root[1].id][0]]; + it('should call itemStateChanged on all of the "root" items', () => { + const subItems = [ + parts.root[0], + parts[parts.root[0].id][0], + parts[parts.root[1].id][0], + ]; element._updateState(subItems, state, parts); expect(element.itemStateChanged.callCount).to.equal(2); - expect(element.itemStateChanged.calledWithMatch(null, {item: subItems[0], state})). - to.equal(true); - expect(element.itemStateChanged.calledWithMatch(null, {item: subItems[2], state})). - to.equal(true); + expect(element.itemStateChanged.calledWithMatch({ detail: { + item: subItems[0], + state, + }})).to.equal(true); + expect(element.itemStateChanged.calledWithMatch({ detail: { + item: subItems[2], + state, + }})).to.equal(true); }); it('should do nothing when items is undefined', () => expect(element._updateState).to.not.throw(TypeError));