Skip to content

Commit

Permalink
feat(chips): Add removal functionality/styling. (#4912)
Browse files Browse the repository at this point in the history
* feat(chips): Add remove functionality/styling.

Add events, styling and keyboard controls to allow removable chips.

 - Add basic styling for a user-provided remove icon.
 - Add keyboard controls for backspace/delete.
 - Add `(remove)` event which is emitted when the remove icon or
   one of the delete keys is pressed.
 - Add `md-chip-remove` directive which can be applied to `<md-icon>`
   (or others) to inform the chip of the remove request.

Add new directive `mdChipInput` for controlling:
 - `(chipAdded)` - Event fired when a chip should be added.
 - `[separatorKeys]` - The list of keycodes that will fire the
   `(chipAdded)` event.
 - `[addOnBlur]` - Whether or not to fire the `(chipAdded)` event
   when the input is blurred.

Additionally, fix some issues with dark theme and add styling/support
for usage inside the `md-input-container` and add styling for focused
chips.

BREAKING CHANGE - The `selectable` property of the `md-chip-list` has
now been moved to `md-chip` to maintain consistency with the new
`removable` option.

If you used the following code,

```html
<md-chip-list [selectable]="selectable">
  <md-chip>My Chip</md-chip>
</md-chip-list>
```

you should switch it to

```html
<md-chip-list>
  <md-chip [selectable]="selectable">My Chip</md-chip>
</md-chip-list>
```

References #120. Closes #3143.
  • Loading branch information
tinayuangao authored and jelbourn committed Jul 14, 2017
1 parent 243b97d commit c82aca9
Show file tree
Hide file tree
Showing 14 changed files with 1,087 additions and 265 deletions.
58 changes: 53 additions & 5 deletions src/demo-app/chips/chips-demo.html
Expand Up @@ -23,11 +23,14 @@ <h4>Advanced</h4>

<md-chip-list selectable="false">
<md-chip color="accent" selected="true">Selected/Colored</md-chip>

<md-chip color="warn" selected="true" *ngIf="visible"
(destroy)="alert('chip destroyed')" (click)="toggleVisible()">
(destroy)="displayMessage('chip destroyed')" (remove)="toggleVisible()">
With Events
<md-icon mdChipRemove>cancel</md-icon>
</md-chip>
</md-chip-list>
<div>{{message}}</div>
</md-card-content>
</md-card>

Expand All @@ -37,16 +40,61 @@ <h4>Advanced</h4>
<md-card-content>
<h4>Input Container</h4>

<md-chip-list>
<md-chip *ngFor="let person of people" [color]="color">
<p>
You can easily put the the <code>&lt;md-chip-list&gt;</code> inside of an
<code>&lt;md-input-container&gt;</code>.
</p>


<md-input-container>
<md-chip-list mdPrefix #chipList1>
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
[removable]="removable" (remove)="remove(person)">
{{person.name}}
<md-icon mdChipRemove *ngIf="removable">cancel</md-icon>
</md-chip>
</md-chip-list>
<input mdInput placeholder="New Contributor..."
[mdChipInputFor]="chipList1"
[mdChipInputSeparatorKeyCodes]="separatorKeysCodes"
[mdChipInputAddOnBlur]="addOnBlur"
(mdChipInputTokenEnd)="add($event)" />
</md-input-container>



<p>
You can also put <code>&lt;md-input-container&gt;</code> outside of an <code>md-chip-list</code>.
With <code>mdChipInput</code> the input work with chip-list
</p>

<md-chip-list #chipList2>
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
[removable]="removable" (remove)="remove(person)">
{{person.name}}
<md-icon mdChipRemove *ngIf="removable">cancel</md-icon>
</md-chip>
</md-chip-list>

<md-input-container>
<input mdInput #input (keyup.enter)="add(input)" placeholder="New Contributor..."/>
<input mdInput placeholder="New Contributor..."
[mdChipInputFor]="chipList2"
[mdChipInputSeparatorKeyCodes]="separatorKeysCodes"
[mdChipInputAddOnBlur]="addOnBlur"
(mdChipInputTokenEnd)="add($event)" />
</md-input-container>

<p>
The example above has overridden the <code>[separatorKeys]</code> input to allow for
<code>ENTER</code>, <code>COMMA</code> and <code>SEMI COLON</code> keys.
</p>

<h4>Options</h4>
<p>
<md-checkbox name="selectable" [(ngModel)]="selectable">Selectable</md-checkbox>
<md-checkbox name="removable" [(ngModel)]="removable">Removable</md-checkbox>
<md-checkbox name="addOnBlur" [(ngModel)]="addOnBlur">Add on Blur</md-checkbox>
</p>

<h4>Stacked Chips</h4>

<p>
Expand Down
14 changes: 13 additions & 1 deletion src/demo-app/chips/chips-demo.scss
Expand Up @@ -20,4 +20,16 @@
.mat-basic-chip {
margin: auto 10px;
}
}

