Skip to content

Commit

Permalink
feat(list): adds list type property
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Removes selectable and multiple properties and adds type property.
With the type property it's now also possible to have radio button lists.

fix #133
  • Loading branch information
hannahu authored and adrianschmidt committed Sep 10, 2019
1 parent 9ab16df commit 39a2ece
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 95 deletions.
4 changes: 2 additions & 2 deletions src/components/list/list-renderer-config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ListType } from './list.types';
export interface ListRendererConfig {
isMenu?: boolean;
isOpen?: boolean;
selectable?: boolean;
badgeIcons?: boolean;
multiple?: boolean;
type?: ListType;
}
109 changes: 64 additions & 45 deletions src/components/list/list-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { ListItem, ListSeparator } from '../../interface';
import { CheckboxTemplate } from '../checkbox/checkbox.template';
import { ListRendererConfig } from './list-renderer-config';
import { RadioButtonTemplate } from './radio-button/radio-button.template';

export class ListRenderer {
private defaultConfig: ListRendererConfig = {
isMenu: false,
isOpen: true,
selectable: false,
badgeIcons: false,
multiple: false,
};

private hasIcons: boolean;
Expand All @@ -29,21 +28,30 @@ export class ListRenderer {
});

const badgeIcons = config.badgeIcons && this.hasIcons;

