Skip to content

Commit c82aca9

Browse files
tinayuangaojelbourn
authored andcommitted
feat(chips): Add removal functionality/styling. (#4912)
* 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 243b97d commit c82aca9

14 files changed

+1087
-265
lines changed

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

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ <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)="displayMessage('chip destroyed')" (remove)="toggleVisible()">
2829
With Events
30+
<md-icon mdChipRemove>cancel</md-icon>
2931
</md-chip>
3032
</md-chip-list>
33+
<div>{{message}}</div>
3134
</md-card-content>
3235
</md-card>
3336

@@ -37,16 +40,61 @@ <h4>Advanced</h4>
3740
<md-card-content>
3841
<h4>Input Container</h4>
3942

40-
<md-chip-list>
41-
<md-chip *ngFor="let person of people" [color]="color">
43+
<p>
44+
You can easily put the the <code>&lt;md-chip-list&gt;</code> inside of an
45+
<code>&lt;md-input-container&gt;</code>.
46+
</p>
47+
48+
49+
<md-input-container>
50+
<md-chip-list mdPrefix #chipList1>
51+
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
52+
[removable]="removable" (remove)="remove(person)">
53+
{{person.name}}
54+
<md-icon mdChipRemove *ngIf="removable">cancel</md-icon>
55+
</md-chip>
56+
</md-chip-list>
57+
<input mdInput placeholder="New Contributor..."
58+
[mdChipInputFor]="chipList1"
59+
[mdChipInputSeparatorKeyCodes]="separatorKeysCodes"
60+
[mdChipInputAddOnBlur]="addOnBlur"
61+
(mdChipInputTokenEnd)="add($event)" />
62+
</md-input-container>
63+
64+
65+
66+
<p>
67+
You can also put <code>&lt;md-input-container&gt;</code> outside of an <code>md-chip-list</code>.
68+
With <code>mdChipInput</code> the input work with chip-list
69+
</p>
70+
71+
<md-chip-list #chipList2>
72+
<md-chip *ngFor="let person of people" [color]="color" [selectable]="selectable"
73+
[removable]="removable" (remove)="remove(person)">
4274
{{person.name}}
75+
<md-icon mdChipRemove *ngIf="removable">cancel</md-icon>
4376
</md-chip>
4477
</md-chip-list>
45-
4678
<md-input-container>
47-
<input mdInput #input (keyup.enter)="add(input)" placeholder="New Contributor..."/>
79+
<input mdInput placeholder="New Contributor..."
80+
[mdChipInputFor]="chipList2"
81+
[mdChipInputSeparatorKeyCodes]="separatorKeysCodes"
82+
[mdChipInputAddOnBlur]="addOnBlur"
83+
(mdChipInputTokenEnd)="add($event)" />
4884
</md-input-container>
4985

86+
<p>
87+
The example above has overridden the <code>[separatorKeys]</code> input to allow for
88+
<code>ENTER</code>, <code>COMMA</code> and <code>SEMI COLON</code> keys.
89+
</p>
90+
91+
<h4>Options</h4>
92+
<p>
93+
<md-checkbox name="selectable" [(ngModel)]="selectable">Selectable</md-checkbox>
94+
<md-checkbox name="removable" [(ngModel)]="removable">Removable</md-checkbox>
95+
<md-checkbox name="addOnBlur" [(ngModel)]="addOnBlur">Add on Blur</md-checkbox>
96+
</p>
97+
5098
<h4>Stacked Chips</h4>
5199

52100
<p>

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,16 @@
2020
.mat-basic-chip {
2121
margin: auto 10px;
2222
}
23-
}
23+
24+
md-chip-list input {
25+
width: 150px;
26+
}
27+
28+
.mat-chip-remove.mat-icon {
29+
font-size: 16px;
30+
width: 1em;
31+
height: 1em;
32+
vertical-align: middle;
33+
cursor: pointer;
34+
}
35+
}

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

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import {Component} from '@angular/core';
2+
import {MdChipInputEvent, ENTER} from '@angular/material';
3+
4+
const COMMA = 188;
25

