Skip to content

Commit f81b595

Browse files
committed
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.
1 parent 37e4bad commit f81b595

15 files changed

+1174
-286
lines changed

src/demo-app/chips/chips-demo.html

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ <h4>Advanced</h4>
2323

2424
<md-chip-list selectable="false">
2525
<md-chip color="accent" selected="true">Selected/Colored</md-chip>
26+
2627
<md-chip color="warn" selected="true" *ngIf="visible"
27-
(destroy)="alert('chip destroyed')" (click)="toggleVisible()">
28+
(destroy)="alert('chip destroyed')" (remove)="toggleVisible()">
29+
<md-icon md-chip-remove>cancel</md-icon>
2830
With Events
2931
</md-chip>
3032
</md-chip-list>
@@ -37,16 +39,37 @@ <h4>Advanced</h4>
3739
<md-card-content>
3840
<h4>Input Container</h4>
3941

40-
<md-chip-list>
41-
<md-chip *ngFor="let person of people" [color]="color">
42-
{{person.name}}
43-
</md-chip>
44-
</md-chip-list>
42+
<p>
43+
You can easily put the the <code>&lt;md-chip-list&gt;</code> inside of an
44+
<code>&lt;md-input-container&gt;</code>.
45+
</p>
46+
47+
<md-input-container [floatPlaceholder]="people.length > 0 ? 'always' : 'auto'">
48+
<md-chip-list>
49+
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
50+
[removable]="removable" (remove)="remove(person)">
51+
{{person.name}}
52+
<md-icon mdChipRemove>cancel</md-icon>
53+
</md-chip>
4554

46-
<md-input-container>
47-
<input mdInput #input (keyup.enter)="add(input)" placeholder="New Contributor..."/>
55+
<input mdInput placeholder="New Contributor..."
56+
mdChipInput (chipAdded)="add($event)"
57+
[separatorKeys]="separatorKeys" [addOnBlur]="addOnBlur" />
58+
</md-chip-list>
4859
</md-input-container>
4960

61+
<p>
62+
The example above has overridden the <code>[separatorKeys]</code> input to allow for
63+
<code>ENTER</code>, <code>COMMA</code> and <code>SEMI COLON</code> keys.
64+
</p>
65+
66+
<h4>Options</h4>
67+
<p>
68+
<md-checkbox name="selectable" [(ngModel)]="selectable">Selectable</md-checkbox>
69+
<md-checkbox name="removable" [(ngModel)]="removable">Removable</md-checkbox>
70+
<md-checkbox name="addOnBlur" [(ngModel)]="addOnBlur">Add on Blur</md-checkbox>
71+
</p>
72+
5073
<h4>Stacked Chips</h4>
5174

5275
<p>

src/demo-app/chips/chips-demo.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@
2020
.mat-basic-chip {
2121
margin: auto 10px;
2222
}
23+
24+
md-chip-list input {
25+
width: 150px;
26+
}
2327
}

src/demo-app/chips/chips-demo.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {Component, ElementRef} from '@angular/core';
1+
import {Component} from '@angular/core';
2+
import {MdChipInputEvent, ENTER, COMMA} from "@angular/material";
23

