diff --git a/src/components/list/list-item.ts b/src/components/list/list-item.ts index 9745903e49..befc359e69 100644 --- a/src/components/list/list-item.ts +++ b/src/components/list/list-item.ts @@ -24,6 +24,11 @@ export interface ListItem { */ iconColor?: string; + /** + * True if a checkbox of this item should be checked + */ + checked?: boolean; + [data: string]: any; } diff --git a/src/components/list/list-renderer-config.ts b/src/components/list/list-renderer-config.ts index f3646bb7d8..590503d9f5 100644 --- a/src/components/list/list-renderer-config.ts +++ b/src/components/list/list-renderer-config.ts @@ -3,4 +3,5 @@ export interface ListRendererConfig { isOpen?: boolean; selectable?: boolean; badgeIcons?: boolean; + includeCheckboxes?: boolean; } diff --git a/src/components/list/list-renderer.tsx b/src/components/list/list-renderer.tsx index 5380924e6d..ff85f8debf 100644 --- a/src/components/list/list-renderer.tsx +++ b/src/components/list/list-renderer.tsx @@ -7,6 +7,7 @@ export class ListRenderer { isOpen: true, selectable: false, badgeIcons: false, + includeCheckboxes: false, }; public render( @@ -15,7 +16,6 @@ export class ListRenderer { ) { items = items || []; config = { ...this.defaultConfig, ...config }; - const twoLines = items.some(item => { return 'secondaryText' in item && !!item.secondaryText; }); @@ -66,12 +66,19 @@ export class ListRenderer { return (
  • + {config.includeCheckboxes + ? this.renderCheckboxes( + item.checked, + item.disabled, + item.id + ) + : null} {item.icon ? this.renderIcon(item) : null} {this.renderText(item.text, item.secondaryText)}
  • @@ -88,7 +95,7 @@ export class ListRenderer { */ private renderText(text: string, secondaryText?: string) { if (!secondaryText) { - return text; + return ; } return ( @@ -123,4 +130,45 @@ export class ListRenderer { /> ); } + + /** + * + * Render a checkbox for a list item + * @param checked + * @param disabled + * @returns {HTMLElement} the checkbox if includeCheckboxes true + */ + + private renderCheckboxes( + checked: boolean, + disabled: boolean, + value: number + ) { + return ( +
    + +
    + + + +
    +
    +
    + ); + } } diff --git a/src/components/list/list.e2e.ts b/src/components/list/list.e2e.ts index dd0a00f016..6c80b5a5c5 100644 --- a/src/components/list/list.e2e.ts +++ b/src/components/list/list.e2e.ts @@ -133,8 +133,8 @@ describe('limel-list', async () => { expect(innerList).toHaveClass('mdc-list--two-line'); }); it('renders items withOUT secondary text as single line', () => { - expect(innerList.children[0].children).toHaveLength(0); - expect(innerList.children[0]).toEqualText('item 1'); + expect(innerList.children[0].children[0].children).toHaveLength(0); + expect(innerList.children[0].children[0]).toEqualText('item 1'); }); it('renders items WITH secondary text as two lines', () => { expect(innerList.children[2].children).toHaveLength(1); @@ -222,4 +222,42 @@ describe('limel-list', async () => { }); }); }); + + describe('when the attribute `includeCheckboxes`', () => { + let items; + beforeEach(async () => { + items = [{ text: 'item 1' }]; + await limelList.setProperty('items', items); + await page.waitForChanges(); + }); + describe('is not set', () => { + it('is not selectable and does not contain checkboxes', () => { + expect(innerList).not.toHaveClass('selectable'); + expect(innerList.children[0].children).toHaveLength(1); + }); + it('the property is falsy', async () => { + const propValue = await limelList.getProperty( + 'includeCheckboxes' + ); + expect(propValue).toBeFalsy(); + }); + }); + + describe('is set', () => { + beforeEach(async () => { + await limelList.setProperty('includeCheckboxes', true); + await page.waitForChanges(); + }); + it('is selectable and contains checkboxes', () => { + expect(innerList).toHaveClass('selectable'); + expect(innerList.children[0].children).toHaveLength(2); // label and checkbox + }); + it('the property is `true`', async () => { + const propValue = await limelList.getProperty( + 'includeCheckboxes' + ); + expect(propValue).toBe(true); + }); + }); + }); }); diff --git a/src/components/list/list.mdx b/src/components/list/list.mdx index 939b60f9ac..eacaab12db 100644 --- a/src/components/list/list.mdx +++ b/src/components/list/list.mdx @@ -24,3 +24,6 @@ menu: Components ### List with badge icons + +### List with checkboxes + diff --git a/src/components/list/list.scss b/src/components/list/list.scss index 604cdd356f..0c7ae59d9b 100644 --- a/src/components/list/list.scss +++ b/src/components/list/list.scss @@ -1,4 +1,5 @@ @import "@lime-material/list/mdc-list"; +@import '@lime-material/checkbox/mdc-checkbox'; :host { display: block; diff --git a/src/components/list/list.tsx b/src/components/list/list.tsx index 93f9f08972..94a7b6234b 100644 --- a/src/components/list/list.tsx +++ b/src/components/list/list.tsx @@ -1,5 +1,14 @@ +import { MDCCheckbox } from '@lime-material/checkbox'; +import { MDCFormField } from '@lime-material/form-field'; import { MDCList } from '@lime-material/list'; -import { Component, Element, Event, EventEmitter, Prop } from '@stencil/core'; +import { + Component, + Element, + Event, + EventEmitter, + Prop, + State, +} from '@stencil/core'; import { ListItem, ListSeparator } from '../../interface'; import { ListRenderer } from './list-renderer'; import { ListRendererConfig } from './list-renderer-config'; @@ -28,6 +37,12 @@ export class List { @Prop() public badgeIcons: boolean; + /** + * True if each list item should be trailed be a checkbox + */ + @Prop() + public includeCheckboxes: boolean; + @Element() private element: HTMLElement; @@ -35,47 +50,64 @@ export class List { private listRenderer = new ListRenderer(); /** - * Fired when a new value has been selected from the list. Only fired if selectable is set to true + * Fired when a new value has been selected from the list. Only fired if selectable or includeCheckboxes is set to true */ @Event() private change: EventEmitter; + @State() + private mdcCheckboxes = []; + public componentDidLoad() { this.mdcList = new MDCList( this.element.shadowRoot.querySelector('.mdc-list') ); - if (!this.selectable) { - return; - } + if (this.selectable || this.includeCheckboxes) { + this.mdcList.singleSelection = true; - this.mdcList.singleSelection = true; + // This is ugly and not the right way to do it. + // A better way would be to implement our own + // adapter and foundation classes for MDCList + // that works with the shadow DOM + this.mdcList.foundation_.adapter_.getFocusedElementIndex = () => { + return this.mdcList.listElements.indexOf( + this.element.shadowRoot.activeElement + ); + }; + const setSelectedIndex = this.mdcList.foundation_.setSelectedIndex; + const self = this; // tslint:disable-line:no-this-assignment + this.mdcList.foundation_.setSelectedIndex = function(...args) { + setSelectedIndex.apply(this, args); + self.handleSelectItem(args[0]); + }; + if (this.includeCheckboxes) { + const elements = Array.from( + this.element.shadowRoot.querySelectorAll('.mdc-form-field') + ); - // This is ugly and not the right way to do it. - // A better way would be to implement our own - // adapter and foundation classes for MDCList - // that works with the shadow DOM - this.mdcList.foundation_.adapter_.getFocusedElementIndex = () => { - return this.mdcList.listElements.indexOf( - this.element.shadowRoot.activeElement - ); - }; - const setSelectedIndex = this.mdcList.foundation_.setSelectedIndex; - const self = this; // tslint:disable-line:no-this-assignment - this.mdcList.foundation_.setSelectedIndex = function(...args) { - setSelectedIndex.apply(this, args); - self.handleSelectItem(args[0]); - }; + elements.forEach(element => { + const formField = new MDCFormField(element); + const checkbox = new MDCCheckbox(element.firstChild); + formField.input = checkbox; + this.mdcCheckboxes.push(checkbox); + }); + } + } } public componentDidUnload() { this.mdcList.destroy(); + this.mdcCheckboxes.forEach(checkbox => { + checkbox.destroy(); + }); } public render() { const config: ListRendererConfig = { - selectable: this.selectable, + selectable: this.selectable || this.includeCheckboxes, badgeIcons: this.badgeIcons, + includeCheckboxes: this.includeCheckboxes, }; return this.listRenderer.render(this.items, config); } @@ -88,16 +120,32 @@ export class List { * @returns {void} */ private handleSelectItem(index: number) { - const selectedElement = this.element.shadowRoot.querySelector( - '.mdc-list-item--selected' - ); - let selectedItem: ListItem = null; - if (selectedElement) { - selectedItem = this.items.filter(item => { - return !('separator' in item); - })[index] as ListItem; - } + if (this.selectable) { + const selectedElement = this.element.shadowRoot.querySelector( + '.mdc-list-item--selected' + ); + let selectedItem: ListItem = null; + if (selectedElement) { + selectedItem = this.items.filter(item => { + return !('separator' in item); + })[index] as ListItem; + } - this.change.emit(selectedItem); + this.change.emit(selectedItem); + } + if (this.includeCheckboxes) { + const checked = this.items.filter((item: ListItem) => { + const optionChecked = this.mdcCheckboxes.some(mdcCheckbox => { + return ( + mdcCheckbox.checked && + mdcCheckbox.value === item.id.toString() + ); + }); + if (optionChecked) { + return item; + } + }); + this.change.emit(checked); + } } } diff --git a/src/examples/list/list-checkboxes.tsx b/src/examples/list/list-checkboxes.tsx new file mode 100644 index 0000000000..d34269856d --- /dev/null +++ b/src/examples/list/list-checkboxes.tsx @@ -0,0 +1,33 @@ +import { Component } from '@stencil/core'; +import { ListItem } from '../../interface'; + +@Component({ + tag: 'limel-example-list-checkboxes', + shadow: true, +}) +export class ListCheckboxesExample { + private items: ListItem[] = [ + { text: 'King of Tokyo', id: 1 }, + { text: 'Smash Up!', id: 2, checked: true }, + { text: 'Pandemic', id: 3 }, + { text: 'Catan', id: 4 }, + { text: 'Ticket to Ride', id: 5 }, + ]; + + public render() { + return [ + { + console.log(event.detail); + }} + includeCheckboxes={true} + items={this.items} + />, +
    , +

    + When importing ListItem, see{' '} + Usage +

    , + ]; + } +}