36
export interface Person {
47
name: string;
@@ -18,6 +21,13 @@ export interface DemoColor {
1821
export class ChipsDemo {
1922
visible: boolean = true;
2023
color: string = '';
24+
selectable: boolean = true;
25+
removable: boolean = true;
26+
addOnBlur: boolean = true;
27+
message: string = '';
28+
29+
// Enter, comma, semi-colon
30+
separatorKeysCodes = [ENTER, COMMA, 186];
2131

2232
people: Person[] = [
2333
{ name: 'Kara' },
@@ -35,17 +45,33 @@ export class ChipsDemo {
3545
{ name: 'Warn', color: 'warn' }
3646
];
3747

38-
alert(message: string): void {
39-
alert(message);
48+
displayMessage(message: string): void {
49+
this.message = message;
4050
}
4151

42-
add(input: HTMLInputElement): void {
43-
if (input.value && input.value.trim() != '') {
44-
this.people.push({ name: input.value.trim() });
52+
add(event: MdChipInputEvent): void {
53+
let input = event.input;
54+
let value = event.value;
55+
56+
// Add our person
57+
if ((value || '').trim()) {
58+
this.people.push({ name: value.trim() });
59+
}
60+
61+
// Reset the input value
62+
if (input) {
4563
input.value = '';
4664
}
4765
}
4866

67+
remove(person: Person): void {
68+
let index = this.people.indexOf(person);
69+
70+
if (index >= 0) {
71+
this.people.splice(index, 1);
72+
}
73+
}
74+
4975
toggleVisible(): void {
5076
this.visible = false;
5177
}

src/lib/chips/_chips-theme.scss

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@
66
$mat-chip-font-size: 13px;
77
$mat-chip-line-height: 16px;
88

9+
@mixin mat-chips-theme-color($color) {
10+
@include mat-chips-color(mat-contrast($color, 500), mat-color($color, 500));
11+
}
12+
13+
@mixin mat-chips-color($foreground, $background) {
14+
background-color: $background;
15+
color: $foreground;
16+
17+
.mat-chip-remove {
18+
color: $foreground;
19+
opacity: 0.4;
20+
}
21+
22+
.mat-chip-remove:hover {
23+
opacity: 0.54;
24+
}
25+
}
926

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

31-
.mat-chip:not(.mat-basic-chip) {
32-
background-color: $unselected-background;
33-
color: $unselected-foreground;
48+
.mat-chip {
49+
@include mat-chips-color($unselected-foreground, $unselected-background);
3450
}
3551

36-
.mat-chip.mat-chip-selected:not(.mat-basic-chip) {
37-
background-color: $selected-background;
38-
color: $selected-foreground;
52+
.mat-chip.mat-chip-selected {
53+
@include mat-chips-color($selected-foreground, $selected-background);
3954

4055
&.mat-primary {
41-
background-color: mat-color($primary);
42-
color: mat-color($primary, default-contrast);
56+
@include mat-chips-theme-color($primary);
4357
}
4458

45-
&.mat-accent {
46-
background-color: mat-color($accent);
47-
color: mat-color($accent, default-contrast);
59+
&.mat-warn {
60+
@include mat-chips-theme-color($warn);
4861
}
4962

50-
&.mat-warn {
51-
background-color: mat-color($warn);
52-
color: mat-color($warn, default-contrast);
63+
&.mat-accent {
64+
@include mat-chips-theme-color($accent);
5365
}
5466
}
5567
}
5668

5769
@mixin mat-chips-typography($config) {
58-
.mat-chip:not(.mat-basic-chip) {
70+
.mat-chip {
5971
font-size: $mat-chip-font-size;
6072
line-height: $mat-chip-line-height;
6173
}

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

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

0 commit comments

Comments
 (0)