34
export interface Person {
45
name: string;
@@ -18,6 +19,12 @@ export interface DemoColor {
1819
export class ChipsDemo {
1920
visible: boolean = true;
2021
color: string = '';
22+
selectable: boolean = true;
23+
removable: boolean = true;
24+
addOnBlur: boolean = true;
25+
26+
// Enter, comma, semi-colon
27+
separatorKeys = [ENTER, COMMA, 186];
2128

2229
people: Person[] = [
2330
{ name: 'Kara' },
@@ -39,10 +46,26 @@ export class ChipsDemo {
3946
alert(message);
4047
}
4148

42-
add(input: ElementRef): void {
43-
if (input.nativeElement.value && input.nativeElement.value.trim() != '') {
44-
this.people.push({ name: input.nativeElement.value.trim() });
45-
input.nativeElement.value = '';
49+
add(event: MdChipInputEvent): void {
50+
let input = event.input;
51+
let value = event.value;
52+
53+
// Add our person
54+
if (value && value.trim() != '') {
55+
this.people.push({ name: value.trim() });
56+
}
57+
58+
// Reset the input value
59+
if (input) {
60+
input.value = '';
61+
}
62+
}
63+
64+
remove(person: Person): void {
65+
let index = this.people.indexOf(person);
66+
67+
if (index >= 0) {
68+
this.people.splice(index, 1);
4669
}
4770
}
4871

src/lib/chips/_chips-theme.scss

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,90 @@
1616

1717
// The spec only provides guidance for light-themed chips. When inside of a dark theme, fall back
1818
// to standard background and foreground colors.
19-
$unselected-background: if($is-dark-theme, mat-color($background, card), #e0e0e0);
19+
$unselected-background: if($is-dark-theme, #656565, #e0e0e0);
2020
$unselected-foreground: if($is-dark-theme, mat-color($foreground, text), $light-foreground);
2121

2222
$selected-background: if($is-dark-theme, mat-color($background, app-bar), #808080);
2323
$selected-foreground: if($is-dark-theme, mat-color($foreground, text), $light-selected-foreground);
2424

25+
$focus-color: mat-color($foreground, text);
26+
2527
.mat-chip:not(.mat-basic-chip) {
2628
background-color: $unselected-background;
2729
color: $unselected-foreground;
30+
31+
.mat-chip-focus-border {
32+
pointer-events: none;
33+
}
34+
35+
&:focus {
36+
outline: none;
37+
border: 2px solid $focus-color;
38+
}
39+
40+
.mat-chip-remove {
41+
color: $unselected-foreground;
42+
opacity: 0.3;
43+
44+
&:hover {
45+
opacity: 0.54;
46+
}
47+
}
2848
}
2949

3050
.mat-chip.mat-chip-selected:not(.mat-basic-chip) {
3151
background-color: $selected-background;
3252
color: $selected-foreground;
3353

54+
.mat-chip-remove {
55+
color: $selected-foreground;
56+
opacity: 0.4;
57+
58+
&:hover {
59+
opacity: 0.54;
60+
}
61+
}
62+
3463
&.mat-primary {
3564
background-color: mat-color($primary, 500);
3665
color: mat-contrast($primary, 500);
66+
67+
.mat-chip-remove {
68+
color: mat-contrast($primary, 500);
69+
opacity: 0.4;
70+
71+
&:hover {
72+
opacity: 0.54;
73+
}
74+
}
3775
}
3876

3977
&.mat-accent {
4078
background-color: mat-color($accent, 500);
4179
color: mat-contrast($accent, 500);
80+
81+
.mat-chip-remove {
82+
color: mat-contrast($accent, 500);
83+
opacity: 0.4;
84+
85+
&:hover {
86+
opacity: 0.54;
87+
}
88+
}
4289
}
4390

4491
&.mat-warn {
4592
background-color: mat-color($warn, 500);
4693
color: mat-contrast($warn, 500);
94+
95+
.mat-chip-remove {
96+
color: mat-contrast($warn, 500);
97+
opacity: 0.4;
98+
99+
&:hover {
100+
opacity: 0.54;
101+
}
102+
}
47103
}
48104
}
49105
}

src/lib/chips/chip-input.spec.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {async, TestBed, ComponentFixture} from "@angular/core/testing";
2+
import {MdChipsModule} from "./index";
3+
import {Component, DebugElement} from "@angular/core";
4+
import {MdChipInput, MdChipInputEvent} from "./chip-input";
5+
import {By} from "@angular/platform-browser";
6+
import {Dir} from "../core/rtl/dir";
7+
import {FakeKeyboardEvent} from "./chip-list.spec";
8+
import {ENTER, COMMA} from "../core/keyboard/keycodes";
9+
10+
describe('MdChipInput', () => {
11+
let fixture: ComponentFixture<any>;
12+
let testChipInput: TestChipInput;
13+
let inputDebugElement: DebugElement;
14+
let inputNativeElement: HTMLElement;
15+
let chipInputDirective: MdChipInput;
16+
17+
let dir = 'ltr';
18+
19+
beforeEach(async(() => {
20+
TestBed.configureTestingModule({
21+
imports: [MdChipsModule],
22+
declarations: [TestChipInput],
23+
providers: [{
24+
provide: Dir, useFactory: () => {
25+
return {value: dir.toLowerCase()}
26+
}
27+
}]
28+
});
29+
30+
TestBed.compileComponents();
31+
}));
32+
33+
beforeEach(async(() => {
34+
fixture = TestBed.createComponent(TestChipInput);
35+
testChipInput = fixture.debugElement.componentInstance;
36+
fixture.detectChanges();
37+
38+
inputDebugElement = fixture.debugElement.query(By.directive(MdChipInput));
39+
chipInputDirective = inputDebugElement.injector.get(MdChipInput) as MdChipInput;
40+
inputNativeElement = inputDebugElement.nativeElement;
41+
}));
42+
43+
describe('basic behavior', () => {
44+
it('emits the (chipAdded) on enter keyup', () => {
45+
let ENTER_EVENT = new FakeKeyboardEvent(ENTER, inputNativeElement) as any;
46+
47+
spyOn(testChipInput, 'add');
48+
49+
chipInputDirective._keydown(ENTER_EVENT);
50+
expect(testChipInput.add).toHaveBeenCalled();
51+
});
52+
});
53+
54+
describe('[addOnBlur]', () => {
55+
it('allows (chipAdded) when true', () => {
56+
spyOn(testChipInput, 'add');
57+
58+
testChipInput.addOnBlur = true;
59+
fixture.detectChanges();
60+
61+
chipInputDirective._blur();
62+
expect(testChipInput.add).toHaveBeenCalled();
63+
});
64+
65+
it('disallows (chipAdded) when false', () => {
66+
spyOn(testChipInput, 'add');
67+
68+
testChipInput.addOnBlur = false;
69+
fixture.detectChanges();
70+
71+
chipInputDirective._blur();
72+
expect(testChipInput.add).not.toHaveBeenCalled();
73+
});
74+
});
75+
76+
describe('[separatorKeys]', () => {
77+
it('does not emit (chipAdded) when a non-separator key is pressed', () => {
78+
let ENTER_EVENT = new FakeKeyboardEvent(ENTER, inputNativeElement) as any;
79+
spyOn(testChipInput, 'add');
80+
81+
testChipInput.separatorKeys = [COMMA];
82+
fixture.detectChanges();
83+
84+
chipInputDirective._keydown(ENTER_EVENT);
85+
expect(testChipInput.add).not.toHaveBeenCalled();
86+
});
87+
88+
it('emits (chipAdded) when a custom separator keys is pressed', () => {
89+
let COMMA_EVENT = new FakeKeyboardEvent(COMMA, inputNativeElement) as any;
90+
spyOn(testChipInput, 'add');
91+
92+
testChipInput.separatorKeys = [COMMA];
93+
fixture.detectChanges();
94+
95+
chipInputDirective._keydown(COMMA_EVENT);
96+
expect(testChipInput.add).toHaveBeenCalled();
97+
});
98+
});
99+
});
100+
101+
@Component({
102+
template: `
103+
<md-chip-list>
104+
<input mdChipInput [addOnBlur]="addOnBlur" [separatorKeys]="separatorKeys"
105+
(chipAdded)="add($event)" />
106+
</md-chip-list>
107+
`
108+
})
109+
class TestChipInput {
110+
addOnBlur: boolean = false;
111+
separatorKeys: number[] = [ENTER];
112+
113+
add(event: MdChipInputEvent) {
114+
}
115+
}

0 commit comments

Comments
 (0)