\ No newline at end of file
diff --git a/src/demo-app/chips/chips-demo.scss b/src/demo-app/chips/chips-demo.scss
new file mode 100644
index 000000000000..c0c2af7666c8
--- /dev/null
+++ b/src/demo-app/chips/chips-demo.scss
@@ -0,0 +1,6 @@
+.chips-demo {
+ .md-chip-list-stacked {
+ display: block;
+ max-width: 200px;
+ }
+}
\ No newline at end of file
diff --git a/src/demo-app/chips/chips-demo.ts b/src/demo-app/chips/chips-demo.ts
new file mode 100644
index 000000000000..b81acc4654b5
--- /dev/null
+++ b/src/demo-app/chips/chips-demo.ts
@@ -0,0 +1,41 @@
+import {Component} from '@angular/core';
+
+export interface Person {
+ name: string;
+}
+
+@Component({
+ moduleId: module.id,
+ selector: 'chips-demo',
+ templateUrl: 'chips-demo.html',
+ styleUrls: ['chips-demo.css']
+})
+export class ChipsDemo {
+ visible: boolean = true;
+ color: string = '';
+
+ people: Person[] = [
+ { name: 'Kara' },
+ { name: 'Jeremy' },
+ { name: 'Topher' },
+ { name: 'Elad' },
+ { name: 'Kristiyan' },
+ { name: 'Paul' }
+ ];
+ favorites: Person[] = [];
+
+ alert(message: string): void {
+ alert(message);
+ }
+
+ add(input: HTMLInputElement): void {
+ if (input.value && input.value.trim() != '') {
+ this.people.push({ name: input.value.trim() });
+ input.value = '';
+ }
+ }
+
+ toggleVisible(): void {
+ this.visible = false;
+ }
+}
diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts
index 5f4a02dc5f68..ec35c947b742 100644
--- a/src/demo-app/demo-app-module.ts
+++ b/src/demo-app/demo-app-module.ts
@@ -13,6 +13,7 @@ import {IconDemo} from './icon/icon-demo';
import {GesturesDemo} from './gestures/gestures-demo';
import {InputDemo} from './input/input-demo';
import {CardDemo} from './card/card-demo';
+import {ChipsDemo} from './chips/chips-demo';
import {RadioDemo} from './radio/radio-demo';
import {ButtonToggleDemo} from './button-toggle/button-toggle-demo';
import {ProgressCircleDemo} from './progress-circle/progress-circle-demo';
@@ -49,6 +50,7 @@ import {ProjectionDemo, ProjectionTestComponent} from './projection/projection-d
ButtonDemo,
ButtonToggleDemo,
CardDemo,
+ ChipsDemo,
CheckboxDemo,
DemoApp,
DialogDemo,
diff --git a/src/demo-app/demo-app/demo-app.ts b/src/demo-app/demo-app/demo-app.ts
index ae2ab491bdf6..2b229a414054 100644
--- a/src/demo-app/demo-app/demo-app.ts
+++ b/src/demo-app/demo-app/demo-app.ts
@@ -23,6 +23,7 @@ export class DemoApp {
{name: 'Button', route: 'button'},
{name: 'Button Toggle', route: 'button-toggle'},
{name: 'Card', route: 'card'},
+ {name: 'Chips', route: 'chips'},
{name: 'Checkbox', route: 'checkbox'},
{name: 'Dialog', route: 'dialog'},
{name: 'Gestures', route: 'gestures'},
diff --git a/src/demo-app/demo-app/routes.ts b/src/demo-app/demo-app/routes.ts
index f2e1cf7b68e9..285be668b898 100644
--- a/src/demo-app/demo-app/routes.ts
+++ b/src/demo-app/demo-app/routes.ts
@@ -22,6 +22,7 @@ import {SlideToggleDemo} from '../slide-toggle/slide-toggle-demo';
import {SliderDemo} from '../slider/slider-demo';
import {RadioDemo} from '../radio/radio-demo';
import {CardDemo} from '../card/card-demo';
+import {ChipsDemo} from '../chips/chips-demo';
import {MenuDemo} from '../menu/menu-demo';
import {RippleDemo} from '../ripple/ripple-demo';
import {DialogDemo} from '../dialog/dialog-demo';
@@ -34,6 +35,7 @@ export const DEMO_APP_ROUTES: Routes = [
{path: '', component: Home},
{path: 'button', component: ButtonDemo},
{path: 'card', component: CardDemo},
+ {path: 'chips', component: ChipsDemo},
{path: 'radio', component: RadioDemo},
{path: 'select', component: SelectDemo},
{path: 'sidenav', component: SidenavDemo},
diff --git a/src/lib/chips/README.md b/src/lib/chips/README.md
new file mode 100644
index 000000000000..ff3f3f8a0f51
--- /dev/null
+++ b/src/lib/chips/README.md
@@ -0,0 +1,201 @@
+# md-chip-list
+
+`md-chip-list` provides a horizontal display of (optionally) selectable, addable, and removable,
+items and an input to create additional ones (again; optional). You can read more about chips
+in the [Material Design spec](https://material.google.com/components/chips.html).
+
+## Requirements
+
+1. Show a static list of chips with proper styling
+2. Show a dynamic list of chips with an Input
+3. Show a dynamic list of chips with an Input/Autocomplete, or a Select
+
+#### Additional Requirements
+
+1. A Chips' Autocomplete or Select should not show items for an existing chip
+
+## Usage
+
+### Static Chips
+
+Static chips can be used to inform a user about a list of existing, unmodifiable, items.
+
+##### static-chips-1.html
+```html
+
+ Baseball
+ Basketball
+ Football
+
+```
+
+Alternatively, you can apply the chip styling to an existing element and this will handle focus and
+selection events.
+
+##### static-chips-2.html
+
+```html
+
+
+
+```
+
+### Dynamic Chips
+
+If you want any of the dynamic functionality supplied by chips, you may utilize the `md-chip-input`
+component (and associated `(chipAdded)` event) in tandem with the `(remove)` event of the `md-chip`
+component.
+
+##### dynamic-chips-1.html
+
+The most basic version simply utilizes the `(chipAdded)` event to push the requested chip onto
+the list. This event handles the keyboard interaction and would also work in tandem with the
+`[chip-separators]` option that allows user-defined separator keys (like `,` or `;`).
+
+```html
+
+
+ {{tag.name}}
+
+
+
+
+```
+
+
+##### autocomplete-chips.html
+
+If you would like to help users by providing some filtering of predefined, options,
+you can simply apply the `md-autocomplete`.
+
+```html
+
+
+ {{tag.name}}
+
+
+
+
+
+
+
+
+```
+
+_**Note:** Notice how this `(remove)` utilizes the `tag` of the `*ngFor` rather than the
+`$chip` variable. For chips which are more complex than strings, this may be desirable._
+
+##### select-chips.html
+
+Finally, you could use an `md-select` instead of an `md-autocomplete` if you do not wish
+the user to be able to create new/unknown chips.
+
+```html
+
+
+ {{favorite.name}}
+
+
+
+
+
+ {{ food.viewValue }}
+
+
+```
+
+## Templates
+
+Obviously, these kinds of complex controls may be a bit burdensome for many developers,
+so I propose we add some "high-level" template components which can be used to make
+development easier and more in-line with what most users are expecting, while still
+providing full flexibility for users who want a more custom component.
+
+##### md-static-chips
+
+The most basic example would be static chips bound to a list of elements which provides
+the `chip-text` expression allowing the component to render the chip without requiring
+a template.
+
+```html
+
+
+```
+
+##### md-dynamic-chips
+
+There are two common variations of the dynamic chips:
+
+1. Chips with just an input
+2. Chips with an input associated with an autocomplete
+
+Both are listed below.
+
+*only the input*
+```html
+
+
+```
+
+*with an autocomplete*
+```html
+
+
+```
+
+There are two methods for providing the suggestions:
+
+1. `[suggestions]="mySuggestions"` - In this mode, the suggestions are bound to an array
+ or list and the filtering is handled by the `md-dynamic-chips` component.
+2. `[suggestions]="getSuggestions($query)"` - In this mode, the suggestions are filtered
+ by user-provided code and can immediately return with the value, or it can return a
+ promise for evaluation at a later time.
+
+The various behaviors could be also be controlled using the `capabilities` option.
+
+```html
+
+
+```
+
+In this way, you can easily customize which behaviors are allowed at any given time.
+
+_**Note:** We should also support the `ng-disabled`/`disabled` parameter which turns off all
+capabilities._
+
+##### md-contact-chips
+
+Finally, we would offer a simple component for the common case of the Contact Chips which
+appears multiple times in the spec.
+
+```html
+
+
+```
+
+The `getInfo($chip)` method should return an object with `name`, `email` and `image` properties
+to be utilized by the contact chips during rendering. If not provided, the default would simply be
+
+```js
+{
+ name: $chip.name,
+ email: $chip.email,
+ image: $chip.image
+}
+````
+
+Similar to the `md-dynamic-chips` and the suggestions, the developer could also supply
+`[contacts]="getContacts($query)"` to asynchronously search for the contacts, or to have
+more control over the filtered list.
diff --git a/src/lib/chips/_chips-theme.scss b/src/lib/chips/_chips-theme.scss
new file mode 100644
index 000000000000..da47e43120af
--- /dev/null
+++ b/src/lib/chips/_chips-theme.scss
@@ -0,0 +1,29 @@
+@import '../core/theming/theming';
+
+@mixin md-chips-theme($theme) {
+ $is-dark-theme: map-get($theme, is-dark);
+ $primary: map-get($theme, primary);
+ $accent: map-get($theme, accent);
+ $warn: map-get($theme, warn);
+ $background: map-get($theme, background);
+
+
+ .md-chip.selected {
+ // TODO: Based on spec, this should be #808080, but we can only use md-contrast with a palette
+ background-color: md-color($md-grey, 600);
+ color: md-contrast($md-grey, 600);
+
+ &.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-key-manager.spec.ts b/src/lib/chips/chip-list-key-manager.spec.ts
new file mode 100644
index 000000000000..4cf0d88372e6
--- /dev/null
+++ b/src/lib/chips/chip-list-key-manager.spec.ts
@@ -0,0 +1,79 @@
+import {QueryList} from '@angular/core';
+import {async, TestBed} from '@angular/core/testing';
+import {MdBasicChip} from './index';
+import {ChipListKeyManager} from './chip-list-key-manager';
+
+class FakeChip extends MdBasicChip {
+ constructor() {
+ // Pass in null for the renderer/elementRef
+ super(null, null);
+ }
+
+ // Override the focus() method to NOT call the underlying renderer (which is null)
+ focus() {
+ this.didfocus.emit();
+ }
+}
+
+describe('ChipListKeyManager', () => {
+ let items: QueryList;
+ let manager: ChipListKeyManager;
+
+ beforeEach(async(() => {
+ items = new QueryList();
+ items.reset([
+ new FakeChip(),
+ new FakeChip(),
+ new FakeChip(),
+ new FakeChip(),
+ new FakeChip()
+ ]);
+
+ manager = new ChipListKeyManager(items);
+
+ TestBed.compileComponents();
+ }));
+
+ describe('basic behaviors', () => {
+ it('watches for chip focus', () => {
+ let array = items.toArray();
+ let lastIndex = array.length - 1;
+ let lastItem = array[lastIndex];
+
+ lastItem.focus();
+
+ expect(manager.focusedItemIndex).toBe(lastIndex);
+ });
+
+ describe('on chip destroy', () => {
+ it('focuses the next item', () => {
+ let array = items.toArray();
+ let midItem = array[2];
+
+ // Focus the middle item
+ midItem.focus();
+
+ // Destroy the middle item
+ midItem.destroy.emit();
+
+ // It focuses the 4th item (now at index 2)
+ expect(manager.focusedItemIndex).toEqual(2);
+ });
+
+ it('focuses the previous item', () => {
+ let array = items.toArray();
+ let lastIndex = array.length - 1;
+ let lastItem = array[lastIndex];
+
+ // Focus the last item
+ lastItem.focus();
+
+ // Destroy the last item
+ lastItem.destroy.emit();
+
+ // It focuses the next-to-last item
+ expect(manager.focusedItemIndex).toEqual(lastIndex - 1);
+ });
+ });
+ });
+});
diff --git a/src/lib/chips/chip-list-key-manager.ts b/src/lib/chips/chip-list-key-manager.ts
new file mode 100644
index 000000000000..9890106862cb
--- /dev/null
+++ b/src/lib/chips/chip-list-key-manager.ts
@@ -0,0 +1,72 @@
+import {QueryList} from '@angular/core';
+import {ListKeyManager} from '../core/a11y/list-key-manager';
+import {MdBasicChip} from './chip';
+
+export class ChipListKeyManager extends ListKeyManager {
+ private _subscribed: MdBasicChip[] = [];
+
+ constructor(private _chips: QueryList) {
+ super(_chips);
+
+ this.subscribeChips(this._chips);
+
+ this._chips.changes.subscribe((chips: QueryList) => {
+ this.subscribeChips(chips);
+ });
+ }
+
+ onKeydown(event: KeyboardEvent): void {
+ let focusedChip: MdBasicChip;
+
+ if (this.isValidIndex(this.focusedItemIndex)) {
+ focusedChip = this._chips.toArray()[this.focusedItemIndex];
+ }
+
+ super.onKeydown(event);
+ }
+
+ protected subscribeChips(chips: QueryList): void {
+ chips.forEach((chip: MdBasicChip) => {
+ this.addChip(chip);
+ });
+ }
+
+ protected addChip(chip: MdBasicChip) {
+ // If we've already been subscribed to a parent, do nothing
+ if (this._subscribed.indexOf(chip) > -1) {
+ return;
+ }
+
+ // Watch for focus events outside of the keyboard navigation
+ chip.didfocus.subscribe(() => {
+ let chipIndex: number = this._chips.toArray().indexOf(chip);
+
+ if (this.isValidIndex(chipIndex)) {
+ this.setFocus(chipIndex, false);
+ }
+ });
+
+ // On destroy, remove the item from our list, and check focus
+ chip.destroy.subscribe(() => {
+ let chipIndex: number = this._chips.toArray().indexOf(chip);
+
+ if (this.isValidIndex(chipIndex)) {
+ //
+ if (chipIndex < this._chips.length - 1) {
+ this.setFocus(chipIndex);
+ } else if (chipIndex - 1 >= 0) {
+ this.setFocus(chipIndex - 1);
+ }
+ }
+
+ this._subscribed.splice(this._subscribed.indexOf(chip), 1);
+ chip.destroy.unsubscribe();
+ });
+
+ this._subscribed.push(chip);
+ }
+
+ private isValidIndex(index: number): boolean {
+ return index >= 0 && index < this._chips.length;
+ }
+}
diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts
new file mode 100644
index 000000000000..14c1b69fa674
--- /dev/null
+++ b/src/lib/chips/chip-list.spec.ts
@@ -0,0 +1,53 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Component, DebugElement} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {MdChipList, MdChipsModule} from './index';
+
+describe('MdChip', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdChipsModule.forRoot()],
+ declarations: [
+ StaticChipList
+ ]
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ describe('basic behaviors', () => {
+ let chipListDebugElement: DebugElement;
+ let chipListNativeElement: HTMLElement;
+ let chipListInstance: MdChipList;
+ let testComponent: StaticChipList;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StaticChipList);
+ fixture.detectChanges();
+
+ chipListDebugElement = fixture.debugElement.query(By.directive(MdChipList));
+ chipListNativeElement = chipListDebugElement.nativeElement;
+ chipListInstance = chipListDebugElement.componentInstance;
+ testComponent = fixture.debugElement.componentInstance;
+ });
+
+ it('adds the `md-chip-list` class', () => {
+ expect(chipListNativeElement.classList).toContain('md-chip-list');
+ });
+ });
+});
+
+@Component({
+ template: `
+
+ {{name}} 1
+ {{name}} 2
+ {{name}} 3
+
+ `
+})
+class StaticChipList {
+ name: 'Test';
+}
diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts
new file mode 100644
index 000000000000..96278afc113a
--- /dev/null
+++ b/src/lib/chips/chip-list.ts
@@ -0,0 +1,77 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ ContentChildren,
+ ElementRef,
+ ModuleWithProviders,
+ NgModule,
+ QueryList,
+ ViewEncapsulation
+} from '@angular/core';
+
+import {MdBasicChip, MdChip} from './chip';
+import {ChipListKeyManager} from './chip-list-key-manager';
+
+export const MD_CHIP_LIST_COMPONENT_CONFIG: Component = {
+ moduleId: module.id,
+ selector: 'md-chip-list, [md-chip-list]',
+ template: `
`,
+ host: {
+ // Properties
+ 'tabindex': '0',
+ 'role': 'listbox',
+
+ // Events
+ '(focus)': 'focus($event)',
+ '(keydown)': 'keydown($event)'
+ },
+ queries: {
+ chips: new ContentChildren(MdBasicChip)
+ },
+ styleUrls: ['chips.css'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush
+};
+
+@Component(MD_CHIP_LIST_COMPONENT_CONFIG)
+export class MdChipList {
+
+ private _keyManager: ChipListKeyManager;
+
+ public chips: QueryList;
+
+ constructor(private _elementRef: ElementRef) {
+ }
+
+ ngAfterContentInit(): void {
+ this._elementRef.nativeElement.classList.add('md-chip-list');
+
+ this._keyManager = new ChipListKeyManager(this.chips).withFocusWrap();
+ }
+
+
+ /********************
+ * EVENTS
+ ********************/
+ focus(event: Event) {
+ this._keyManager.focusFirstItem();
+ }
+
+ keydown(event: KeyboardEvent) {
+ this._keyManager.onKeydown(event);
+ }
+}
+
+@NgModule({
+ imports: [],
+ exports: [MdChipList, MdBasicChip, MdChip],
+ declarations: [MdChipList, MdBasicChip, MdChip]
+})
+export class MdChipsModule {
+ static forRoot(): ModuleWithProviders {
+ return {
+ ngModule: MdChipsModule,
+ providers: []
+ };
+ }
+}
diff --git a/src/lib/chips/chip.spec.ts b/src/lib/chips/chip.spec.ts
new file mode 100644
index 000000000000..710922a3eb87
--- /dev/null
+++ b/src/lib/chips/chip.spec.ts
@@ -0,0 +1,78 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Component, DebugElement} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {MdChip, MdChipEvent, MdChipsModule} from './index';
+
+describe('MdChip', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdChipsModule.forRoot()],
+ declarations: [
+ SingleChip
+ ]
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ describe('basic behaviors', () => {
+ let chipDebugElement: DebugElement;
+ let chipNativeElement: HTMLElement;
+ let chipInstance: MdChip;
+ let testComponent: SingleChip;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SingleChip);
+ fixture.detectChanges();
+
+ chipDebugElement = fixture.debugElement.query(By.directive(MdChip));
+ chipNativeElement = chipDebugElement.nativeElement;
+ chipInstance = chipDebugElement.componentInstance;
+ testComponent = fixture.debugElement.componentInstance;
+ });
+
+ it('adds the `md-chip` class', () => {
+ expect(chipNativeElement.classList).toContain('md-chip');
+ });
+
+ it('emits didfocus on focus', () => {
+ spyOn(testComponent, 'chipFocus');
+
+ chipInstance.focus();
+
+ expect(testComponent.chipFocus).toHaveBeenCalledTimes(1);
+ });
+
+ it('emits didfocus on click', () => {
+ spyOn(testComponent, 'chipFocus');
+
+ chipNativeElement.click();
+
+ expect(testComponent.chipFocus).toHaveBeenCalledTimes(1);
+ });
+
+ it('emits destroy on destruction', () => {
+ spyOn(testComponent, 'chipDestroy');
+
+ // Fake a destroy callback
+ chipInstance.ngOnDestroy();
+
+ expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1);
+ });
+ });
+});
+
+@Component({
+ template: `
+
+ {{name}}
+ `
+})
+class SingleChip {
+ name: string = 'Test';
+
+ chipFocus(event: MdChipEvent) {}
+ chipDestroy(event: MdChipEvent) {}
+}
diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts
new file mode 100644
index 000000000000..c687adca53ad
--- /dev/null
+++ b/src/lib/chips/chip.ts
@@ -0,0 +1,107 @@
+import {
+ // Classes
+ Component,
+ ElementRef,
+ EventEmitter,
+ OnDestroy,
+ Renderer,
+
+ // Functions
+ forwardRef
+} from '@angular/core';
+
+import {MdFocusable} from '../core/a11y/list-key-manager';
+
+export class MdChipEvent {
+ chip: MdBasicChip;
+}
+
+export const MD_BASIC_CHIP_COMPONENT_CONFIG: Component = {
+ selector: 'md-basic-chip, [md-basic-chip]',
+ template: ``,
+ host: {
+ // Properties
+ 'tabindex': '-1',
+ 'role': 'option',
+
+ // Attributes
+ '[attr.disabled]': 'disabled',
+ '[attr.aria-disabled]': 'isAriaDisabled',
+
+ // Events
+ '(click)': 'click($event)',
+ },
+ inputs: ['disabled'],
+ outputs: ['didfocus', 'destroy']
+};
+
+@Component(MD_BASIC_CHIP_COMPONENT_CONFIG)
+export class MdBasicChip implements MdFocusable, OnDestroy {
+
+ protected _disabled: boolean = false;
+
+ // Declare outputs
+ public didfocus = new EventEmitter();
+ public destroy = new EventEmitter();
+
+ constructor(protected _renderer: Renderer, protected _elementRef: ElementRef) {}
+
+ ngAfterContentInit(): void {}
+
+ ngOnDestroy(): void {
+ this.destroy.emit({chip: this});
+ }
+
+ focus(): void {
+ this._renderer.invokeElementMethod(this._elementRef.nativeElement, 'focus');
+ this.didfocus.emit({chip: this});
+ }
+
+ get disabled(): boolean {
+ return this._disabled;
+ }
+
+ set disabled(value: boolean) {
+ this._disabled = (value === false || value === undefined) ? null : true;
+ }
+
+ get isAriaDisabled(): string {
+ return String(this.disabled);
+ }
+
+ click(event: Event) {
+ // No matter what, we should emit the didfocus event
+ this.didfocus.emit({chip: this});
+
+ // Check disabled
+ if (this.disabled) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+}
+
+export const MD_CHIP_COMPONENT_CONFIG: Component = {
+ template: MD_BASIC_CHIP_COMPONENT_CONFIG.template,
+ host: MD_BASIC_CHIP_COMPONENT_CONFIG.host,
+ inputs: MD_BASIC_CHIP_COMPONENT_CONFIG.inputs,
+ outputs: MD_BASIC_CHIP_COMPONENT_CONFIG.outputs,
+
+ selector: 'md-chip, [md-chip]',
+ providers: [{provide: MdBasicChip, useExisting: forwardRef(() => MdChip)}]
+};
+
+@Component(MD_CHIP_COMPONENT_CONFIG)
+export class MdChip extends MdBasicChip {
+
+ constructor(protected _renderer: Renderer, protected _elementRef: ElementRef) {
+ super(_renderer, _elementRef);
+ }
+
+ ngAfterContentInit(): void {
+ super.ngAfterContentInit();
+
+ this._elementRef.nativeElement.classList.add('md-chip');
+ }
+
+}
diff --git a/src/lib/chips/chips.scss b/src/lib/chips/chips.scss
new file mode 100644
index 000000000000..d2f50d84754a
--- /dev/null
+++ b/src/lib/chips/chips.scss
@@ -0,0 +1,82 @@
+$md-chip-vertical-padding: 8px;
+$md-chip-horizontal-padding: 12px;
+$md-chip-font-size: 13px;
+$md-chip-line-height: 16px;
+
+$md-chips-chip-margin: $md-chip-horizontal-padding / 4;
+
+.md-chip-list-wrapper {
+
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: flex-start;
+
+ /*
+ * Only apply the margins to chips
+ */
+ .md-chip {
+ margin: 0 $md-chips-chip-margin 0 $md-chips-chip-margin;
+
+ // Remove the margin from the first element (in both LTR and RTL)
+ &:first-child {
+ margin: {
+ left: 0;
+ right: $md-chips-chip-margin;
+ }
+
+ [dir='rtl'] & {
+ margin: {
+ left: $md-chips-chip-margin;
+ right: 0;
+ }
+ }
+ }
+
+ // Remove the margin from the last element (in both LTR and RTL)
+ &:last-child {
+ margin: {
+ left: $md-chips-chip-margin;
+ right: 0;
+ }
+
+ [dir='rtl'] & {
+ margin: {
+ left: 0;
+ right: $md-chips-chip-margin;
+ }
+ }
+ }
+ }
+}
+
+.md-chip {
+ display: inline-block;
+ padding: $md-chip-vertical-padding $md-chip-horizontal-padding
+ $md-chip-vertical-padding $md-chip-horizontal-padding;
+ border-radius: $md-chip-horizontal-padding * 2;
+
+ background-color: #e0e0e0;
+ color: rgba(0, 0, 0, 0.87);
+ font-size: $md-chip-font-size;
+ line-height: $md-chip-line-height;
+}
+
+.md-chip-list-stacked .md-chip-list-wrapper {
+ display: block;
+
+ .md-chip {
+ display: block;
+ margin: 0;
+ margin-bottom: $md-chip-vertical-padding;
+
+ [dir='rtl'] & {
+ margin: 0;
+ margin-bottom: $md-chip-vertical-padding;
+ }
+
+ &:last-child, [dir='rtl'] &:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
diff --git a/src/lib/chips/index.ts b/src/lib/chips/index.ts
new file mode 100644
index 000000000000..938151ce2bea
--- /dev/null
+++ b/src/lib/chips/index.ts
@@ -0,0 +1,2 @@
+export * from './chip-list';
+export * from './chip';
diff --git a/src/lib/core/a11y/list-key-manager.spec.ts b/src/lib/core/a11y/list-key-manager.spec.ts
index 918f9c049bf5..2eab6118c916 100644
--- a/src/lib/core/a11y/list-key-manager.spec.ts
+++ b/src/lib/core/a11y/list-key-manager.spec.ts
@@ -15,11 +15,13 @@ class FakeQueryList extends QueryList {
}
}
-const DOWN_ARROW_EVENT = { keyCode: DOWN_ARROW } as KeyboardEvent;
-const UP_ARROW_EVENT = { keyCode: UP_ARROW } as KeyboardEvent;
-const TAB_EVENT = { keyCode: TAB } as KeyboardEvent;
-const HOME_EVENT = { keyCode: HOME } as KeyboardEvent;
-const END_EVENT = { keyCode: END } as KeyboardEvent;
+function noop() {}
+
+const DOWN_ARROW_EVENT = { keyCode: DOWN_ARROW, preventDefault: noop } as KeyboardEvent;
+const UP_ARROW_EVENT = { keyCode: UP_ARROW, preventDefault: noop } as KeyboardEvent;
+const TAB_EVENT = { keyCode: TAB, preventDefault: noop } as KeyboardEvent;
+const HOME_EVENT = { keyCode: HOME, preventDefault: noop } as KeyboardEvent;
+const END_EVENT = { keyCode: END, preventDefault: noop } as KeyboardEvent;
describe('ListKeyManager', () => {
let keyManager: ListKeyManager;
@@ -180,6 +182,16 @@ describe('ListKeyManager', () => {
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);
});
+ it('should setFocus() without focusing the element', () => {
+ expect(keyManager.focusedItemIndex)
+ .toBe(0, `Expected focus to be on the first item of the list.`);
+
+ keyManager.setFocus(1, false);
+ expect(keyManager.focusedItemIndex)
+ .toBe(1, `Expected focusedItemIndex to be updated when setFocus() was called.`);
+ expect(itemList.items[1].focus).not.toHaveBeenCalledTimes(1);
+ });
+
it('should focus the first item when focusFirstItem() is called', () => {
keyManager.onKeydown(DOWN_ARROW_EVENT);
keyManager.onKeydown(DOWN_ARROW_EVENT);
diff --git a/src/lib/core/a11y/list-key-manager.ts b/src/lib/core/a11y/list-key-manager.ts
index 8e447ff0c327..59a5da5ebe52 100644
--- a/src/lib/core/a11y/list-key-manager.ts
+++ b/src/lib/core/a11y/list-key-manager.ts
@@ -32,10 +32,18 @@ export class ListKeyManager {
return this;
}
- /** Sets the focus of the list to the item at the index specified. */
- setFocus(index: number): void {
+ /**
+ * Sets the focus of the list to the item at the index specified.
+ *
+ * @param index The index of the item to be focused.
+ * @param focusElement Whether or not to focus the element as well. Defaults to `true`.
+ */
+ setFocus(index: number, focusElement = true): void {
this._focusedItemIndex = index;
- this._items.toArray()[index].focus();
+
+ if (focusElement) {
+ this._items.toArray()[index].focus();
+ }
}
/** Sets the focus properly depending on the key event passed in. */
@@ -43,15 +51,19 @@ export class ListKeyManager {
switch (event.keyCode) {
case DOWN_ARROW:
this.focusNextItem();
+ event.preventDefault();
break;
case UP_ARROW:
this.focusPreviousItem();
+ event.preventDefault();
break;
case HOME:
this.focusFirstItem();
+ event.preventDefault();
break;
case END:
this.focusLastItem();
+ event.preventDefault();
break;
case TAB:
this._tabOut.next(null);
diff --git a/src/lib/core/keyboard/keycodes.ts b/src/lib/core/keyboard/keycodes.ts
index 6204987a0e8a..ab287296e9c8 100644
--- a/src/lib/core/keyboard/keycodes.ts
+++ b/src/lib/core/keyboard/keycodes.ts
@@ -18,3 +18,6 @@ export const END = 35;
export const ENTER = 13;
export const SPACE = 32;
export const TAB = 9;
+
+export const BACKSPACE = 8;
+export const DELETE = 46;
diff --git a/src/lib/core/theming/_all-theme.scss b/src/lib/core/theming/_all-theme.scss
index 084e2585350e..4c7a252ea4c4 100644
--- a/src/lib/core/theming/_all-theme.scss
+++ b/src/lib/core/theming/_all-theme.scss
@@ -4,6 +4,7 @@
@import '../../button-toggle/button-toggle-theme';
@import '../../card/card-theme';
@import '../../checkbox/checkbox-theme';
+@import '../../chips/chips-theme';
@import '../../dialog/dialog-theme';
@import '../../grid-list/grid-list-theme';
@import '../../icon/icon-theme';
@@ -29,6 +30,7 @@
@include md-button-toggle-theme($theme);
@include md-card-theme($theme);
@include md-checkbox-theme($theme);
+ @include md-chips-theme($theme);
@include md-dialog-theme($theme);
@include md-grid-list-theme($theme);
@include md-icon-theme($theme);
diff --git a/src/lib/index.ts b/src/lib/index.ts
index e242a9fc8777..7202a6b57f55 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -4,6 +4,7 @@ export * from './module';
export * from './button/index';
export * from './button-toggle/index';
export * from './card/index';
+export * from './chips/index';
export * from './checkbox/index';
export * from './dialog/index';
export * from './grid-list/index';
diff --git a/src/lib/module.ts b/src/lib/module.ts
index 31d18001e089..1a5d835c1bac 100644
--- a/src/lib/module.ts
+++ b/src/lib/module.ts
@@ -21,6 +21,7 @@ import {MdSidenavModule} from './sidenav/index';
import {MdListModule} from './list/index';
import {MdGridListModule} from './grid-list/index';
import {MdCardModule} from './card/index';
+import {MdChipsModule} from './chips/index';
import {MdIconModule} from './icon/index';
import {MdProgressCircleModule} from './progress-circle/index';
import {MdProgressBarModule} from './progress-bar/index';
@@ -37,6 +38,7 @@ const MATERIAL_MODULES = [
MdButtonModule,
MdButtonToggleModule,
MdCardModule,
+ MdChipsModule,
MdCheckboxModule,
MdDialogModule,
MdGridListModule,
@@ -68,6 +70,7 @@ const MATERIAL_MODULES = [
imports: [
MdButtonModule.forRoot(),
MdCardModule.forRoot(),
+ MdChipsModule.forRoot(),
MdCheckboxModule.forRoot(),
MdGridListModule.forRoot(),
MdInputModule.forRoot(),