mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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.
This commit is contained in:
committed by
Jeremy Elbourn
parent
243b97dfb0
commit
c82aca9f8d
@@ -23,11 +23,14 @@
|
||||
|
||||
<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>
|
||||
|
||||
@@ -37,16 +40,61 @@
|
||||
<md-card-content>
|
||||
<h4>Input Container</h4>
|
||||
|
||||
<md-chip-list>
|
||||
<md-chip *ngFor="let person of people" [color]="color">
|
||||
{{person.name}}
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
<p>
|
||||
You can easily put the the <code><md-chip-list></code> inside of an
|
||||
<code><md-input-container></code>.
|
||||
</p>
|
||||
|
||||
|
||||
<md-input-container>
|
||||
<input mdInput #input (keyup.enter)="add(input)" placeholder="New Contributor..."/>
|
||||
<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><md-input-container></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 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MdChipInputEvent, ENTER} from '@angular/material';
|
||||
|
||||
const COMMA = 188;
|
||||
|
||||
export interface Person {
|
||||
name: string;
|
||||
@@ -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' },
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
&.mat-accent {
|
||||
background-color: mat-color($accent);
|
||||
color: mat-color($accent, default-contrast);
|
||||
@include mat-chips-theme-color($primary);
|
||||
}
|
||||
|
||||
&.mat-warn {
|
||||
background-color: mat-color($warn);
|
||||
color: mat-color($warn, default-contrast);
|
||||
@include mat-chips-theme-color($warn);
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
@@ -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,0 +1,103 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Directive, Output, EventEmitter, ElementRef, Input} from '@angular/core';
|
||||
import {coerceBooleanProperty} from '@angular/cdk';
|
||||
import {ENTER} from '../core/keyboard/keycodes';
|
||||
import {MdChipList} from './chip-list';
|
||||
|
||||
export interface MdChipInputEvent {
|
||||
input: HTMLInputElement;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: 'input[mdChipInputFor], input[matChipInputFor]',
|
||||
host: {
|
||||
'class': 'mat-chip-input',
|
||||
'(keydown)': '_keydown($event)',
|
||||
'(blur)': '_blur()'
|
||||
}
|
||||
})
|
||||
export class MdChipInput {
|
||||
|
||||
_chipList: MdChipList;
|
||||
|
||||
/** Register input for chip list */
|
||||
@Input('mdChipInputFor')
|
||||
set chipList(value: MdChipList) {
|
||||
if (value) {
|
||||
this._chipList = value;
|
||||
this._chipList.registerInput(this._inputElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the chipEnd event will be emitted when the input is blurred.
|
||||
*/
|
||||
@Input('mdChipInputAddOnBlur')
|
||||
get addOnBlur() { return this._addOnBlur; }
|
||||
set addOnBlur(value) { this._addOnBlur = coerceBooleanProperty(value); }
|
||||
_addOnBlur: boolean = false;
|
||||
|
||||
/**
|
||||
* The list of key codes that will trigger a chipEnd event.
|
||||
*
|
||||
* Defaults to `[ENTER]`.
|
||||
*/
|
||||
// TODO(tinayuangao): Support Set here
|
||||
@Input('mdChipInputSeparatorKeyCodes') separatorKeyCodes: number[] = [ENTER];
|
||||
|
||||
/** Emitted when a chip is to be added. */
|
||||
@Output('mdChipInputTokenEnd')
|
||||
chipEnd = new EventEmitter<MdChipInputEvent>();
|
||||
|
||||
@Input('matChipInputFor')
|
||||
set matChipList(value: MdChipList) { this.chipList = value; }
|
||||
|
||||
@Input('matChipInputAddOnBlur')
|
||||
get matAddOnBlur() { return this._addOnBlur; }
|
||||
set matAddOnBlur(value) { this.addOnBlur = value; }
|
||||
|
||||
@Input('matChipInputSeparatorKeyCodes')
|
||||
get matSeparatorKeyCodes() { return this.separatorKeyCodes; }
|
||||
set matSeparatorKeyCodes(v: number[]) { this.separatorKeyCodes = v; }
|
||||
|
||||
/** The native input element to which this directive is attached. */
|
||||
protected _inputElement: HTMLInputElement;
|
||||
|
||||
constructor(protected _elementRef: ElementRef) {
|
||||
this._inputElement = this._elementRef.nativeElement as HTMLInputElement;
|
||||
}
|
||||
|
||||
/** Utility method to make host definition/tests more clear. */
|
||||
_keydown(event?: KeyboardEvent) {
|
||||
this._emitChipEnd(event);
|
||||
}
|
||||
|
||||
/** Checks to see if the blur should emit the (chipEnd) event. */
|
||||
_blur() {
|
||||
if (this.addOnBlur) {
|
||||
this._emitChipEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks to see if the (chipEnd) event needs to be emitted. */
|
||||
_emitChipEnd(event?: KeyboardEvent) {
|
||||
if (!this._inputElement.value && !!event) {
|
||||
this._chipList._keydown(event);
|
||||
}
|
||||
if (!event || this.separatorKeyCodes.indexOf(event.keyCode) > -1) {
|
||||
this.chipEnd.emit({ input: this._inputElement, value: this._inputElement.value });
|
||||
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+295
-193
@@ -1,32 +1,292 @@
|
||||
import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {Component, DebugElement, QueryList} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MdChip, MdChipList, MdChipsModule} from './index';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MdChipList, MdChipsModule} from './index';
|
||||
import {FocusKeyManager} from '../core/a11y/focus-key-manager';
|
||||
import {SPACE, LEFT_ARROW, RIGHT_ARROW, TAB} from '../core/keyboard/keycodes';
|
||||
import {createKeyboardEvent} from '@angular/cdk/testing';
|
||||
|
||||
import {MdInputModule} from '../input/index';
|
||||
import {LEFT_ARROW, RIGHT_ARROW, BACKSPACE, DELETE, TAB} from '../core/keyboard/keycodes';
|
||||
import {Directionality} from '../core';
|
||||
|
||||
describe('MdChipList', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
let chipListDebugElement: DebugElement;
|
||||
let chipListNativeElement: HTMLElement;
|
||||
let chipListInstance: MdChipList;
|
||||
let testComponent: StaticChipList;
|
||||
let chips: QueryList<MdChip>;
|
||||
let testComponent: StandardChipList;
|
||||
let chips: QueryList<any>;
|
||||
let manager: FocusKeyManager;
|
||||
|
||||
let dir = 'ltr';
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MdChipsModule],
|
||||
declarations: [StaticChipList]
|
||||
imports: [MdChipsModule, MdInputModule, NoopAnimationsModule],
|
||||
declarations: [
|
||||
StandardChipList, InputContainerChipList
|
||||
],
|
||||
providers: [{
|
||||
provide: Directionality, useFactory: () => {
|
||||
return {value: dir.toLowerCase()};
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StaticChipList);
|
||||
describe('StandardChipList', () => {
|
||||
describe('basic behaviors', () => {
|
||||
beforeEach(async(() => {
|
||||
setupStandardList();
|
||||
}));
|
||||
|
||||
it('should add the `mat-chip-list` class', () => {
|
||||
expect(chipListNativeElement.classList).toContain('mat-chip-list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('focus behaviors', () => {
|
||||
beforeEach(async(() => {
|
||||
setupStandardList();
|
||||
manager = chipListInstance._keyManager;
|
||||
}));
|
||||
|
||||
it('should focus the first chip on focus', () => {
|
||||
chipListInstance.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(manager.activeItemIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should watch for chip focus', () => {
|
||||
let array = chips.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
lastItem.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(manager.activeItemIndex).toBe(lastIndex);
|
||||
});
|
||||
|
||||
it('should watch for chip focus', () => {
|
||||
let array = chips.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
|
||||
lastItem.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(manager.activeItemIndex).toBe(lastIndex);
|
||||
});
|
||||
|
||||
describe('on chip destroy', () => {
|
||||
it('should focus the next item', () => {
|
||||
let array = chips.toArray();
|
||||
let midItem = array[2];
|
||||
|
||||
// Focus the middle item
|
||||
midItem.focus();
|
||||
|
||||
// Destroy the middle item
|
||||
testComponent.remove = 2;
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the 4th item (now at index 2)
|
||||
expect(manager.activeItemIndex).toEqual(2);
|
||||
});
|
||||
|
||||
|
||||
it('should focus the previous item', () => {
|
||||
let array = chips.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
|
||||
// Focus the last item
|
||||
lastItem.focus();
|
||||
|
||||
// Destroy the last item
|
||||
testComponent.remove = lastIndex;
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex - 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyboard behavior', () => {
|
||||
describe('LTR (default)', () => {
|
||||
beforeEach(async(() => {
|
||||
dir = 'ltr';
|
||||
setupStandardList();
|
||||
manager = chipListInstance._keyManager;
|
||||
}));
|
||||
|
||||
it('should focus previous item when press LEFT ARROW', () => {
|
||||
let nativeChips = chipListNativeElement.querySelectorAll('md-chip');
|
||||
let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
|
||||
|
||||
let LEFT_EVENT = createKeyboardEvent('keydown', LEFT_ARROW, lastNativeChip);
|
||||
let array = chips.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
|
||||
// Focus the last item in the array
|
||||
lastItem.focus();
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex);
|
||||
|
||||
// Press the LEFT arrow
|
||||
chipListInstance._keydown(LEFT_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex - 1);
|
||||
});
|
||||
|
||||
it('should focus next item when press RIGHT ARROW', () => {
|
||||
let nativeChips = chipListNativeElement.querySelectorAll('md-chip');
|
||||
let firstNativeChip = nativeChips[0] as HTMLElement;
|
||||
|
||||
let RIGHT_EVENT: KeyboardEvent =
|
||||
createKeyboardEvent('keydown', RIGHT_ARROW, firstNativeChip);
|
||||
let array = chips.toArray();
|
||||
let firstItem = array[0];
|
||||
|
||||
// Focus the last item in the array
|
||||
firstItem.focus();
|
||||
expect(manager.activeItemIndex).toEqual(0);
|
||||
|
||||
// Press the RIGHT arrow
|
||||
chipListInstance._keydown(RIGHT_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.activeItemIndex).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('RTL', () => {
|
||||
beforeEach(async(() => {
|
||||
dir = 'rtl';
|
||||
setupStandardList();
|
||||
manager = chipListInstance._keyManager;
|
||||
}));
|
||||
|
||||
it('should focus previous item when press RIGHT ARROW', () => {
|
||||
let nativeChips = chipListNativeElement.querySelectorAll('md-chip');
|
||||
let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
|
||||
|
||||
let RIGHT_EVENT: KeyboardEvent =
|
||||
createKeyboardEvent('keydown', RIGHT_ARROW, lastNativeChip);
|
||||
let array = chips.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
|
||||
// Focus the last item in the array
|
||||
lastItem.focus();
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex);
|
||||
|
||||
// Press the RIGHT arrow
|
||||
chipListInstance._keydown(RIGHT_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex - 1);
|
||||
});
|
||||
|
||||
it('should focus next item when press LEFT ARROW', () => {
|
||||
let nativeChips = chipListNativeElement.querySelectorAll('md-chip');
|
||||
let firstNativeChip = nativeChips[0] as HTMLElement;
|
||||
|
||||
let LEFT_EVENT: KeyboardEvent =
|
||||
createKeyboardEvent('keydown', LEFT_ARROW, firstNativeChip);
|
||||
let array = chips.toArray();
|
||||
let firstItem = array[0];
|
||||
|
||||
// Focus the last item in the array
|
||||
firstItem.focus();
|
||||
expect(manager.activeItemIndex).toEqual(0);
|
||||
|
||||
// Press the LEFT arrow
|
||||
chipListInstance._keydown(LEFT_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.activeItemIndex).toEqual(1);
|
||||
});
|
||||
|
||||
it('should allow focus to escape when tabbing away', fakeAsync(() => {
|
||||
chipListInstance._keyManager.onKeydown(createKeyboardEvent('keydown', TAB));
|
||||
|
||||
expect(chipListInstance._tabIndex)
|
||||
.toBe(-1, 'Expected tabIndex to be set to -1 temporarily.');
|
||||
|
||||
tick();
|
||||
|
||||
expect(chipListInstance._tabIndex).toBe(0, 'Expected tabIndex to be reset back to 0');
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('InputContainerChipList', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
setupInputList();
|
||||
});
|
||||
|
||||
describe('keyboard behavior', () => {
|
||||
beforeEach(() => {
|
||||
manager = chipListInstance._keyManager;
|
||||
});
|
||||
|
||||
describe('when the input has focus', () => {
|
||||
|
||||
it('should focus the last chip when press DELETE', () => {
|
||||
let nativeInput = fixture.nativeElement.querySelector('input');
|
||||
let DELETE_EVENT: KeyboardEvent =
|
||||
createKeyboardEvent('keydown', DELETE, nativeInput);
|
||||
|
||||
// Focus the input
|
||||
nativeInput.focus();
|
||||
expect(manager.activeItemIndex).toBe(-1);
|
||||
|
||||
// Press the DELETE key
|
||||
chipListInstance._keydown(DELETE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the last chip
|
||||
expect(manager.activeItemIndex).toEqual(chips.length - 1);
|
||||
});
|
||||
|
||||
it('should focus the last chip when press BACKSPACE', () => {
|
||||
let nativeInput = fixture.nativeElement.querySelector('input');
|
||||
let BACKSPACE_EVENT: KeyboardEvent =
|
||||
createKeyboardEvent('keydown', BACKSPACE, nativeInput);
|
||||
|
||||
// Focus the input
|
||||
nativeInput.focus();
|
||||
expect(manager.activeItemIndex).toBe(-1);
|
||||
|
||||
// Press the BACKSPACE key
|
||||
chipListInstance._keydown(BACKSPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the last chip
|
||||
expect(manager.activeItemIndex).toEqual(chips.length - 1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function setupStandardList() {
|
||||
fixture = TestBed.createComponent(StandardChipList);
|
||||
fixture.detectChanges();
|
||||
|
||||
chipListDebugElement = fixture.debugElement.query(By.directive(MdChipList));
|
||||
@@ -34,196 +294,24 @@ describe('MdChipList', () => {
|
||||
chipListInstance = chipListDebugElement.componentInstance;
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
chips = chipListInstance.chips;
|
||||
});
|
||||
}
|
||||
|
||||
describe('basic behaviors', () => {
|
||||
it('adds the `md-chip-list` class', () => {
|
||||
expect(chipListNativeElement.classList).toContain('mat-chip-list');
|
||||
});
|
||||
});
|
||||
function setupInputList() {
|
||||
fixture = TestBed.createComponent(InputContainerChipList);
|
||||
fixture.detectChanges();
|
||||
|
||||
describe('focus behaviors', () => {
|
||||
beforeEach(() => {
|
||||
manager = chipListInstance._keyManager;
|
||||
});
|
||||
|
||||
it('should focus the first chip on focus', () => {
|
||||
chipListInstance.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(manager.activeItemIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should watch for chip focus', () => {
|
||||
let array = chips.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
|
||||
lastItem.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(manager.activeItemIndex).toBe(lastIndex);
|
||||
});
|
||||
|
||||
describe('on chip destroy', () => {
|
||||
it('focuses the next item', () => {
|
||||
let array = chips.toArray();
|
||||
let midItem = array[2];
|
||||
|
||||
// Focus the middle item
|
||||
midItem.focus();
|
||||
|
||||
// Destroy the middle item
|
||||
testComponent.remove = 2;
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the 4th item (now at index 2)
|
||||
expect(manager.activeItemIndex).toEqual(2);
|
||||
});
|
||||
|
||||
it('should focus the previous item', () => {
|
||||
let array = chips.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
|
||||
// Focus the last item by fake updating the _hasFocus state for unit tests.
|
||||
lastItem._hasFocus = true;
|
||||
|
||||
// Destroy the last item
|
||||
testComponent.remove = lastIndex;
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex - 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyboard behavior', () => {
|
||||
beforeEach(() => {
|
||||
manager = chipListInstance._keyManager;
|
||||
});
|
||||
|
||||
it('left arrow focuses previous item', () => {
|
||||
let nativeChips = chipListNativeElement.querySelectorAll('md-chip');
|
||||
let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
|
||||
|
||||
let LEFT_EVENT = createKeyboardEvent('keydown', LEFT_ARROW, lastNativeChip);
|
||||
let array = chips.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
|
||||
// Focus the last item in the array
|
||||
lastItem.focus();
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex);
|
||||
|
||||
// Press the LEFT arrow
|
||||
chipListInstance._keydown(LEFT_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.activeItemIndex).toEqual(lastIndex - 1);
|
||||
});
|
||||
|
||||
it('right arrow focuses next item', () => {
|
||||
let nativeChips = chipListNativeElement.querySelectorAll('md-chip');
|
||||
let firstNativeChip = nativeChips[0] as HTMLElement;
|
||||
|
||||
let RIGHT_EVENT = createKeyboardEvent('keydown', RIGHT_ARROW, firstNativeChip);
|
||||
let array = chips.toArray();
|
||||
let firstItem = array[0];
|
||||
|
||||
// Focus the last item in the array
|
||||
firstItem.focus();
|
||||
expect(manager.activeItemIndex).toEqual(0);
|
||||
|
||||
// Press the RIGHT arrow
|
||||
chipListInstance._keydown(RIGHT_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.activeItemIndex).toEqual(1);
|
||||
});
|
||||
|
||||
describe('when selectable is true', () => {
|
||||
beforeEach(() => {
|
||||
testComponent.selectable = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('SPACE selects/deselects the currently focused chip', () => {
|
||||
let nativeChips = chipListNativeElement.querySelectorAll('md-chip');
|
||||
let firstNativeChip = nativeChips[0] as HTMLElement;
|
||||
|
||||
let SPACE_EVENT = createKeyboardEvent('keydown', SPACE, firstNativeChip);
|
||||
let firstChip: MdChip = chips.toArray()[0];
|
||||
|
||||
spyOn(testComponent, 'chipSelect');
|
||||
spyOn(testComponent, 'chipDeselect');
|
||||
|
||||
// Make sure we have the first chip focused
|
||||
chipListInstance.focus();
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
it('allow focus to escape when tabbing away', fakeAsync(() => {
|
||||
chipListInstance._keyManager.onKeydown(createKeyboardEvent('keydown', TAB));
|
||||
|
||||
expect(chipListInstance._tabIndex)
|
||||
.toBe(-1, 'Expected tabIndex to be set to -1 temporarily.');
|
||||
|
||||
tick();
|
||||
|
||||
expect(chipListInstance._tabIndex).toBe(0, 'Expected tabIndex to be reset back to 0');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when selectable is false', () => {
|
||||
beforeEach(() => {
|
||||
testComponent.selectable = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('SPACE ignores selection', () => {
|
||||
let SPACE_EVENT = createKeyboardEvent('keydown', SPACE);
|
||||
let firstChip: MdChip = chips.toArray()[0];
|
||||
|
||||
spyOn(testComponent, 'chipSelect');
|
||||
|
||||
// Make sure we have the first chip focused
|
||||
chipListInstance.focus();
|
||||
|
||||
// Use the spacebar to attempt to select the chip
|
||||
chipListInstance._keydown(SPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(firstChip.selected).toBeFalsy();
|
||||
expect(testComponent.chipSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
chipListDebugElement = fixture.debugElement.query(By.directive(MdChipList));
|
||||
chipListNativeElement = chipListDebugElement.nativeElement;
|
||||
chipListInstance = chipListDebugElement.componentInstance;
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
chips = chipListInstance.chips;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-chip-list [selectable]="selectable">
|
||||
<md-chip-list>
|
||||
<div *ngFor="let i of [0,1,2,3,4]">
|
||||
<div *ngIf="remove != i">
|
||||
<md-chip (select)="chipSelect(i)" (deselect)="chipDeselect(i)">
|
||||
@@ -233,11 +321,25 @@ describe('MdChipList', () => {
|
||||
</div>
|
||||
</md-chip-list>`
|
||||
})
|
||||
class StaticChipList {
|
||||
class StandardChipList {
|
||||
name: string = 'Test';
|
||||
selectable: boolean = true;
|
||||
remove: number;
|
||||
|
||||
chipSelect: (index?: number) => void = () => {};
|
||||
chipDeselect: (index?: number) => void = () => {};
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-input-container>
|
||||
<md-chip-list #chipList>
|
||||
<md-chip>Chip 1</md-chip>
|
||||
<md-chip>Chip 1</md-chip>
|
||||
<md-chip>Chip 1</md-chip>
|
||||
</md-chip-list>
|
||||
<input mdInput name="test" [mdChipInputFor]="chipList"/>
|
||||
</md-input-container>
|
||||
`
|
||||
})
|
||||
class InputContainerChipList {
|
||||
}
|
||||
|
||||
+138
-64
@@ -15,13 +15,16 @@ import {
|
||||
QueryList,
|
||||
ViewEncapsulation,
|
||||
OnDestroy,
|
||||
Optional,
|
||||
ElementRef,
|
||||
Renderer2,
|
||||
} from '@angular/core';
|
||||
|
||||
import {MdChip} from './chip';
|
||||
import {FocusKeyManager} from '../core/a11y/focus-key-manager';
|
||||
import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
|
||||
import {BACKSPACE, DELETE, LEFT_ARROW, RIGHT_ARROW, UP_ARROW} from '../core/keyboard/keycodes';
|
||||
import {coerceBooleanProperty, Directionality} from '@angular/cdk';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {coerceBooleanProperty} from '@angular/cdk';
|
||||
|
||||
/**
|
||||
* A material design chips component (named ChipList for it's similarity to the List component).
|
||||
@@ -38,12 +41,10 @@ import {coerceBooleanProperty} from '@angular/cdk';
|
||||
selector: 'md-chip-list, mat-chip-list',
|
||||
template: `<div class="mat-chip-list-wrapper"><ng-content></ng-content></div>`,
|
||||
host: {
|
||||
// Properties
|
||||
'[attr.tabindex]': '_tabIndex',
|
||||
'role': 'listbox',
|
||||
'class': 'mat-chip-list',
|
||||
|
||||
// Events
|
||||
'(focus)': 'focus()',
|
||||
'(keydown)': '_keydown($event)'
|
||||
},
|
||||
@@ -56,8 +57,11 @@ import {coerceBooleanProperty} from '@angular/cdk';
|
||||
})
|
||||
export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
|
||||
/** When a chip is destroyed, we track the index so we can focus the appropriate next chip. */
|
||||
protected _lastDestroyedIndex: number|null = null;
|
||||
|
||||
/** Track which chips we're listening to for focus/destruction. */
|
||||
private _subscribed: WeakMap<MdChip, boolean> = new WeakMap();
|
||||
protected _chipSet: WeakMap<MdChip, boolean> = new WeakMap();
|
||||
|
||||
/** Subscription to tabbing out from the chip list. */
|
||||
private _tabOutSubscription: Subscription;
|
||||
@@ -65,14 +69,20 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
/** Whether or not the chip is selectable. */
|
||||
protected _selectable: boolean = true;
|
||||
|
||||
protected _inputElement: HTMLInputElement;
|
||||
|
||||
/** Tab index for the chip list. */
|
||||
_tabIndex = 0;
|
||||
|
||||
/** The FocusKeyManager which handles focus. */
|
||||
_keyManager: FocusKeyManager;
|
||||
|
||||
/** The chip components contained within this chip list. */
|
||||
chips: QueryList<MdChip>;
|
||||
|
||||
/** Tab index for the chip list. */
|
||||
_tabIndex = 0;
|
||||
constructor(protected _renderer: Renderer2, protected _elementRef: ElementRef,
|
||||
@Optional() private _dir: Directionality) {
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this._keyManager = new FocusKeyManager(this.chips).withWrap();
|
||||
@@ -87,9 +97,23 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
// Go ahead and subscribe all of the initial chips
|
||||
this._subscribeChips(this.chips);
|
||||
|
||||
// Make sure we set our tab index at the start
|
||||
this._updateTabIndex();
|
||||
|
||||
// When the list changes, re-subscribe
|
||||
this.chips.changes.subscribe((chips: QueryList<MdChip>) => {
|
||||
this._subscribeChips(chips);
|
||||
|
||||
// If we have 0 chips, attempt to focus an input (if available)
|
||||
if (chips.length === 0) {
|
||||
this._focusInput();
|
||||
}
|
||||
|
||||
// Check to see if we need to update our tab index
|
||||
this._updateTabIndex();
|
||||
|
||||
// Check to see if we have a destroyed chip and need to refocus
|
||||
this._updateFocusForDestroyedChips();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,64 +128,69 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
* it's selected state is always ignored.
|
||||
*/
|
||||
@Input()
|
||||
get selectable(): boolean { return this._selectable; }
|
||||
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.
|
||||
*/
|
||||
focus() {
|
||||
// TODO: ARIA says this should focus the first `selected` chip.
|
||||
this._keyManager.setFirstItemActive();
|
||||
/** Associates an HTML input element with this chip list. */
|
||||
registerInput(inputElement: HTMLInputElement) {
|
||||
this._inputElement = inputElement;
|
||||
}
|
||||
|
||||
/** Passes relevant key presses to our key manager. */
|
||||
_keydown(event: KeyboardEvent) {
|
||||
let target = event.target as HTMLElement;
|
||||
|
||||
// If they are on a chip, check for space/left/right, otherwise pass to our key manager
|
||||
if (target && target.classList.contains('mat-chip')) {
|
||||
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;
|
||||
case LEFT_ARROW:
|
||||
this._keyManager.setPreviousItemActive();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case RIGHT_ARROW:
|
||||
this._keyManager.setNextItemActive();
|
||||
event.preventDefault();
|
||||
break;
|
||||
default:
|
||||
this._keyManager.onKeydown(event);
|
||||
}
|
||||
/**
|
||||
* Focuses the the first non-disabled chip in this chip list, or the associated input when there
|
||||
* are no eligible chips.
|
||||
*/
|
||||
focus() {
|
||||
// TODO: ARIA says this should focus the first `selected` chip if any are selected.
|
||||
if (this.chips.length > 0) {
|
||||
this._keyManager.setFirstItemActive();
|
||||
} else {
|
||||
this._focusInput();
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggles the selected state of the currently focused chip. */
|
||||
protected _toggleSelectOnFocusedChip(): void {
|
||||
// Allow disabling of chip selection
|
||||
if (!this.selectable) {
|
||||
/** Attempt to focus an input if we have one. */
|
||||
_focusInput() {
|
||||
if (this._inputElement) {
|
||||
this._inputElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass events to the keyboard manager. Available here for tests.
|
||||
*/
|
||||
_keydown(event: KeyboardEvent) {
|
||||
let code = event.keyCode;
|
||||
let target = event.target as HTMLElement;
|
||||
let isInputEmpty = this._isInputEmpty(target);
|
||||
let isRtl = this._dir && this._dir.value == 'rtl';
|
||||
|
||||
let isPrevKey = (code === (isRtl ? RIGHT_ARROW : LEFT_ARROW));
|
||||
let isNextKey = (code === (isRtl ? LEFT_ARROW : RIGHT_ARROW));
|
||||
let isBackKey = (code === BACKSPACE || code == DELETE || code == UP_ARROW || isPrevKey);
|
||||
// If they are on an empty input and hit backspace/delete/left arrow, focus the last chip
|
||||
if (isInputEmpty && isBackKey) {
|
||||
this._keyManager.setLastItemActive();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
let focusedIndex = this._keyManager.activeItemIndex;
|
||||
|
||||
if (typeof focusedIndex === 'number' && this._isValidIndex(focusedIndex)) {
|
||||
let focusedChip: MdChip = this.chips.toArray()[focusedIndex];
|
||||
|
||||
if (focusedChip) {
|
||||
focusedChip.toggleSelected();
|
||||
// If they are on a chip, check for space/left/right, otherwise pass to our key manager (like
|
||||
// up/down keys)
|
||||
if (target && target.classList.contains('mat-chip')) {
|
||||
if (isPrevKey) {
|
||||
this._keyManager.setPreviousItemActive();
|
||||
event.preventDefault();
|
||||
} else if (isNextKey) {
|
||||
this._keyManager.setNextItemActive();
|
||||
event.preventDefault();
|
||||
} else {
|
||||
this._keyManager.onKeydown(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,6 +205,14 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
chips.forEach(chip => this._addChip(chip));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the tab index as you should not be allowed to focus an empty list.
|
||||
*/
|
||||
protected _updateTabIndex(): void {
|
||||
// If we have 0 chips, we should not allow keyboard focus
|
||||
this._tabIndex = (this.chips.length === 0 ? -1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a specific chip to our subscribed list. If the chip has
|
||||
* already been subscribed, this ensures it is only subscribed
|
||||
@@ -186,7 +223,7 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
*/
|
||||
protected _addChip(chip: MdChip) {
|
||||
// If we've already been subscribed to a parent, do nothing
|
||||
if (this._subscribed.has(chip)) {
|
||||
if (this._chipSet.has(chip)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,24 +236,52 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
// On destroy, remove the item from our list, and check focus
|
||||
// On destroy, remove the item from our list, and setup our destroyed focus check
|
||||
chip.destroy.subscribe(() => {
|
||||
let chipIndex: number = this.chips.toArray().indexOf(chip);
|
||||
|
||||
if (this._isValidIndex(chipIndex) && chip._hasFocus) {
|
||||
// Check whether the chip is the last item
|
||||
if (chipIndex < this.chips.length - 1) {
|
||||
this._keyManager.setActiveItem(chipIndex);
|
||||
} else if (chipIndex - 1 >= 0) {
|
||||
this._keyManager.setActiveItem(chipIndex - 1);
|
||||
if (this._isValidIndex(chipIndex)) {
|
||||
if (chip._hasFocus) {
|
||||
// Check whether the chip is the last item
|
||||
if (chipIndex < this.chips.length - 1) {
|
||||
this._keyManager.setActiveItem(chipIndex);
|
||||
} else if (chipIndex - 1 >= 0) {
|
||||
this._keyManager.setActiveItem(chipIndex - 1);
|
||||
}
|
||||
}
|
||||
if (this._keyManager.activeItemIndex === chipIndex) {
|
||||
this._lastDestroyedIndex = chipIndex;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this._subscribed.delete(chip);
|
||||
this._chipSet.delete(chip);
|
||||
chip.destroy.unsubscribe();
|
||||
});
|
||||
|
||||
this._subscribed.set(chip, true);
|
||||
this._chipSet.set(chip, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a focus chip was recently destroyed so that we can refocus the next closest
|
||||
* one.
|
||||
*/
|
||||
protected _updateFocusForDestroyedChips() {
|
||||
let chipsArray = this.chips;
|
||||
|
||||
if (this._lastDestroyedIndex != null && chipsArray.length > 0) {
|
||||
// Check whether the destroyed chip was the last item
|
||||
const newFocusIndex = Math.min(this._lastDestroyedIndex, chipsArray.length - 1);
|
||||
this._keyManager.setActiveItem(newFocusIndex);
|
||||
let focusChip = this._keyManager.activeItem;
|
||||
|
||||
// Focus the chip
|
||||
if (focusChip) {
|
||||
focusChip.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset our destroyed index
|
||||
this._lastDestroyedIndex = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,4 +294,13 @@ export class MdChipList implements AfterContentInit, OnDestroy {
|
||||
return index >= 0 && index < this.chips.length;
|
||||
}
|
||||
|
||||
private _isInputEmpty(element: HTMLElement): boolean {
|
||||
if (element && element.nodeName.toLowerCase() === 'input') {
|
||||
let input = element as HTMLInputElement;
|
||||
|
||||
return !input.value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {Component, DebugElement} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {MdChip, MdChipsModule} from './index';
|
||||
|
||||
describe('Chip Remove', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
let testChip: TestChip;
|
||||
let chipDebugElement: DebugElement;
|
||||
let chipNativeElement: HTMLElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MdChipsModule],
|
||||
declarations: [
|
||||
TestChip
|
||||
]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(TestChip);
|
||||
testChip = fixture.debugElement.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
chipDebugElement = fixture.debugElement.query(By.directive(MdChip));
|
||||
chipNativeElement = chipDebugElement.nativeElement;
|
||||
}));
|
||||
|
||||
describe('basic behavior', () => {
|
||||
it('should applies the `mat-chip-remove` CSS class', () => {
|
||||
let hrefElement = chipNativeElement.querySelector('a')!;
|
||||
|
||||
expect(hrefElement.classList).toContain('mat-chip-remove');
|
||||
});
|
||||
|
||||
it('should emits (remove) on click', () => {
|
||||
let hrefElement = chipNativeElement.querySelector('a')!;
|
||||
|
||||
testChip.removable = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
spyOn(testChip, 'didRemove');
|
||||
|
||||
hrefElement.click();
|
||||
|
||||
expect(testChip.didRemove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-chip [removable]="removable" (remove)="didRemove()"><a mdChipRemove></a></md-chip>
|
||||
`
|
||||
})
|
||||
class TestChip {
|
||||
removable: boolean;
|
||||
|
||||
didRemove() {}
|
||||
}
|
||||
+161
-21
@@ -1,7 +1,10 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component, DebugElement} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {createKeyboardEvent} from '@angular/cdk/testing';
|
||||
import {MdChipList, MdChip, MdChipEvent, MdChipsModule} from './index';
|
||||
import {SPACE, DELETE, BACKSPACE} from '../core/keyboard/keycodes';
|
||||
import {Directionality} from '../core';
|
||||
|
||||
describe('Chips', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
@@ -10,12 +13,19 @@ describe('Chips', () => {
|
||||
let chipNativeElement: HTMLElement;
|
||||
let chipInstance: MdChip;
|
||||
|
||||
let dir = 'ltr';
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MdChipsModule],
|
||||
declarations: [
|
||||
BasicChip, SingleChip
|
||||
]
|
||||
],
|
||||
providers: [{
|
||||
provide: Directionality, useFactory: () => {
|
||||
return {value: dir};
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
@@ -47,25 +57,25 @@ describe('Chips', () => {
|
||||
describe('MdChip', () => {
|
||||
let testComponent: SingleChip;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SingleChip);
|
||||
fixture.detectChanges();
|
||||
|
||||
chipDebugElement = fixture.debugElement.query(By.directive(MdChip));
|
||||
chipListNativeElement = fixture.debugElement.query(By.directive(MdChipList)).nativeElement;
|
||||
chipNativeElement = chipDebugElement.nativeElement;
|
||||
chipInstance = chipDebugElement.injector.get(MdChip);
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
|
||||
document.body.appendChild(chipNativeElement);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(chipNativeElement);
|
||||
});
|
||||
|
||||
describe('basic behaviors', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SingleChip);
|
||||
fixture.detectChanges();
|
||||
|
||||
chipDebugElement = fixture.debugElement.query(By.directive(MdChip));
|
||||
chipListNativeElement = fixture.debugElement.query(By.directive(MdChipList)).nativeElement;
|
||||
chipNativeElement = chipDebugElement.nativeElement;
|
||||
chipInstance = chipDebugElement.injector.get(MdChip);
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
|
||||
document.body.appendChild(chipNativeElement);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(chipNativeElement);
|
||||
});
|
||||
|
||||
it('adds the `md-chip` class', () => {
|
||||
expect(chipNativeElement.classList).toContain('mat-chip');
|
||||
});
|
||||
@@ -110,7 +120,132 @@ describe('Chips', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(chipNativeElement.classList).toContain('mat-chip-selected');
|
||||
expect(testComponent.chipSelect).toHaveBeenCalledWith({ chip: chipInstance });
|
||||
expect(testComponent.chipSelect).toHaveBeenCalledWith({chip: chipInstance});
|
||||
});
|
||||
|
||||
it('allows removal', () => {
|
||||
spyOn(testComponent, 'chipRemove');
|
||||
|
||||
chipInstance.remove();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance});
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyboard behavior', () => {
|
||||
|
||||
describe('when selectable is true', () => {
|
||||
beforeEach(() => {
|
||||
testComponent.selectable = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should selects/deselects the currently focused chip on SPACE', () => {
|
||||
const SPACE_EVENT: KeyboardEvent = createKeyboardEvent('keydown', SPACE) as KeyboardEvent;
|
||||
const CHIP_EVENT: MdChipEvent = {chip: chipInstance};
|
||||
|
||||
spyOn(testComponent, 'chipSelect');
|
||||
spyOn(testComponent, 'chipDeselect');
|
||||
|
||||
// Use the spacebar to select the chip
|
||||
chipInstance._handleKeydown(SPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(chipInstance.selected).toBeTruthy();
|
||||
expect(testComponent.chipSelect).toHaveBeenCalledTimes(1);
|
||||
expect(testComponent.chipSelect).toHaveBeenCalledWith(CHIP_EVENT);
|
||||
|
||||
// Use the spacebar to deselect the chip
|
||||
chipInstance._handleKeydown(SPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(chipInstance.selected).toBeFalsy();
|
||||
expect(testComponent.chipDeselect).toHaveBeenCalledTimes(1);
|
||||
expect(testComponent.chipDeselect).toHaveBeenCalledWith(CHIP_EVENT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when selectable is false', () => {
|
||||
beforeEach(() => {
|
||||
testComponent.selectable = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('SPACE ignores selection', () => {
|
||||
const SPACE_EVENT: KeyboardEvent = createKeyboardEvent('keydown', SPACE) as KeyboardEvent;
|
||||
|
||||
spyOn(testComponent, 'chipSelect');
|
||||
|
||||
// Use the spacebar to attempt to select the chip
|
||||
chipInstance._handleKeydown(SPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(chipInstance.selected).toBeFalsy();
|
||||
expect(testComponent.chipSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when removable is true', () => {
|
||||
beforeEach(() => {
|
||||
testComponent.removable = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('DELETE emits the (remove) event', () => {
|
||||
const DELETE_EVENT = createKeyboardEvent('keydown', DELETE) as KeyboardEvent;
|
||||
|
||||
spyOn(testComponent, 'chipRemove');
|
||||
|
||||
// Use the delete to remove the chip
|
||||
chipInstance._handleKeydown(DELETE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.chipRemove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('BACKSPACE emits the (remove) event', () => {
|
||||
const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE) as KeyboardEvent;
|
||||
|
||||
spyOn(testComponent, 'chipRemove');
|
||||
|
||||
// Use the delete to remove the chip
|
||||
chipInstance._handleKeydown(BACKSPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.chipRemove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when removable is false', () => {
|
||||
beforeEach(() => {
|
||||
testComponent.removable = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('DELETE does not emit the (remove) event', () => {
|
||||
const DELETE_EVENT = createKeyboardEvent('keydown', DELETE) as KeyboardEvent;
|
||||
|
||||
spyOn(testComponent, 'chipRemove');
|
||||
|
||||
// Use the delete to remove the chip
|
||||
chipInstance._handleKeydown(DELETE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.chipRemove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('BACKSPACE does not emit the (remove) event', () => {
|
||||
const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE) as KeyboardEvent;
|
||||
|
||||
spyOn(testComponent, 'chipRemove');
|
||||
|
||||
// Use the delete to remove the chip
|
||||
chipInstance._handleKeydown(BACKSPACE_EVENT);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.chipRemove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the aria-label for disabled chips', () => {
|
||||
@@ -130,9 +265,11 @@ describe('Chips', () => {
|
||||
template: `
|
||||
<md-chip-list>
|
||||
<div *ngIf="shouldShow">
|
||||
<md-chip [color]="color" [selected]="selected" [disabled]="disabled"
|
||||
<md-chip [selectable]="selectable" [removable]="removable"
|
||||
[color]="color" [selected]="selected" [disabled]="disabled"
|
||||
(focus)="chipFocus($event)" (destroy)="chipDestroy($event)"
|
||||
(select)="chipSelect($event)" (deselect)="chipDeselect($event)">
|
||||
(select)="chipSelect($event)" (deselect)="chipDeselect($event)"
|
||||
(remove)="chipRemove($event)">
|
||||
{{name}}
|
||||
</md-chip>
|
||||
</div>
|
||||
@@ -143,12 +280,15 @@ class SingleChip {
|
||||
name: string = 'Test';
|
||||
color: string = 'primary';
|
||||
selected: boolean = false;
|
||||
selectable: boolean = true;
|
||||
removable: boolean = true;
|
||||
shouldShow: boolean = true;
|
||||
|
||||
chipFocus: (event?: MdChipEvent) => void = () => {};
|
||||
chipDestroy: (event?: MdChipEvent) => void = () => {};
|
||||
chipSelect: (event?: MdChipEvent) => void = () => {};
|
||||
chipDeselect: (event?: MdChipEvent) => void = () => {};
|
||||
chipRemove: (event?: MdChipEvent) => void = () => {};
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
||||
+111
-8
@@ -8,18 +8,21 @@
|
||||
|
||||
import {
|
||||
Directive,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output,
|
||||
Renderer2,
|
||||
forwardRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import {Focusable} from '../core/a11y/focus-key-manager';
|
||||
import {coerceBooleanProperty} from '@angular/cdk';
|
||||
import {CanColor, mixinColor} from '../core/common-behaviors/color';
|
||||
import {CanDisable, mixinDisabled} from '../core/common-behaviors/disabled';
|
||||
import {SPACE, BACKSPACE, DELETE} from '../core/keyboard/keycodes';
|
||||
|
||||
export interface MdChipEvent {
|
||||
chip: MdChip;
|
||||
@@ -58,12 +61,15 @@ export class MdBasicChip { }
|
||||
'[attr.disabled]': 'disabled || null',
|
||||
'[attr.aria-disabled]': 'disabled.toString()',
|
||||
'(click)': '_handleClick($event)',
|
||||
'(keydown)': '_handleKeydown($event)',
|
||||
'(focus)': '_hasFocus = true',
|
||||
'(blur)': '_hasFocus = false',
|
||||
}
|
||||
})
|
||||
export class MdChip extends _MdChipMixinBase implements Focusable, OnDestroy, CanColor, CanDisable {
|
||||
|
||||
@ContentChild(forwardRef(() => MdChipRemove)) _chipRemove: MdChipRemove;
|
||||
|
||||
/** Whether the chip is selected. */
|
||||
@Input() get selected(): boolean { return this._selected; }
|
||||
set selected(value: boolean) {
|
||||
@@ -72,6 +78,31 @@ export class MdChip extends _MdChipMixinBase implements Focusable, OnDestroy, Ca
|
||||
}
|
||||
protected _selected: boolean = false;
|
||||
|
||||
/**
|
||||
* Whether or not the chips are selectable. When a chip is not selectable,
|
||||
* changes to it's selected state are always ignored.
|
||||
*/
|
||||
@Input() get selectable(): boolean {
|
||||
return this._selectable;
|
||||
}
|
||||
|
||||
set selectable(value: boolean) {
|
||||
this._selectable = coerceBooleanProperty(value);
|
||||
}
|
||||
protected _selectable: boolean = true;
|
||||
|
||||
/**
|
||||
* Determines whether or not the chip displays the remove styling and emits (remove) events.
|
||||
*/
|
||||
@Input() get removable(): boolean {
|
||||
return this._removable;
|
||||
}
|
||||
|
||||
set removable(value: boolean) {
|
||||
this._removable = coerceBooleanProperty(value);
|
||||
}
|
||||
protected _removable: boolean = true;
|
||||
|
||||
/** Whether the chip has focus. */
|
||||
_hasFocus: boolean = false;
|
||||
|
||||
@@ -91,14 +122,14 @@ export class MdChip extends _MdChipMixinBase implements Focusable, OnDestroy, Ca
|
||||
super(renderer, elementRef);
|
||||
}
|
||||
|
||||
/** Emitted when a chip is to be removed. */
|
||||
@Output('remove') onRemove = new EventEmitter<MdChipEvent>();
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy.emit({chip: this});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the current selected state of this chip.
|
||||
* @return Whether the chip is selected.
|
||||
*/
|
||||
/** Toggles the current selected state of this chip. */
|
||||
toggleSelected(): boolean {
|
||||
this.selected = !this.selected;
|
||||
return this.selected;
|
||||
@@ -110,14 +141,86 @@ export class MdChip extends _MdChipMixinBase implements Focusable, OnDestroy, Ca
|
||||
this.onFocus.emit({chip: this});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows for programmatic removal of the chip. Called by the MdChipList when the DELETE or
|
||||
* BACKSPACE keys are pressed.
|
||||
*
|
||||
* Informs any listeners of the removal request. Does not remove the chip from the DOM.
|
||||
*/
|
||||
remove(): void {
|
||||
if (this.removable) {
|
||||
this.onRemove.emit({chip: this});
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensures events fire properly upon click. */
|
||||
_handleClick(event: Event) {
|
||||
// Check disabled
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
this.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.focus();
|
||||
}
|
||||
|
||||
/** Handle custom key presses. */
|
||||
_handleKeydown(event: KeyboardEvent) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.keyCode) {
|
||||
case DELETE:
|
||||
case BACKSPACE:
|
||||
// If we are removable, remove the focused chip
|
||||
this.remove();
|
||||
// Always prevent so page navigation does not occur
|
||||
event.preventDefault();
|
||||
break;
|
||||
case SPACE:
|
||||
// If we are selectable, toggle the focused chip
|
||||
if (this.selectable) {
|
||||
this.toggleSelected();
|
||||
}
|
||||
|
||||
// Always prevent space from scrolling the page since the list has focus
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Applies proper (click) support and adds styling for use with the Material Design "cancel" icon
|
||||
* available at https://material.io/icons/#ic_cancel.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <md-chip>
|
||||
* <md-icon mdChipRemove>cancel</md-icon>
|
||||
* </md-chip>
|
||||
*
|
||||
* You *may* use a custom icon, but you may need to override the `md-chip-remove` positioning styles
|
||||
* to properly center the icon within the chip.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[mdChipRemove], [matChipRemove]',
|
||||
host: {
|
||||
'class': 'mat-chip-remove',
|
||||
'(click)': '_handleClick($event)'
|
||||
}
|
||||
})
|
||||
export class MdChipRemove {
|
||||
constructor(protected _parentChip: MdChip) {}
|
||||
|
||||
/** Calls the parent chip's public `remove()` method if applicable. */
|
||||
_handleClick() {
|
||||
if (this._parentChip.removable) {
|
||||
this._parentChip.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
$mat-chip-vertical-padding: 8px;
|
||||
$mat-chip-horizontal-padding: 12px;
|
||||
$mat-chip-remove-margin: -4px;
|
||||
|
||||
$mat-chips-chip-margin: $mat-chip-horizontal-padding / 4;
|
||||
|
||||
@@ -24,6 +25,16 @@ $mat-chips-chip-margin: $mat-chip-horizontal-padding / 4;
|
||||
[dir='rtl'] & {
|
||||
margin: 0 $mat-chips-chip-margin 0 0;
|
||||
}
|
||||
|
||||
.mat-input-prefix & {
|
||||
&:last-child {
|
||||
margin-right: $mat-chips-chip-margin;
|
||||
}
|
||||
|
||||
[dir='rtl'] &:last-child {
|
||||
margin-left: $mat-chips-chip-margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include cdk-high-contrast {
|
||||
@@ -49,3 +60,11 @@ $mat-chips-chip-margin: $mat-chip-horizontal-padding / 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-input-prefix .mat-chip-list-wrapper {
|
||||
margin-bottom: $mat-chip-vertical-padding;
|
||||
}
|
||||
|
||||
.mat-chip-remove {
|
||||
margin: 0 $mat-chip-remove-margin 0 0;
|
||||
}
|
||||
|
||||
+10
-10
@@ -8,16 +8,16 @@
|
||||
|
||||
import {NgModule} from '@angular/core';
|
||||
import {MdChipList} from './chip-list';
|
||||
import {MdChip, MdBasicChip} from './chip';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
exports: [MdChipList, MdChip, MdBasicChip],
|
||||
declarations: [MdChipList, MdChip, MdBasicChip]
|
||||
})
|
||||
export class MdChipsModule {}
|
||||
|
||||
import {MdBasicChip, MdChip, MdChipRemove} from './chip';
|
||||
import {MdChipInput} from './chip-input';
|
||||
|
||||
export * from './chip-list';
|
||||
export * from './chip';
|
||||
export * from './chip-input';
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
exports: [MdChipList, MdChip, MdChipInput, MdChipRemove, MdChipRemove, MdBasicChip],
|
||||
declarations: [MdChipList, MdChip, MdChipInput, MdChipRemove, MdChipRemove, MdBasicChip]
|
||||
})
|
||||
export class MdChipsModule {}
|
||||
|
||||
@@ -511,7 +511,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
|
||||
/**
|
||||
* Throws an error if the container's input child was removed.
|
||||
*/
|
||||
private _validateInputChild() {
|
||||
protected _validateInputChild() {
|
||||
if (!this._mdInputChild) {
|
||||
throw getMdInputContainerMissingMdInputError();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user