diff --git a/src/demo-app/chips/chips-demo.html b/src/demo-app/chips/chips-demo.html
index 5100ef5f31fc..d3ba62b3824f 100644
--- a/src/demo-app/chips/chips-demo.html
+++ b/src/demo-app/chips/chips-demo.html
@@ -1,68 +1,65 @@
-
- Static Chips
-
- Simple
-
-
- Chip 1
- Chip 2
- Chip 3
-
-
- Advanced
-
-
- Selected/Colored
-
- With Events
-
-
-
- Unstyled
-
-
- Basic Chip 1
- Basic Chip 2
- Basic Chip 3
-
-
- Material Contributors
-
-
-
- {{person.name}}
-
-
-
-
-
-
-
-
- Stacked Chips
-
-
- You can also stack the chips if you want them on top of each other.
-
-
-
-
- None
-
-
-
- Primary
-
-
-
- Accent
-
-
-
- Warn
-
-
-
+
+ Static Chips
+
+
+ Simple
+
+
+ Chip 1
+ Chip 2
+ Chip 3
+
+
+ Unstyled
+
+
+ Basic Chip 1
+ Basic Chip 2
+ Basic Chip 3
+
+
+ Advanced
+
+
+ Selected/Colored
+
+ With Events
+
+
+
+
+
+
+ Dynamic Chips
+
+
+ Input Container
+
+
+
+ {{person.name}}
+
+
+
+
+
+
+
+ Stacked Chips
+
+
+ You can also stack the chips if you want them on top of each other and/or use the
+ (focus)
event to run custom code.
+
+
+
+
+ {{aColor.name}}
+
+
+
+
\ No newline at end of file
diff --git a/src/demo-app/chips/chips-demo.scss b/src/demo-app/chips/chips-demo.scss
index 46e1d249941e..f0ba734b465f 100644
--- a/src/demo-app/chips/chips-demo.scss
+++ b/src/demo-app/chips/chips-demo.scss
@@ -4,6 +4,19 @@
max-width: 200px;
}
+ md-card {
+ padding: 0;
+ margin: 16px;
+
+ & md-toolbar {
+ margin: 0;
+ }
+
+ & md-card-content {
+ padding: 24px;
+ }
+ }
+
md-basic-chip {
margin: auto 10px;
}
diff --git a/src/demo-app/chips/chips-demo.ts b/src/demo-app/chips/chips-demo.ts
index 45ac721ff490..16135f235cf0 100644
--- a/src/demo-app/chips/chips-demo.ts
+++ b/src/demo-app/chips/chips-demo.ts
@@ -5,6 +5,11 @@ export interface Person {
name: string;
}
+export interface DemoColor {
+ name: string;
+ color: string;
+}
+
@Component({
moduleId: module.id,
selector: 'chips-demo',
@@ -24,6 +29,13 @@ export class ChipsDemo {
{ name: 'Paul' }
];
+ availableColors: DemoColor[] = [
+ { name: 'none', color: '' },
+ { name: 'Primary', color: 'primary' },
+ { name: 'Accent', color: 'accent' },
+ { name: 'Warn', color: 'warn' }
+ ];
+
alert(message: string): void {
alert(message);
}
diff --git a/src/lib/chips/_chips-theme.scss b/src/lib/chips/_chips-theme.scss
index 9fa1b60b2240..3dd5dccfc8e2 100644
--- a/src/lib/chips/_chips-theme.scss
+++ b/src/lib/chips/_chips-theme.scss
@@ -1,3 +1,4 @@
+@import '../core/theming/palette';
@import '../core/theming/theming';
@mixin md-chips-theme($theme) {
@@ -6,27 +7,40 @@
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
+ $foreground: map-get($theme, foreground);
+
+ // Use spec-recommended color for regular foreground, and utilise contrast color for a grey very
+ // close to the selected spec since no guidance is provided and to ensure palette consistency.
+ $light-foreground: rgba(0, 0, 0, 0.87);
+ $light-selected-foreground: md-contrast($md-grey, 600);
+
+ // The spec only provides guidance for light-themed chips. When inside of a dark theme, fall back
+ // to standard background and foreground colors.
+ $unselected-background: if($is-dark-theme, md-color($background, card), #e0e0e0);
+ $unselected-foreground: if($is-dark-theme, md-color($foreground, text), $light-foreground);
+
+ $selected-background: if($is-dark-theme, md-color($background, app-bar), #808080);
+ $selected-foreground: if($is-dark-theme, md-color($foreground, text), $light-selected-foreground);
.md-chip {
- background-color: #e0e0e0;
- color: rgba(0, 0, 0, 0.87);
+ background-color: $unselected-background;
+ color: $unselected-foreground;
}
- .md-chip.selected {
- // There is no dark theme in the current spec, so this applies to both
- background-color: #808080;
-
- // Use a contrast color for a grey very close to the background color
- color: md-contrast($md-grey, 600);
+ .md-chip.md-chip-selected {
+ background-color: $selected-background;
+ color: $selected-foreground;
&.md-primary {
background-color: md-color($primary, 500);
color: md-contrast($primary, 500);
}
+
&.md-accent {
background-color: md-color($accent, 500);
color: md-contrast($accent, 500);
}
+
&.md-warn {
background-color: md-color($warn, 500);
color: md-contrast($warn, 500);
diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts
index 01daa7be7bd9..3552f90996f6 100644
--- a/src/lib/chips/chip-list.spec.ts
+++ b/src/lib/chips/chip-list.spec.ts
@@ -3,6 +3,8 @@ import {Component, DebugElement, QueryList} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdChip, MdChipList, MdChipsModule} from './index';
import {ListKeyManager} from '../core/a11y/list-key-manager';
+import {FakeEvent} from '../core/a11y/list-key-manager.spec';
+import {SPACE} from '../core/keyboard/keycodes';
describe('MdChipList', () => {
let fixture: ComponentFixture;
@@ -10,7 +12,7 @@ describe('MdChipList', () => {
let chipListNativeElement: HTMLElement;
let chipListInstance: MdChipList;
let testComponent: StaticChipList;
- let items: QueryList;
+ let chips: QueryList;
let manager: ListKeyManager;
beforeEach(async(() => {
@@ -22,9 +24,7 @@ describe('MdChipList', () => {
});
TestBed.compileComponents();
- }));
- beforeEach(() => {
fixture = TestBed.createComponent(StaticChipList);
fixture.detectChanges();
@@ -32,7 +32,8 @@ describe('MdChipList', () => {
chipListNativeElement = chipListDebugElement.nativeElement;
chipListInstance = chipListDebugElement.componentInstance;
testComponent = fixture.debugElement.componentInstance;
- });
+ chips = chipListInstance.chips;
+ }));
describe('basic behaviors', () => {
it('adds the `md-chip-list` class', () => {
@@ -42,12 +43,20 @@ describe('MdChipList', () => {
describe('focus behaviors', () => {
beforeEach(() => {
- items = chipListInstance.chips;
manager = chipListInstance._keyManager;
});
+ it('focuses the first chip on focus', () => {
+ let FOCUS_EVENT: Event = {} as Event;
+
+ chipListInstance.focus(FOCUS_EVENT);
+ fixture.detectChanges();
+
+ expect(manager.focusedItemIndex).toBe(0);
+ });
+
it('watches for chip focus', () => {
- let array = items.toArray();
+ let array = chips.toArray();
let lastIndex = array.length - 1;
let lastItem = array[lastIndex];
@@ -59,7 +68,7 @@ describe('MdChipList', () => {
describe('on chip destroy', () => {
it('focuses the next item', () => {
- let array = items.toArray();
+ let array = chips.toArray();
let midItem = array[2];
// Focus the middle item
@@ -74,7 +83,7 @@ describe('MdChipList', () => {
});
it('focuses the previous item', () => {
- let array = items.toArray();
+ let array = chips.toArray();
let lastIndex = array.length - 1;
let lastItem = array[lastIndex];
@@ -91,20 +100,89 @@ describe('MdChipList', () => {
});
});
+ describe('keyboard behavior', () => {
+
+ describe('when selectable is true', () => {
+ beforeEach(() => {
+ testComponent.selectable = true;
+ fixture.detectChanges();
+ });
+
+ it('SPACE selects/deselects the currently focused chip', () => {
+ let FOCUS_EVENT: Event = {} as Event;
+ let SPACE_EVENT: KeyboardEvent = new FakeEvent(SPACE) as KeyboardEvent;
+ let firstChip: MdChip = chips.toArray()[0];
+
+ spyOn(testComponent, 'chipSelect');
+ spyOn(testComponent, 'chipDeselect');
+
+ // Make sure we have the first chip focused
+ chipListInstance.focus(FOCUS_EVENT);
+
+ // Use the spacebar to select the chip
+ chipListInstance._keydown(SPACE_EVENT);
+ fixture.detectChanges();
+
+ expect(firstChip.selected).toBeTruthy();
+ expect(testComponent.chipSelect).toHaveBeenCalledTimes(1);
+ expect(testComponent.chipSelect).toHaveBeenCalledWith(0);
+
+ // Use the spacebar to deselect the chip
+ chipListInstance._keydown(SPACE_EVENT);
+ fixture.detectChanges();
+
+ expect(firstChip.selected).toBeFalsy();
+ expect(testComponent.chipDeselect).toHaveBeenCalledTimes(1);
+ expect(testComponent.chipDeselect).toHaveBeenCalledWith(0);
+ });
+ });
+
+ describe('when selectable is false', () => {
+ beforeEach(() => {
+ testComponent.selectable = false;
+ fixture.detectChanges();
+ });
+
+ it('SPACE ignores selection', () => {
+ let FOCUS_EVENT: Event = {} as Event;
+ let SPACE_EVENT: KeyboardEvent = new FakeEvent(SPACE) as KeyboardEvent;
+ let firstChip: MdChip = chips.toArray()[0];
+
+ spyOn(testComponent, 'chipSelect');
+
+ // Make sure we have the first chip focused
+ chipListInstance.focus(FOCUS_EVENT);
+
+ // Use the spacebar to attempt to select the chip
+ chipListInstance._keydown(SPACE_EVENT);
+ fixture.detectChanges();
+
+ expect(firstChip.selected).toBeFalsy();
+ expect(testComponent.chipSelect).not.toHaveBeenCalled();
+ });
+ });
+
+ });
+
});
@Component({
template: `
-
- {{name}} 1
- {{name}} 2
- {{name}} 3
- {{name}} 4
- {{name}} 5
-
- `
+
+
+
+
+ {{name}} {{i + 1}}
+
+
+
+ `
})
class StaticChipList {
- name: 'Test';
+ name: string = 'Test';
+ selectable: boolean = true;
remove: Number;
+
+ chipSelect(index: Number) {}
+ chipDeselect(index: Number) {}
}
diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts
index aebd396ac829..a37f8aab3b2b 100644
--- a/src/lib/chips/chip-list.ts
+++ b/src/lib/chips/chip-list.ts
@@ -4,6 +4,7 @@ import {
Component,
ContentChildren,
ElementRef,
+ Input,
ModuleWithProviders,
NgModule,
QueryList,
@@ -12,6 +13,8 @@ import {
import {MdChip} from './chip';
import {ListKeyManager} from '../core/a11y/list-key-manager';
+import {coerceBooleanProperty} from '../core/coercion/boolean-property';
+import {SPACE} from '../core/keyboard/keycodes';
/**
* A material design chips component (named ChipList for it's similarity to the List component).
@@ -34,8 +37,8 @@ import {ListKeyManager} from '../core/a11y/list-key-manager';
'class': 'md-chip-list',
// Events
- '(focus)': '_keyManager.focusFirstItem()',
- '(keydown)': 'keydown($event)'
+ '(focus)': 'focus($event)',
+ '(keydown)': '_keydown($event)'
},
queries: {
chips: new ContentChildren(MdChip)
@@ -49,29 +52,85 @@ export class MdChipList implements AfterContentInit {
/** Track which chips we're listening to for focus/destruction. */
private _subscribed: WeakMap = new WeakMap();
+ /** Whether or not the chip is selectable. */
+ protected _selectable: boolean = true;
+
/** The ListKeyManager which handles focus. */
_keyManager: ListKeyManager;
/** The chip components contained within this chip list. */
chips: QueryList;
- constructor(private _elementRef: ElementRef) {}
+ constructor(private _elementRef: ElementRef) {
+ }
ngAfterContentInit(): void {
this._keyManager = new ListKeyManager(this.chips).withFocusWrap();
// Go ahead and subscribe all of the initial chips
- this.subscribeChips(this.chips);
+ this._subscribeChips(this.chips);
// When the list changes, re-subscribe
this.chips.changes.subscribe((chips: QueryList) => {
- this.subscribeChips(chips);
+ this._subscribeChips(chips);
});
}
+ /**
+ * Whether or not this chip is selectable. When a chip is not selectable,
+ * it's selected state is always ignored.
+ */
+ @Input() get selectable(): boolean {
+ return this._selectable;
+ }
+
+ set selectable(value: boolean) {
+ this._selectable = coerceBooleanProperty(value);
+ }
+
+ /**
+ * Programmatically focus the chip list. This in turn focuses the first non-disabled chip in this
+ * chip list.
+ *
+ * TODO: ARIA says this should focus the first `selected` chip.
+ */
+ focus(event: Event) {
+ this._keyManager.focusFirstItem();
+ }
+
/** Pass relevant key presses to our key manager. */
- keydown(event: KeyboardEvent) {
- this._keyManager.onKeydown(event);
+ _keydown(event: KeyboardEvent) {
+ switch (event.keyCode) {
+ case SPACE:
+ // If we are selectable, toggle the focused chip
+ if (this.selectable) {
+ this._toggleSelectOnFocusedChip();
+ }
+
+ // Always prevent space from scrolling the page since the list has focus
+ event.preventDefault();
+ break;
+ default:
+ this._keyManager.onKeydown(event);
+ }
+ }
+
+ /** Toggles the selected state of the currently focused chip. */
+ protected _toggleSelectOnFocusedChip(): void {
+ // Allow disabling of chip selection
+ if (!this.selectable) {
+ return;
+ }
+
+ let focusedIndex = this._keyManager.focusedItemIndex;
+
+ if (this._isValidIndex(focusedIndex)) {
+ let focusedChip: MdChip = this.chips.toArray()[focusedIndex];
+
+ if (focusedChip) {
+ focusedChip.toggleSelected();
+ }
+ }
}
/**
@@ -80,8 +139,8 @@ export class MdChipList implements AfterContentInit {
*
* @param chips The list of chips to be subscribed.
*/
- protected subscribeChips(chips: QueryList): void {
- chips.forEach(chip => this.addChip(chip));
+ protected _subscribeChips(chips: QueryList): void {
+ chips.forEach(chip => this._addChip(chip));
}
/**
@@ -92,7 +151,7 @@ export class MdChipList implements AfterContentInit {
* @param chip The chip to be subscribed (or checked for existing
* subscription).
*/
- protected addChip(chip: MdChip) {
+ protected _addChip(chip: MdChip) {
// If we've already been subscribed to a parent, do nothing
if (this._subscribed.has(chip)) {
return;
@@ -102,7 +161,7 @@ export class MdChipList implements AfterContentInit {
chip.onFocus.subscribe(() => {
let chipIndex: number = this.chips.toArray().indexOf(chip);
- if (this.isValidIndex(chipIndex)) {
+ if (this._isValidIndex(chipIndex)) {
this._keyManager.updateFocusedItemIndex(chipIndex);
}
});
@@ -111,7 +170,7 @@ export class MdChipList implements AfterContentInit {
chip.destroy.subscribe(() => {
let chipIndex: number = this.chips.toArray().indexOf(chip);
- if (this.isValidIndex(chipIndex)) {
+ if (this._isValidIndex(chipIndex)) {
// Check whether the chip is the last item
if (chipIndex < this.chips.length - 1) {
this._keyManager.setFocus(chipIndex);
@@ -133,7 +192,7 @@ export class MdChipList implements AfterContentInit {
* @param index The index to be checked.
* @returns {boolean} True if the index is valid for our list of chips.
*/
- private isValidIndex(index: number): boolean {
+ private _isValidIndex(index: number): boolean {
return index >= 0 && index < this.chips.length;
}
diff --git a/src/lib/chips/chip.spec.ts b/src/lib/chips/chip.spec.ts
index 0b623e3bb3ce..9797909d7c65 100644
--- a/src/lib/chips/chip.spec.ts
+++ b/src/lib/chips/chip.spec.ts
@@ -1,7 +1,7 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
-import {MdChipList, MdChip, MdChipsModule} from './index';
+import {MdChipList, MdChip, MdChipEvent, MdChipsModule} from './index';
describe('Chips', () => {
let fixture: ComponentFixture;
@@ -86,6 +86,28 @@ describe('Chips', () => {
expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1);
});
+
+ it('allows color customization', () => {
+ expect(chipNativeElement.classList).toContain('md-primary');
+
+ testComponent.color = 'warn';
+ fixture.detectChanges();
+
+ expect(chipNativeElement.classList).not.toContain('md-primary');
+ expect(chipNativeElement.classList).toContain('md-warn');
+ });
+
+ it('allows selection', () => {
+ spyOn(testComponent, 'chipSelect');
+ expect(chipNativeElement.classList).not.toContain('md-chip-selected');
+
+ testComponent.selected = true;
+ fixture.detectChanges();
+
+ expect(chipNativeElement.classList).toContain('md-chip-selected');
+ expect(testComponent.chipSelect).toHaveBeenCalledWith({ chip: chipInstance });
+ });
+
});
});
});
@@ -94,20 +116,30 @@ describe('Chips', () => {
template: `
-
+
{{name}}
`
})
class SingleChip {
- name: String = 'Test';
- shouldShow: Boolean = true;
+ name: string = 'Test';
+ color: string = 'primary';
+ selected: boolean = false;
+ shouldShow: boolean = true;
+
+ chipFocus(event: MdChipEvent) {
+ }
+
+ chipDestroy(event: MdChipEvent) {
+ }
- chipFocus() {
+ chipSelect(event: MdChipEvent) {
}
- chipDestroy() {
+ chipDeselect(event: MdChipEvent) {
}
}
diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts
index 81cd0f48914d..823ce71fcdbf 100644
--- a/src/lib/chips/chip.ts
+++ b/src/lib/chips/chip.ts
@@ -17,7 +17,7 @@ export interface MdChipEvent {
}
/**
- * A material design styled Chip component. Used inside the ChipList component.
+ * A material design styled Chip component. Used inside the MdChipList component.
*/
@Component({
selector: 'md-basic-chip, [md-basic-chip], md-chip, [md-chip]',
@@ -26,6 +26,7 @@ export interface MdChipEvent {
'tabindex': '-1',
'role': 'option',
+ '[class.md-chip-selected]': 'selected',
'[attr.disabled]': 'disabled',
'[attr.aria-disabled]': '_isAriaDisabled',
@@ -34,31 +35,37 @@ export interface MdChipEvent {
})
export class MdChip implements MdFocusable, OnInit, OnDestroy {
- /* Whether or not the chip is disabled. */
+ /** Whether or not the chip is disabled. Disabled chips cannot be focused. */
protected _disabled: boolean = null;
- /**
- * Emitted when the chip is focused.
- */
+ /** Whether or not the chip is selected. */
+ protected _selected: boolean = false;
+
+ /** The palette color of selected chips. */
+ protected _color: string = 'primary';
+
+ /** Emitted when the chip is focused. */
onFocus = new EventEmitter();
- /**
- * Emitted when the chip is destroyed.
- */
+ /** Emitted when the chip is selected. */
+ @Output() select = new EventEmitter();
+
+ /** Emitted when the chip is deselected. */
+ @Output() deselect = new EventEmitter();
+
+ /** Emitted when the chip is destroyed. */
@Output() destroy = new EventEmitter();
- constructor(protected _renderer: Renderer, protected _elementRef: ElementRef) {}
+ constructor(protected _renderer: Renderer, protected _elementRef: ElementRef) {
+ }
ngOnInit(): void {
- let el: HTMLElement = this._elementRef.nativeElement;
-
- if (el.nodeName.toLowerCase() == 'md-chip' || el.hasAttribute('md-chip')) {
- el.classList.add('md-chip');
- }
+ this._addDefaultCSSClass();
+ this._updateColor(this._color);
}
ngOnDestroy(): void {
- this.destroy.emit({ chip: this });
+ this.destroy.emit({chip: this});
}
/** Whether or not the chip is disabled. */
@@ -76,10 +83,40 @@ export class MdChip implements MdFocusable, OnInit, OnDestroy {
return String(coerceBooleanProperty(this.disabled));
}
+ /** Whether or not this chip is selected. */
+ @Input() get selected(): boolean {
+ return this._selected;
+ }
+
+ set selected(value: boolean) {
+ this._selected = coerceBooleanProperty(value);
+
+ if (this._selected) {
+ this.select.emit({chip: this});
+ } else {
+ this.deselect.emit({chip: this});
+ }
+ }
+
+ /** Toggles the current selected state of this chip. */
+ toggleSelected(): boolean {
+ this.selected = !this.selected;
+ return this.selected;
+ }
+
+ /** The color of the chip. Can be `primary`, `accent`, or `warn`. */
+ @Input() get color(): string {
+ return this._color;
+ }
+
+ set color(value: string) {
+ this._updateColor(value);
+ }
+
/** Allows for programmatic focusing of the chip. */
focus(): void {
this._renderer.invokeElementMethod(this._elementRef.nativeElement, 'focus');
- this.onFocus.emit({ chip: this });
+ this.onFocus.emit({chip: this});
}
/** Ensures events fire properly upon click. */
@@ -92,4 +129,27 @@ export class MdChip implements MdFocusable, OnInit, OnDestroy {
this.focus();
}
}
+
+ /** Initializes the appropriate CSS classes based on the chip type (basic or standard). */
+ private _addDefaultCSSClass() {
+ let el: HTMLElement = this._elementRef.nativeElement;
+
+ if (el.nodeName.toLowerCase() == 'md-chip' || el.hasAttribute('md-chip')) {
+ el.classList.add('md-chip');
+ }
+ }
+
+ /** Updates the private _color variable and the native element. */
+ private _updateColor(newColor: string) {
+ this._setElementColor(this._color, false);
+ this._setElementColor(newColor, true);
+ this._color = newColor;
+ }
+
+ /** Sets the md-color on the native element. */
+ private _setElementColor(color: string, isAdd: boolean) {
+ if (color != null && color != '') {
+ this._renderer.setElementClass(this._elementRef.nativeElement, `md-${color}`, isAdd);
+ }
+ }
}
diff --git a/src/lib/core/a11y/list-key-manager.spec.ts b/src/lib/core/a11y/list-key-manager.spec.ts
index 2d2e14eefa53..6a75c1be87ce 100644
--- a/src/lib/core/a11y/list-key-manager.spec.ts
+++ b/src/lib/core/a11y/list-key-manager.spec.ts
@@ -15,7 +15,7 @@ class FakeQueryList extends QueryList {
}
}
-class FakeEvent {
+export class FakeEvent {
defaultPrevented: boolean = false;
constructor(public keyCode: number) {}
preventDefault() {