Skip to content

Commit

Permalink
feat(limel-list): add checkbox to list item
Browse files Browse the repository at this point in the history
  • Loading branch information
BregenzerK committed Nov 21, 2018
1 parent 5b4759a commit 5cfe177
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 37 deletions.
5 changes: 5 additions & 0 deletions src/components/list/list-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export interface ListItem {
*/
iconColor?: string;

/**
* True if a checkbox of this item should be checked
*/
checked?: boolean;

[data: string]: any;
}

Expand Down
1 change: 1 addition & 0 deletions src/components/list/list-renderer-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export interface ListRendererConfig {
isOpen?: boolean;
selectable?: boolean;
badgeIcons?: boolean;
includeCheckboxes?: boolean;
}
54 changes: 51 additions & 3 deletions src/components/list/list-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export class ListRenderer {
isOpen: true,
selectable: false,
badgeIcons: false,
includeCheckboxes: false,
};

public render(
Expand All @@ -15,7 +16,6 @@ export class ListRenderer {
) {
items = items || [];
config = { ...this.defaultConfig, ...config };

const twoLines = items.some(item => {
return 'secondaryText' in item && !!item.secondaryText;
});
Expand Down Expand Up @@ -66,12 +66,19 @@ export class ListRenderer {

return (
<li
class="mdc-list-item"
class="mdc-list-item mdc-form-field"
role={config.isMenu ? 'menuitem' : ''}
tabindex={item.disabled ? '-1' : '0'}
aria-disabled={item.disabled ? 'true' : 'false'}
data-index={index}
>
{config.includeCheckboxes
? this.renderCheckboxes(
item.checked,
item.disabled,
item.id
)
: null}
{item.icon ? this.renderIcon(item) : null}
{this.renderText(item.text, item.secondaryText)}
</li>
Expand All @@ -88,7 +95,7 @@ export class ListRenderer {
*/
private renderText(text: string, secondaryText?: string) {
if (!secondaryText) {
return text;
return <label>{text}</label>;
}

return (
Expand Down Expand Up @@ -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 (
<div
class={`
mdc-checkbox
${disabled ? 'mdc-checkbox--disabled' : ''}
`}
>
<input
type="checkbox"
class="mdc-checkbox__native-control"
value={value}
checked={checked}
disabled={disabled}
/>
<div class="mdc-checkbox__background">
<svg class="mdc-checkbox__checkmark" viewBox="0 0 24 24">
<path
className="mdc-checkbox__checkmark-path"
fill="none"
d="M1.73,12.91 8.1,19.28 22.79,4.59"
/>
</svg>
<div class="mdc-checkbox__mixedmark" />
</div>
</div>
);
}
}
42 changes: 40 additions & 2 deletions src/components/list/list.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
});
});
});
3 changes: 3 additions & 0 deletions src/components/list/list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ menu: Components

### List with badge icons
<limel-example name="limel-example-list-badge-icons" path="list" />

### List with checkboxes
<limel-example name="limel-example-list-checkboxes" path="list" />
1 change: 1 addition & 0 deletions src/components/list/list.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "@lime-material/list/mdc-list";
@import '@lime-material/checkbox/mdc-checkbox';

:host {
display: block;
Expand Down
112 changes: 80 additions & 32 deletions src/components/list/list.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -28,54 +37,77 @@ 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;

private mdcList: MDCList;
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);
}
Expand All @@ -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);
}
}
}
33 changes: 33 additions & 0 deletions src/examples/list/list-checkboxes.tsx
Original file line number Diff line number Diff line change
@@ -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 [
<limel-list
onChange={event => {
console.log(event.detail);
}}
includeCheckboxes={true}
items={this.items}
/>,
<hr />,
<p>
When importing ListItem, see{' '}
<a href="/usage#import-statements">Usage</a>
</p>,
];
}
}

0 comments on commit 5cfe177

Please sign in to comment.