md-chip-list input {
width: 150px;
}

.mat-chip-remove.mat-icon {
font-size: 16px;
width: 1em;
height: 1em;
vertical-align: middle;
cursor: pointer;
}
}
36 changes: 31 additions & 5 deletions src/demo-app/chips/chips-demo.ts
@@ -1,4 +1,7 @@
import {Component} from '@angular/core';
import {MdChipInputEvent, ENTER} from '@angular/material';

const COMMA = 188;

export interface Person {
name: string;
Expand All @@ -18,6 +21,13 @@ export interface DemoColor {
export class ChipsDemo {
visible: boolean = true;
color: string = '';
selectable: boolean = true;
removable: boolean = true;
addOnBlur: boolean = true;
message: string = '';

// Enter, comma, semi-colon
separatorKeysCodes = [ENTER, COMMA, 186];

people: Person[] = [
{ name: 'Kara' },
Expand All @@ -35,17 +45,33 @@ export class ChipsDemo {
{ name: 'Warn', color: 'warn' }
];

alert(message: string): void {
alert(message);
displayMessage(message: string): void {
this.message = message;
}

add(input: HTMLInputElement): void {
if (input.value && input.value.trim() != '') {
this.people.push({ name: input.value.trim() });
add(event: MdChipInputEvent): void {
let input = event.input;
let value = event.value;

// Add our person
if ((value || '').trim()) {
this.people.push({ name: value.trim() });
}

// Reset the input value
if (input) {
input.value = '';
}
}

remove(person: Person): void {
let index = this.people.indexOf(person);

if (index >= 0) {
this.people.splice(index, 1);
}
}

toggleVisible(): void {
this.visible = false;
}
Expand Down
42 changes: 27 additions & 15 deletions src/lib/chips/_chips-theme.scss
Expand Up @@ -6,6 +6,23 @@
$mat-chip-font-size: 13px;
$mat-chip-line-height: 16px;

@mixin mat-chips-theme-color($color) {
@include mat-chips-color(mat-contrast($color, 500), mat-color($color, 500));
}

@mixin mat-chips-color($foreground, $background) {
background-color: $background;
color: $foreground;

.mat-chip-remove {
color: $foreground;
opacity: 0.4;
}

.mat-chip-remove:hover {
opacity: 0.54;
}
}

@mixin mat-chips-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
Expand All @@ -28,34 +45,29 @@ $mat-chip-line-height: 16px;
$selected-background: if($is-dark-theme, mat-color($background, app-bar), #808080);
$selected-foreground: if($is-dark-theme, mat-color($foreground, text), $light-selected-foreground);

.mat-chip:not(.mat-basic-chip) {
background-color: $unselected-background;
color: $unselected-foreground;
.mat-chip {
@include mat-chips-color($unselected-foreground, $unselected-background);
}

.mat-chip.mat-chip-selected:not(.mat-basic-chip) {
background-color: $selected-background;
color: $selected-foreground;
.mat-chip.mat-chip-selected {
@include mat-chips-color($selected-foreground, $selected-background);

&.mat-primary {
background-color: mat-color($primary);
color: mat-color($primary, default-contrast);
@include mat-chips-theme-color($primary);
}

&.mat-accent {
background-color: mat-color($accent);
color: mat-color($accent, default-contrast);
&.mat-warn {
@include mat-chips-theme-color($warn);
}

&.mat-warn {
background-color: mat-color($warn);
color: mat-color($warn, default-contrast);
&.mat-accent {
@include mat-chips-theme-color($accent);
}
}
}

@mixin mat-chips-typography($config) {
.mat-chip:not(.mat-basic-chip) {
.mat-chip {
font-size: $mat-chip-font-size;
line-height: $mat-chip-line-height;
}
Expand Down
120 changes: 120 additions & 0 deletions src/lib/chips/chip-input.spec.ts
@@ -0,0 +1,120 @@
import {async, TestBed, ComponentFixture} from '@angular/core/testing';
import {MdChipsModule} from './index';
import {Component, DebugElement} from '@angular/core';
import {MdChipInput, MdChipInputEvent} from './chip-input';
import {By} from '@angular/platform-browser';
import {Directionality} from '../core';
import {createKeyboardEvent} from '@angular/cdk/testing';

import {ENTER} from '../core/keyboard/keycodes';

const COMMA = 188;

describe('MdChipInput', () => {
let fixture: ComponentFixture<any>;
let testChipInput: TestChipInput;
let inputDebugElement: DebugElement;
let inputNativeElement: HTMLElement;
let chipInputDirective: MdChipInput;

let dir = 'ltr';

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdChipsModule],
declarations: [TestChipInput],
providers: [{
provide: Directionality, useFactory: () => {
return {value: dir.toLowerCase()};
}
}]
});

TestBed.compileComponents();
}));

beforeEach(async(() => {
fixture = TestBed.createComponent(TestChipInput);
testChipInput = fixture.debugElement.componentInstance;
fixture.detectChanges();

inputDebugElement = fixture.debugElement.query(By.directive(MdChipInput));
chipInputDirective = inputDebugElement.injector.get(MdChipInput) as MdChipInput;
inputNativeElement = inputDebugElement.nativeElement;
}));

describe('basic behavior', () => {
it('emits the (chipEnd) on enter keyup', () => {
let ENTER_EVENT = createKeyboardEvent('keydown', ENTER, inputNativeElement) as any;

spyOn(testChipInput, 'add');

chipInputDirective._keydown(ENTER_EVENT);
expect(testChipInput.add).toHaveBeenCalled();
});
});

describe('[addOnBlur]', () => {
it('allows (chipEnd) when true', () => {
spyOn(testChipInput, 'add');

testChipInput.addOnBlur = true;
fixture.detectChanges();

chipInputDirective._blur();
expect(testChipInput.add).toHaveBeenCalled();
});

it('disallows (chipEnd) when false', () => {
spyOn(testChipInput, 'add');

testChipInput.addOnBlur = false;
fixture.detectChanges();

chipInputDirective._blur();
expect(testChipInput.add).not.toHaveBeenCalled();
});
});

describe('[separatorKeysCodes]', () => {
it('does not emit (chipEnd) when a non-separator key is pressed', () => {
let ENTER_EVENT = createKeyboardEvent('keydown', ENTER, inputNativeElement) as any;
spyOn(testChipInput, 'add');

testChipInput.separatorKeys = [COMMA];
fixture.detectChanges();

chipInputDirective._keydown(ENTER_EVENT);
expect(testChipInput.add).not.toHaveBeenCalled();
});

it('emits (chipEnd) when a custom separator keys is pressed', () => {
let COMMA_EVENT = createKeyboardEvent('keydown', COMMA, inputNativeElement) as any;
spyOn(testChipInput, 'add');

testChipInput.separatorKeys = [COMMA];
fixture.detectChanges();

chipInputDirective._keydown(COMMA_EVENT);
expect(testChipInput.add).toHaveBeenCalled();
});
});
});

@Component({
template: `
<md-chip-list #chipList>
</md-chip-list>
<input mdInput [mdChipInputFor]="chipList"
[mdChipInputAddOnBlur]="addOnBlur"
[mdChipInputSeparatorKeyCodes]="separatorKeys"
(mdChipInputTokenEnd)="add($event)" />
`
})
class TestChipInput {
addOnBlur: boolean = false;
separatorKeys: number[] = [ENTER];

add(_: MdChipInputEvent) {
}
}

0 comments on commit c82aca9

Please sign in to comment.