diff --git a/README.md b/README.md index 1f4f8b4..dd6ccc4 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Detailed API and example usage can be found in the sample application in tests/d | `Attribute` | `expansion` | `hash` | | component which handles expansion and collapsing for entire list. This component should be wrapped inside component helper. | | `Sub Attribute` | `onExpandAll` | `action closure` | | callback functions user provided to handle all list items collapsing. This is an attribute on frost-list-expansion component.| | `Sub Attribute` | `onCollapseAll` | `action closure` | | callback functions user provided to handle all list items expansion. This is an attribute on frost-list-expansion component. | -| `Attribute` | `sorting` | `hash` | | component which handles expansion and collapsing for entire list. This component should be wrapped inside component helper. | +| `Attribute` | `sorting` | `hash` | | component which handles sorting for the list. This component should be wrapped inside component helper. | | `Sub Attribute` | `activeSorting` | `array` | | Array that specifies the sort order. eg. [{"direction: "asc/desc", "value": }], This is an attribute on frost-list-expansion component.| | `Sub Attribute` | `properties` | `array` | | Array of sortable attributes. eg. [{"label: "foo", "value": "bar"}], This is an attribute on frost-sort component.| | `Sub Attribute` | `onSort` | `action closure` | | callback functions user provided to handle sorting. This is an attribute on frost-sort component.| @@ -67,6 +67,8 @@ Detailed API and example usage can be found in the sample application in tests/d | `Attribute` | `itemDefinitions` | `hash` | | Optional: A set of components that are to be used in the list as the `item` component. Note that this had to be used in conjunction with `componentKeyNamesForTypes` | | `Attribute` | `itemExpansionDefinitions` | `hash` | | Optional: A set of components that are to be used in the list as the `itemExpansion` component. Note that this had to be used in conjunction with `componentKeyNamesForTypes` | | `Attribute` | `disableDeselectAll` | `boolean` | false | Optional: disables deselect all click turning frost list into a multi-select type list | +| `Attribute` | `alwaysExpanded` | `boolean` | false | Optional: all list items will always be expanded. frost-list-expansion and frost-list-item-expansion will not be displayed. | +| `Attribute` | `singleSelection` | `boolean` | false | Optional: disables multiple selection entirely and displays radio buttons instead of checkboxes in frost-list-item-selection to clarify the list's behavior. | ### Infinite scroll diff --git a/addon/components/frost-list-item-content.js b/addon/components/frost-list-item-content.js index 6722a91..1c9c7fb 100644 --- a/addon/components/frost-list-item-content.js +++ b/addon/components/frost-list-item-content.js @@ -1,3 +1,4 @@ +import computed, {readOnly} from 'ember-computed-decorators' import {Component} from 'ember-frost-core' import {PropTypes} from 'ember-prop-types' @@ -27,16 +28,23 @@ export default Component.extend({ itemExpansion: PropTypes.oneOfType([ PropTypes.null, PropTypes.EmberComponent - ]) + ]), + alwaysExpanded: PropTypes.bool, + singleSelection: PropTypes.bool }, getDefaultProps () { return { isAnyItemExpansion: false } - } + }, // == Computed Properties =================================================== + @readOnly + @computed('itemExpansion', 'alwaysExpanded') + isExpansionIconVisible (itemExpansion, alwaysExpanded) { + return itemExpansion && !alwaysExpanded + } // == Functions ============================================================= diff --git a/addon/components/frost-list-item-selection.js b/addon/components/frost-list-item-selection.js index 445ba4c..da4d7f4 100644 --- a/addon/components/frost-list-item-selection.js +++ b/addon/components/frost-list-item-selection.js @@ -28,7 +28,9 @@ export default Component.extend({ PropTypes.EmberObject, PropTypes.object ]), + index: PropTypes.number, isSelected: PropTypes.bool, + singleSelection: PropTypes.bool, onSelect: PropTypes.func.isRequired }, diff --git a/addon/components/frost-list.js b/addon/components/frost-list.js index e030d37..4e18a4e 100644 --- a/addon/components/frost-list.js +++ b/addon/components/frost-list.js @@ -66,6 +66,8 @@ export default Component.extend({ PropTypes.EmberObject, PropTypes.object ]), + alwaysExpanded: PropTypes.bool, + singleSelection: PropTypes.bool, // Options - sub-components pagination: PropTypes.EmberComponent, @@ -110,6 +112,8 @@ export default Component.extend({ item: 'itemName', itemExpansion: 'itemExpansionName' }, + alwaysExpanded: false, + singleSelection: false, // Smoke and mirrors options alwaysUseDefaultHeight: false, @@ -132,12 +136,20 @@ export default Component.extend({ return [] } + const alwaysExpanded = this.get('alwaysExpanded') return items.map(item => { + let expanded + if (alwaysExpanded === true) { + expanded = true + } else { + expanded = isEmpty(expandedItems) ? false : expandedItems.some( + expandedItem => _itemComparator(expandedItem, item)) + } + return ObjectProxy.create({ content: item, states: { - isExpanded: isEmpty(expandedItems) ? false : expandedItems.some( - expandedItem => _itemComparator(expandedItem, item)), + isExpanded: expanded, isSelected: isEmpty(selectedItems) ? false : selectedItems.some( selectedItem => _itemComparator(selectedItem, item)) } @@ -207,6 +219,12 @@ export default Component.extend({ return pagination && isAnyItemExpansion }, + @readOnly + @computed('isAnyItemExpansion', 'alwaysExpanded') + isCollapseExpandAllVisible (isAnyItemExpansion, alwaysExpanded) { + return isAnyItemExpansion && !alwaysExpanded + }, + // == Functions ============================================================= setShift (event) { @@ -331,14 +349,18 @@ export default Component.extend({ const _itemComparator = this.get('_itemComparator') const clonedSelectedItems = A(this.get('selectedItems').slice()) const _rangeState = this.get('_rangeState') + const singleSelection = this.get('singleSelection') if (isRangeSelect === false && this.get('disableDeselectAll') === true) { // Ensure we are not interrupting a range select prior to forcing a isSpecificSelect isSpecificSelect = true } - // Selects are proccessed in order of precedence: specific, range, basic - if (isSpecificSelect) { + // If single selection is enabled, we can just use the basic selection + // Otherwise, selects are proccessed in order of precedence: specific, range, basic + if (singleSelection) { + selection.basic(clonedSelectedItems, item, _rangeState, _itemComparator) + } else if (isSpecificSelect) { selection.specific(clonedSelectedItems, item, _rangeState, _itemComparator) } else if (isRangeSelect) { selection.range(items, clonedSelectedItems, item, _rangeState, _itemComparator, itemKey) diff --git a/addon/templates/components/frost-list-item-content.hbs b/addon/templates/components/frost-list-item-content.hbs index fddbb9c..c8ece2d 100644 --- a/addon/templates/components/frost-list-item-content.hbs +++ b/addon/templates/components/frost-list-item-content.hbs @@ -8,7 +8,7 @@ data-test={{hook (concat hook '-item-container') index=index }} >
- {{#if itemExpansion}} + {{#if isExpansionIconVisible}} {{frost-list-item-expansion hook=(concat hookPrefix '-expansion') hookQualifiers=(hash index=index) @@ -23,9 +23,11 @@ hook=(concat hookPrefix '-selection') hookQualifiers=(hash index=index) model=model.content + index=index isSelected=model.states.isSelected onSelect=onSelect size=size + singleSelection=singleSelection }} {{/if}} diff --git a/addon/templates/components/frost-list-item-selection.hbs b/addon/templates/components/frost-list-item-selection.hbs index 5432888..a6c6bad 100644 --- a/addon/templates/components/frost-list-item-selection.hbs +++ b/addon/templates/components/frost-list-item-selection.hbs @@ -1,7 +1,14 @@ -{{! Template for the frost-list-item-selection component }} - -{{frost-checkbox - checked=isSelected - hook=(concat hookPrefix '-checkbox') - size=size -}} +{{#if singleSelection}} + {{frost-radio-button + checked=isSelected + hook=(concat hookPrefix '-radio-button') + size=size + value=(concat index '') + }} +{{else}} + {{frost-checkbox + checked=isSelected + hook=(concat hookPrefix '-checkbox') + size=size + }} +{{/if}} diff --git a/addon/templates/components/frost-list.hbs b/addon/templates/components/frost-list.hbs index 0e01a02..2b01fba 100644 --- a/addon/templates/components/frost-list.hbs +++ b/addon/templates/components/frost-list.hbs @@ -18,7 +18,7 @@
{{/if}} - {{#if isAnyItemExpansion}} + {{#if isCollapseExpandAllVisible}} {{frost-list-expansion hook=(concat hookPrefix '-expansion') onCollapseAll=(action '_collapseAll') @@ -63,6 +63,8 @@ onSelectionChange=onSelectionChange onExpand=(action '_expand') onSelect=(action '_select') + alwaysExpanded=alwaysExpanded + singleSelection=singleSelection }} {{else}} {{yield to="inverse"}} diff --git a/tests/dummy/app/pods/application/template.hbs b/tests/dummy/app/pods/application/template.hbs index 5e8be4d..bb1d842 100644 --- a/tests/dummy/app/pods/application/template.hbs +++ b/tests/dummy/app/pods/application/template.hbs @@ -19,6 +19,7 @@ {{#link-to 'infinite'}}Infinite{{/link-to}} {{#link-to 'paged'}}Paged{{/link-to}} {{#link-to 'typed'}}Typed{{/link-to}} + {{#link-to 'single'}}Single select{{/link-to}}
{{/frost-scroll}} diff --git a/tests/dummy/app/pods/single/controller.js b/tests/dummy/app/pods/single/controller.js new file mode 100644 index 0000000..5c39e3e --- /dev/null +++ b/tests/dummy/app/pods/single/controller.js @@ -0,0 +1,50 @@ +import Ember from 'ember' +const {A, Controller, isEmpty} = Ember +import computed, {readOnly} from 'ember-computed-decorators' +import {sort} from 'ember-frost-sort' + +export default Controller.extend({ + + // == Dependencies ========================================================== + + // == Properties ============================================================ + + expandedItems: A([]), + selectedItems: A([]), + sortOrder: A(['-id']), + sortingProperties: [ + {label: 'Id', value: 'id'}, + {label: 'Label', value: 'label'} + ], + + // == Computed Properties =================================================== + + @readOnly + @computed('model.[]', 'sortOrder.[]') + items (model, sortOrder) { + if (isEmpty(model)) { + return [] + } + return sort(model, sortOrder) // Client side sorting + }, + + // == Functions ============================================================= + + // == Lifecycle Hooks ======================================================= + + // == Actions =============================================================== + + actions: { + onExpansionChange (expandedItems) { + this.get('expandedItems').setObjects(expandedItems) + }, + + onSelectionChange (selectedItems) { + this.get('selectedItems').setObjects(selectedItems) + }, + + onSortingChange (sortOrder) { + this.get('sortOrder').setObjects(sortOrder) + } + } +}) diff --git a/tests/dummy/app/pods/single/route.js b/tests/dummy/app/pods/single/route.js new file mode 100644 index 0000000..8472756 --- /dev/null +++ b/tests/dummy/app/pods/single/route.js @@ -0,0 +1,8 @@ +import Ember from 'ember' +const {Route} = Ember + +export default Route.extend({ + model () { + return this.store.findAll('list-item') + } +}) diff --git a/tests/dummy/app/pods/single/template.hbs b/tests/dummy/app/pods/single/template.hbs new file mode 100644 index 0000000..ead5a35 --- /dev/null +++ b/tests/dummy/app/pods/single/template.hbs @@ -0,0 +1,17 @@ +{{frost-list + class='demo' + hook='demo' + item=(component 'list-item') + itemExpansion=(component 'list-item-expansion') + items=items + expandedItems=expandedItems + selectedItems=selectedItems + onExpansionChange=(action 'onExpansionChange') + onSelectionChange=(action 'onSelectionChange') + sorting=(component 'frost-sort' + sortOrder=sortOrder + sortingProperties=sortingProperties + onChange=(action 'onSortingChange') + ) + singleSelection=true +}} diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 0cf0d5b..4c11ed9 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -16,6 +16,7 @@ Router.map(function () { this.route('paged') this.route('size') this.route('typed') + this.route('single') }) export default Router diff --git a/tests/integration/components/frost-list-item-selection-test.js b/tests/integration/components/frost-list-item-selection-test.js index cadf5a6..aaa65d2 100644 --- a/tests/integration/components/frost-list-item-selection-test.js +++ b/tests/integration/components/frost-list-item-selection-test.js @@ -27,7 +27,8 @@ describe(test.label, function () { this.on('selectAction', selectSpy) this.setProperties({ hook: 'myListItemSelection', - model: model + model: model, + singleSelection: false }) this.render(hbs` {{frost-list-item-selection @@ -36,6 +37,7 @@ describe(test.label, function () { model=model size='medium' onSelect=(action 'selectAction') + singleSelection=singleSelection }} `) }) @@ -87,4 +89,22 @@ describe(test.label, function () { expect($hook('myListItemSelection')).to.have.class('is-selected') }) }) + + describe('when singleSelection is true', function () { + beforeEach(function () { + this.set('singleSelection', true) + }) + + it('should set -radio-button hook correctly', function () { + expect($hook('myListItemSelection-radio-button')).to.be.length(1) + }) + + it('should use radio buttons instead of checkboxes', function () { + expect($hook('myListItemSelection-radio-button-input').attr('type')).to.equal('radio') + }) + + it('should add correct size to the radio buttons', function () { + expect($hook('myListItemSelection-radio-button')).to.have.class('medium') + }) + }) }) diff --git a/tests/integration/components/frost-list-test.js b/tests/integration/components/frost-list-test.js index dfaa6f6..d4f381b 100644 --- a/tests/integration/components/frost-list-test.js +++ b/tests/integration/components/frost-list-test.js @@ -1563,4 +1563,129 @@ describe(test.label, function () { }) }) }) + + describe('When singleSelection is true', function () { + beforeEach(function () { + const testItems = A([ + Ember.Object.create({id: '0'}), + Ember.Object.create({id: '1'}), + Ember.Object.create({id: '2'}) + ]) + + this.setProperties({ + items: testItems, + selectedItems: A([]), + onSelectionChange: (selectedItems) => { + this.get('selectedItems').setObjects(selectedItems) + } + }) + + this.render(hbs` + {{frost-list + singleSelection=true + item=(component 'frost-list-item') + hook='myList' + items=items + selectedItems=selectedItems + onSelectionChange=onSelectionChange + }} + `) + return wait() + }) + + describe('should only select one item with shift', function () { + beforeEach(function () { + $hook('myList-itemContent-item', {index: 0}).click() + const clickEvent = $.Event('click') + clickEvent.shiftKey = true + $hook('myList-itemContent-item', {index: 2}).trigger(clickEvent) + }) + + itShouldHaveItemSelectedState(0, false) + itShouldHaveItemSelectedState(1, false) + itShouldHaveItemSelectedState(2, true) + itShouldHaveSelectedItemsLength(1) + }) + + describe('should only select one item with specific click', function () { + beforeEach(function () { + $hook('myList-itemContent-selection', {index: 0}).click() + $hook('myList-itemContent-selection', {index: 1}).click() + }) + + itShouldHaveItemSelectedState(0, false) + itShouldHaveItemSelectedState(1, true) + itShouldHaveSelectedItemsLength(1) + }) + }) + + describe('When alwaysExpanded is true', function () { + describe('itemExpansion is provided', function () { + beforeEach(function () { + registerMockComponent(this, 'mock-item-expansion') + const testItems = A([ + Ember.Object.create({id: '0'}), + Ember.Object.create({id: '1'}) + ]) + + this.setProperties({ + items: testItems + }) + + this.render(hbs` + {{frost-list + alwaysExpanded=true + item=(component 'frost-list-item') + itemExpansion=(component 'mock-item-expansion' class='mock-item-expansion') + hook='myList' + items=items + }} + `) + return wait() + }) + + afterEach(function () { + unregisterMockComponent(this, 'mock-item-expansion') + }) + + it('should not show list-expansion header', function () { + expect($hook('myList-expansion')).to.have.length(0) + }) + + it('should not show item-expansion arrow', function () { + expect($hook('myList-itemContent-expansion')).to.have.length(0) + }) + + it('should have all items expanded', function () { + expect($hook('myList-itemContent-itemExpansion')).to.have.length(2) + }) + }) + + describe('itemExpansion not provided', function () { + beforeEach(function () { + const testItems = A([ + Ember.Object.create({id: '0'}), + Ember.Object.create({id: '1'}) + ]) + + this.setProperties({ + items: testItems + }) + + this.render(hbs` + {{frost-list + alwaysExpanded=true + item=(component 'frost-list-item') + hook='myList' + items=items + }} + `) + return wait() + }) + + it('should still render items if itemExpansion not provided', function () { + expect($hook('myList-itemContent')).to.have.length(2) + }) + }) + }) })