let role = config.selectable && config.multiple ? 'group' : null;
if (!role) {
role = config.isMenu ? 'menu' : 'listbox';
const selectableListTypes = ['selectable', 'radio', 'checkbox'];

let role;
switch (config.type) {
case 'checkbox':
role = 'group';
break;
case 'radio':
role = 'radiogroup';
break;
default:
role = config.isMenu ? 'menu' : 'listbox';
}

const classNames = {
'mdc-list': true,
'mdc-list--two-line': twoLines,
selectable: selectableListTypes.includes(config.type),
'mdc-list--avatar-list': badgeIcons,
};

return (
<ul
class={`
mdc-list
${twoLines ? 'mdc-list--two-line' : ''}
${config.isMenu ? 'mdc-menu__items' : ''}
${config.selectable ? 'selectable' : ''}
${badgeIcons ? 'mdc-list--avatar-list' : ''}
`}
class={classNames}
aria-hidden={(!config.isOpen).toString()}
role={role}
aria-orientation="vertical"
Expand Down Expand Up @@ -71,18 +79,20 @@ export class ListRenderer {
return <li class="mdc-list-divider" role="separator" />;
}

if (config.selectable && config.multiple) {
return this.renderCheckboxListItem(config, item, index);
if (['radio', 'checkbox'].includes(config.type)) {
return this.renderVariantListItem(config, item, index);
}

const classNames = {
'mdc-list-item': true,
'mdc-list-item--disabled': item.disabled,
'mdc-list-item__text': !item.secondaryText,
'mdc-list-item--selected': item.selected,
};

return (
<li
class={`
mdc-list-item
${item.disabled ? 'mdc-list-item--disabled' : ''}
${!item.secondaryText ? 'mdc-list-item__text' : ''}
${item.selected ? 'mdc-list-item--selected' : ''}
`}
class={classNames}
role={config.isMenu ? 'menuitem' : ''}
tabindex={item.disabled ? '-1' : '0'}
aria-disabled={item.disabled ? 'true' : 'false'}
Expand Down Expand Up @@ -145,55 +155,64 @@ export class ListRenderer {
);
}

private renderCheckboxListItem(
private renderVariantListItem(
config: ListRendererConfig,
item: ListItem,
index: number
) {
let itemTemplate;
if (config.type === 'radio') {
itemTemplate = (
<RadioButtonTemplate
id={`c_${index}`}
checked={item.selected}
disabled={item.disabled}
/>
);
} else if (config.type === 'checkbox') {
itemTemplate = (
<CheckboxTemplate
id={`c_${index}`}
checked={item.selected}
disabled={item.disabled}
/>
);
}

const classNames = {
'mdc-list-item': true,
'mdc-list-item--disabled': item.disabled,
'mdc-list-item__text': !item.secondaryText,
};

return (
<li
class={`
mdc-list-item
${item.disabled ? 'mdc-list-item--disabled' : ''}
${!item.secondaryText ? 'mdc-list-item__text' : ''}
`}
role="checkbox"
class={classNames}
role={config.type}
aria-checked={item.selected ? 'true' : 'false'}
tabindex={item.disabled ? '-1' : '0'}
aria-disabled={item.disabled ? 'true' : 'false'}
data-index={index}
>
{this.renderCheckboxListItemContent(config, item, index)}
{this.renderVariantListItemContent(config, item, itemTemplate)}
</li>
);
}

private renderCheckboxListItemContent(
private renderVariantListItemContent(
config: ListRendererConfig,
item: ListItem,
index: number
itemTemplate: any
) {
if (this.hasIcons) {
return [
item.icon ? this.renderIcon(config, item) : null,
this.renderText(item.text, item.secondaryText),
<div class="mdc-list-item__meta">
<CheckboxTemplate
id={`c_${index}`}
checked={item.selected}
disabled={item.disabled}
/>
</div>,
<div class="mdc-list-item__meta">{itemTemplate}</div>,
];
}
return [
<div class="mdc-list-item__graphic">
<CheckboxTemplate
id={`c_${index}`}
checked={item.selected}
disabled={item.disabled}
/>
</div>,
<div class="mdc-list-item__graphic">{itemTemplate}</div>,
this.renderText(item.text, item.secondaryText),
];
}
Expand Down
16 changes: 6 additions & 10 deletions src/components/list/list.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,22 +170,18 @@ describe('limel-list', async () => {
});
});

describe('when the attribute `selectable`', () => {
describe('when the attribute `type`', () => {
describe('is not set', () => {
it('is not selectable', () => {
expect(innerList).not.toHaveClass('selectable');
});
it('the property is falsy', async () => {
const propValue = await limelList.getProperty('selectable');
expect(propValue).toBeFalsy();
});
});

describe('is set', () => {
describe('is set as `selectable`', () => {
let items;
beforeEach(async () => {
page = await newE2EPage({
html: '<limel-list selectable></limel-list>',
html: '<limel-list type="selectable"></limel-list>',
});
limelList = await page.find('limel-list');
innerList = await page.find('limel-list>>>ul');
Expand All @@ -196,9 +192,9 @@ describe('limel-list', async () => {
it('is selectable', () => {
expect(innerList).toHaveClass('selectable');
});
it('the property is `true`', async () => {
const propValue = await limelList.getProperty('selectable');
expect(propValue).toBe(true);
it('has the value `selectable`', async () => {
const propValue = await limelList.getProperty('type');
expect(propValue).toBe('selectable');
});
describe('the `change` event', () => {
let spy;
Expand Down
12 changes: 12 additions & 0 deletions src/components/list/list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,15 @@ When importing ListItem, see [Import Statements](/#import-statements).
When importing ListItem, see [Import Statements](/#import-statements).

<limel-example name="limel-example-list-checkbox-icons" path="list" />

### List with radio buttons

When importing ListItem, see [Import Statements](/#import-statements).

<limel-example name="limel-example-list-radio-button" path="list" />

### List with radio buttons and icons

When importing ListItem, see [Import Statements](/#import-statements).

<limel-example name="limel-example-list-radio-button-icons" path="list" />
2 changes: 2 additions & 0 deletions src/components/list/list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

@import '../checkbox/checkbox.scss';

@import './radio-button/radio-button.scss';

/**
* @prop --icon-background-color: Color to use for icon background.
*/
Expand Down
66 changes: 45 additions & 21 deletions src/components/list/list.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { MDCList, MDCListActionEvent } from '@lime-material-16px/list';
import { Component, Element, Event, EventEmitter, Prop } from '@stencil/core';
import { strings } from '@lime-material-16px/list/constants';
import {
Component,
Element,
Event,
EventEmitter,
Prop,
Watch,
} from '@stencil/core';
import { ListItem, ListSeparator } from '../../interface';
import { ListRenderer } from './list-renderer';
import { ListRendererConfig } from './list-renderer-config';
import { ListType } from './list.types';

const { ACTION_EVENT } = strings;

@Component({
tag: 'limel-list',
Expand All @@ -17,29 +28,28 @@ export class List {
public items: Array<ListItem | ListSeparator>;

/**
* True if the items in the list should be selectable/clickable
*/
@Prop()
public selectable: boolean;

/**
* True if the list should display larger icons with a background
* Set to `true` if the list should display larger icons with a background
*/
@Prop()
public badgeIcons: boolean;

/**
* True if it should be possible to select multiple items
* The type of the list, omit to get a regular list. Available types are:
* `selectable`: regular list with single selection.
* `radio`: radio button list with single selection.
* `checkbox`: checkbox list with multiple selection.
*/
@Prop()
public multiple: boolean;
public type: ListType;

@Element()
private element: HTMLElement;

private mdcList: MDCList;
private listRenderer = new ListRenderer();
private config: ListRendererConfig;
private listRenderer = new ListRenderer();
private mdcList: MDCList;
private multiple: boolean;
private selectable: boolean;

/**
* Fired when a new value has been selected from the list. Only fired if selectable is set to true
Expand All @@ -56,31 +66,42 @@ export class List {
this.element.shadowRoot.querySelector('.mdc-list')
);

if (!this.selectable) {
return;
}

this.mdcList.listen('MDCList:action', this.handleAction);
this.mdcList.singleSelection = !this.multiple;
this.handleType();
}

public componentDidUnload() {
if (this.selectable) {
this.mdcList.unlisten('MDCList:action', this.handleAction);
this.mdcList.unlisten(ACTION_EVENT, this.handleAction);
}

this.mdcList.destroy();
}

public render() {
this.config = {
selectable: this.selectable,
badgeIcons: this.badgeIcons,
multiple: this.multiple,
type: this.type,
};
return this.listRenderer.render(this.items, this.config);
}

@Watch('type')
protected handleType() {
this.mdcList.unlisten(ACTION_EVENT, this.handleAction);

this.selectable = ['selectable', 'radio', 'checkbox'].includes(
this.type
);
this.multiple = this.type === 'checkbox';

if (!this.selectable) {
return;
}

this.mdcList.listen(ACTION_EVENT, this.handleAction);
this.mdcList.singleSelection = !this.multiple;
}

private handleAction(event: MDCListActionEvent) {
if (!this.multiple) {
this.handleSingleSelect(event.detail.index);
Expand All @@ -99,6 +120,9 @@ export class List {
});

if (selectedItem) {
if (this.type === 'radio' && listItems[index] === selectedItem) {
return;
}
this.change.emit({ ...selectedItem, selected: false });
}

Expand Down
7 changes: 7 additions & 0 deletions src/components/list/list.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* The type of the list, omit to get a regular list. Available types are:
* `selectable`: regular list with single selection.
* `radio`: radio button list with single selection.
* `checkbox`: checkbox list with multiple selection.
*/
export type ListType = 'selectable' | 'radio' | 'checkbox';
2 changes: 1 addition & 1 deletion src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ export class Menu {

public render() {
const config: ListRendererConfig = {
selectable: true,
isMenu: true,
isOpen: this.open,
badgeIcons: this.badgeIcons,
type: 'selectable',
};
return (
<div class="mdc-menu-surface--anchor">
Expand Down
Loading

0 comments on commit 39a2ece

Please sign in to comment.