mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(chips): add keyboard interaction. (#2046)
Add basic focus/keyboard support for chips. - Up/down arrows navigate chips. - Clicking a chip properly focuses it for subsequent keyboard navigation. - More demos. Confirmed AoT compatibility. References #120.
This commit is contained in:
committed by
Jeremy Elbourn
parent
bb1f00629b
commit
ba85883e64
@@ -2,11 +2,67 @@
|
||||
<section>
|
||||
<h3>Static Chips</h3>
|
||||
|
||||
<h5>Simple</h5>
|
||||
|
||||
<md-chip-list>
|
||||
<md-chip>Basic Chip</md-chip>
|
||||
<md-chip class="selected md-primary">Primary</md-chip>
|
||||
<md-chip class="selected md-accent">Accent</md-chip>
|
||||
<md-chip class="selected md-warn">Warn</md-chip>
|
||||
<md-chip>Chip 1</md-chip>
|
||||
<md-chip>Chip 2</md-chip>
|
||||
<md-chip>Chip 3</md-chip>
|
||||
</md-chip-list>
|
||||
|
||||
<h5>Advanced</h5>
|
||||
|
||||
<md-chip-list>
|
||||
<md-chip class="md-accent selected">Selected/Colored</md-chip>
|
||||
<md-chip class="md-warn" *ngIf="visible"
|
||||
(destroy)="alert('chip destroyed')" (click)="toggleVisible()">
|
||||
With Events
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
|
||||
<h5>Unstyled</h5>
|
||||
|
||||
<md-chip-list>
|
||||
<md-basic-chip>Basic Chip 1</md-basic-chip>
|
||||
<md-basic-chip>Basic Chip 2</md-basic-chip>
|
||||
<md-basic-chip>Basic Chip 3</md-basic-chip>
|
||||
</md-chip-list>
|
||||
|
||||
<h3>Material Contributors</h3>
|
||||
|
||||
<md-chip-list>
|
||||
<md-chip *ngFor="let person of people; let even = even" [ngClass]="[color, even ? 'selected' : '' ]">
|
||||
{{person.name}}
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
|
||||
<br />
|
||||
|
||||
<md-input #input (keyup.enter)="add(input)" (blur)="add(input)" placeholder="New Contributor...">
|
||||
</md-input>
|
||||
|
||||
<h3>Stacked Chips</h3>
|
||||
|
||||
<p>
|
||||
You can also stack the chips if you want them on top of each other.
|
||||
</p>
|
||||
|
||||
<md-chip-list class="md-chip-list-stacked">
|
||||
<md-chip (focus)="color = ''" class="selected">
|
||||
None
|
||||
</md-chip>
|
||||
|
||||
<md-chip (focus)="color = 'md-primary'" class="selected md-primary">
|
||||
Primary
|
||||
</md-chip>
|
||||
|
||||
<md-chip (focus)="color = 'md-accent'" class="selected md-accent">
|
||||
Accent
|
||||
</md-chip>
|
||||
|
||||
<md-chip (focus)="color = 'md-warn'" class="selected md-warn">
|
||||
Warn
|
||||
</md-chip>
|
||||
</md-chip-list>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1,2 +1,10 @@
|
||||
.chips-demo {
|
||||
.md-chip-list-stacked {
|
||||
display: block;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
md-basic-chip {
|
||||
margin: auto 10px;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MdInput} from '@angular/material';
|
||||
|
||||
export interface Person {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
@@ -7,4 +12,30 @@ import {Component} from '@angular/core';
|
||||
styleUrls: ['chips-demo.css']
|
||||
})
|
||||
export class ChipsDemo {
|
||||
visible: boolean = true;
|
||||
color: string = '';
|
||||
|
||||
people: Person[] = [
|
||||
{ name: 'Kara' },
|
||||
{ name: 'Jeremy' },
|
||||
{ name: 'Topher' },
|
||||
{ name: 'Elad' },
|
||||
{ name: 'Kristiyan' },
|
||||
{ name: 'Paul' }
|
||||
];
|
||||
|
||||
alert(message: string): void {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
add(input: MdInput): void {
|
||||
if (input.value && input.value.trim() != '') {
|
||||
this.people.push({ name: input.value.trim() });
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
toggleVisible(): void {
|
||||
this.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
}
|
||||
|
||||
.md-chip.selected {
|
||||
// There is no dark theme in the current spec, so this applies to both
|
||||
background-color: #808080;
|
||||
|
||||
// Use a contrast color for a grey very close to the background color
|
||||
color: md-contrast($md-grey, 600);
|
||||
|
||||
&.md-primary {
|
||||
background-color: md-color($primary, 500);
|
||||
color: md-contrast($primary, 500);
|
||||
@@ -26,4 +32,4 @@
|
||||
color: md-contrast($warn, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component, DebugElement} from '@angular/core';
|
||||
import {Component, DebugElement, QueryList} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MdChipList, MdChipsModule} from './index';
|
||||
import {MdChip, MdChipList, MdChipsModule} from './index';
|
||||
import {ListKeyManager} from '../core/a11y/list-key-manager';
|
||||
|
||||
describe('MdChip', () => {
|
||||
describe('MdChipList', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
let chipListDebugElement: DebugElement;
|
||||
let chipListNativeElement: HTMLElement;
|
||||
let chipListInstance: MdChipList;
|
||||
let testComponent: StaticChipList;
|
||||
let items: QueryList<MdChip>;
|
||||
let manager: ListKeyManager;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -15,39 +22,87 @@ describe('MdChip', () => {
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(StaticChipList);
|
||||
fixture.detectChanges();
|
||||
|
||||
chipListDebugElement = fixture.debugElement.query(By.directive(MdChipList));
|
||||
chipListNativeElement = chipListDebugElement.nativeElement;
|
||||
chipListInstance = chipListDebugElement.componentInstance;
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
}));
|
||||
|
||||
describe('basic behaviors', () => {
|
||||
let chipListDebugElement: DebugElement;
|
||||
let chipListNativeElement: HTMLElement;
|
||||
let chipListInstance: MdChipList;
|
||||
let testComponent: StaticChipList;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StaticChipList);
|
||||
fixture.detectChanges();
|
||||
|
||||
chipListDebugElement = fixture.debugElement.query(By.directive(MdChipList));
|
||||
chipListNativeElement = chipListDebugElement.nativeElement;
|
||||
chipListInstance = chipListDebugElement.componentInstance;
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
});
|
||||
|
||||
it('adds the `md-chip-list` class', () => {
|
||||
expect(chipListNativeElement.classList).toContain('md-chip-list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('focus behaviors', () => {
|
||||
beforeEach(() => {
|
||||
items = chipListInstance.chips;
|
||||
manager = chipListInstance._keyManager;
|
||||
});
|
||||
|
||||
it('watches for chip focus', () => {
|
||||
let array = items.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
|
||||
lastItem.focus();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(manager.focusedItemIndex).toBe(lastIndex);
|
||||
});
|
||||
|
||||
describe('on chip destroy', () => {
|
||||
it('focuses the next item', () => {
|
||||
let array = items.toArray();
|
||||
let midItem = array[2];
|
||||
|
||||
// Focus the middle item
|
||||
midItem.focus();
|
||||
|
||||
// Destroy the middle item
|
||||
testComponent.remove = 2;
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the 4th item (now at index 2)
|
||||
expect(manager.focusedItemIndex).toEqual(2);
|
||||
});
|
||||
|
||||
it('focuses the previous item', () => {
|
||||
let array = items.toArray();
|
||||
let lastIndex = array.length - 1;
|
||||
let lastItem = array[lastIndex];
|
||||
|
||||
// Focus the last item
|
||||
lastItem.focus();
|
||||
|
||||
// Destroy the last item
|
||||
testComponent.remove = lastIndex;
|
||||
fixture.detectChanges();
|
||||
|
||||
// It focuses the next-to-last item
|
||||
expect(manager.focusedItemIndex).toEqual(lastIndex - 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-chip-list>
|
||||
<md-chip>{{name}} 1</md-chip>
|
||||
<md-chip>{{name}} 2</md-chip>
|
||||
<md-chip>{{name}} 3</md-chip>
|
||||
<div *ngIf="remove != 0"><md-chip>{{name}} 1</md-chip></div>
|
||||
<div *ngIf="remove != 1"><md-chip>{{name}} 2</md-chip></div>
|
||||
<div *ngIf="remove != 2"><md-chip>{{name}} 3</md-chip></div>
|
||||
<div *ngIf="remove != 3"><md-chip>{{name}} 4</md-chip></div>
|
||||
<div *ngIf="remove != 4"><md-chip>{{name}} 5</md-chip></div>
|
||||
</md-chip-list>
|
||||
`
|
||||
})
|
||||
class StaticChipList {
|
||||
name: 'Test';
|
||||
remove: Number;
|
||||
}
|
||||
|
||||
+114
-4
@@ -1,32 +1,142 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ContentChildren,
|
||||
ElementRef,
|
||||
ModuleWithProviders,
|
||||
NgModule,
|
||||
QueryList,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
|
||||
import {MdChip} from './chip';
|
||||
import {ListKeyManager} from '../core/a11y/list-key-manager';
|
||||
|
||||
/**
|
||||
* A material design chips component (named ChipList for it's similarity to the List component).
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* <md-chip-list>
|
||||
* <md-chip>Chip 1<md-chip>
|
||||
* <md-chip>Chip 2<md-chip>
|
||||
* </md-chip-list>
|
||||
*/
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'md-chip-list',
|
||||
template: `<ng-content></ng-content>`,
|
||||
template: `<div class="md-chip-list-wrapper"><ng-content></ng-content></div>`,
|
||||
host: {
|
||||
// Properties
|
||||
'tabindex': '0',
|
||||
'role': 'listbox',
|
||||
'class': 'md-chip-list'
|
||||
'class': 'md-chip-list',
|
||||
|
||||
// Events
|
||||
'(focus)': '_keyManager.focusFirstItem()',
|
||||
'(keydown)': 'keydown($event)'
|
||||
},
|
||||
queries: {
|
||||
chips: new ContentChildren(MdChip)
|
||||
},
|
||||
styleUrls: ['chips.css'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MdChipList {
|
||||
export class MdChipList implements AfterContentInit {
|
||||
|
||||
/** Track which chips we're listening to for focus/destruction. */
|
||||
private _subscribed: WeakMap<MdChip, boolean> = new WeakMap();
|
||||
|
||||
/** The ListKeyManager which handles focus. */
|
||||
_keyManager: ListKeyManager;
|
||||
|
||||
/** The chip components contained within this chip list. */
|
||||
chips: QueryList<MdChip>;
|
||||
|
||||
constructor(private _elementRef: ElementRef) {}
|
||||
|
||||
ngAfterContentInit(): void {}
|
||||
ngAfterContentInit(): void {
|
||||
this._keyManager = new ListKeyManager(this.chips).withFocusWrap();
|
||||
|
||||
// Go ahead and subscribe all of the initial chips
|
||||
this.subscribeChips(this.chips);
|
||||
|
||||
// When the list changes, re-subscribe
|
||||
this.chips.changes.subscribe((chips: QueryList<MdChip>) => {
|
||||
this.subscribeChips(chips);
|
||||
});
|
||||
}
|
||||
|
||||
/** Pass relevant key presses to our key manager. */
|
||||
keydown(event: KeyboardEvent) {
|
||||
this._keyManager.onKeydown(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through the list of chips and add them to our list of
|
||||
* subscribed chips.
|
||||
*
|
||||
* @param chips The list of chips to be subscribed.
|
||||
*/
|
||||
protected subscribeChips(chips: QueryList<MdChip>): void {
|
||||
chips.forEach(chip => this.addChip(chip));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a specific chip to our subscribed list. If the chip has
|
||||
* already been subscribed, this ensures it is only subscribed
|
||||
* once.
|
||||
*
|
||||
* @param chip The chip to be subscribed (or checked for existing
|
||||
* subscription).
|
||||
*/
|
||||
protected addChip(chip: MdChip) {
|
||||
// If we've already been subscribed to a parent, do nothing
|
||||
if (this._subscribed.has(chip)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Watch for focus events outside of the keyboard navigation
|
||||
chip.onFocus.subscribe(() => {
|
||||
let chipIndex: number = this.chips.toArray().indexOf(chip);
|
||||
|
||||
if (this.isValidIndex(chipIndex)) {
|
||||
this._keyManager.updateFocusedItemIndex(chipIndex);
|
||||
}
|
||||
});
|
||||
|
||||
// On destroy, remove the item from our list, and check focus
|
||||
chip.destroy.subscribe(() => {
|
||||
let chipIndex: number = this.chips.toArray().indexOf(chip);
|
||||
|
||||
if (this.isValidIndex(chipIndex)) {
|
||||
// Check whether the chip is the last item
|
||||
if (chipIndex < this.chips.length - 1) {
|
||||
this._keyManager.setFocus(chipIndex);
|
||||
} else if (chipIndex - 1 >= 0) {
|
||||
this._keyManager.setFocus(chipIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
this._subscribed.delete(chip);
|
||||
chip.destroy.unsubscribe();
|
||||
});
|
||||
|
||||
this._subscribed.set(chip, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to ensure all indexes are valid.
|
||||
*
|
||||
* @param index The index to be checked.
|
||||
* @returns {boolean} True if the index is valid for our list of chips.
|
||||
*/
|
||||
private isValidIndex(index: number): boolean {
|
||||
return index >= 0 && index < this.chips.length;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
|
||||
+85
-14
@@ -1,47 +1,118 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component, DebugElement} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {MdChip, MdChipsModule} from './index';
|
||||
import {MdChipList, MdChip, MdChipsModule} from './index';
|
||||
|
||||
describe('MdChip', () => {
|
||||
describe('Chips', () => {
|
||||
let fixture: ComponentFixture<any>;
|
||||
let chipDebugElement: DebugElement;
|
||||
let chipListNativeElement: HTMLElement;
|
||||
let chipNativeElement: HTMLElement;
|
||||
let chipInstance: MdChip;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MdChipsModule.forRoot()],
|
||||
declarations: [
|
||||
SingleChip
|
||||
BasicChip, SingleChip
|
||||
]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
}));
|
||||
|
||||
describe('basic behaviors', () => {
|
||||
let chipDebugElement: DebugElement;
|
||||
let chipNativeElement: HTMLElement;
|
||||
let chipInstance: MdChip;
|
||||
let testComponent: SingleChip;
|
||||
describe('MdBasicChip', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SingleChip);
|
||||
fixture = TestBed.createComponent(BasicChip);
|
||||
fixture.detectChanges();
|
||||
|
||||
chipDebugElement = fixture.debugElement.query(By.directive(MdChip));
|
||||
chipNativeElement = chipDebugElement.nativeElement;
|
||||
chipInstance = chipDebugElement.componentInstance;
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
|
||||
document.body.appendChild(chipNativeElement);
|
||||
});
|
||||
|
||||
it('adds the `md-chip` class', () => {
|
||||
expect(chipNativeElement.classList).toContain('md-chip');
|
||||
afterEach(() => {
|
||||
document.body.removeChild(chipNativeElement);
|
||||
});
|
||||
|
||||
it('does not add the `md-chip` class', () => {
|
||||
expect(chipNativeElement.classList).not.toContain('md-chip');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MdChip', () => {
|
||||
let testComponent: SingleChip;
|
||||
|
||||
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.componentInstance;
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
|
||||
document.body.appendChild(chipNativeElement);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(chipNativeElement);
|
||||
});
|
||||
|
||||
it('adds the `md-chip` class', () => {
|
||||
expect(chipNativeElement.classList).toContain('md-chip');
|
||||
});
|
||||
|
||||
it('emits focus on click', () => {
|
||||
spyOn(chipInstance, 'focus').and.callThrough();
|
||||
|
||||
chipNativeElement.click();
|
||||
|
||||
expect(chipInstance.focus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('emits destroy on destruction', () => {
|
||||
spyOn(testComponent, 'chipDestroy').and.callThrough();
|
||||
|
||||
// Force a destroy callback
|
||||
testComponent.shouldShow = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.chipDestroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
template: `<md-chip>{{name}}</md-chip>`
|
||||
template: `
|
||||
<md-chip-list>
|
||||
<div *ngIf="shouldShow">
|
||||
<md-chip (focus)="chipFocus($event)" (destroy)="chipDestroy($event)">
|
||||
{{name}}
|
||||
</md-chip>
|
||||
</div>
|
||||
</md-chip-list>`
|
||||
})
|
||||
class SingleChip {
|
||||
name: 'Test';
|
||||
name: String = 'Test';
|
||||
shouldShow: Boolean = true;
|
||||
|
||||
chipFocus() {
|
||||
}
|
||||
|
||||
chipDestroy() {
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `<md-basic-chip>{{name}}</md-basic-chip>`
|
||||
})
|
||||
class BasicChip {
|
||||
}
|
||||
|
||||
+85
-7
@@ -1,17 +1,95 @@
|
||||
import { Component, ElementRef, Renderer } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
Renderer
|
||||
} from '@angular/core';
|
||||
|
||||
import {MdFocusable} from '../core/a11y/list-key-manager';
|
||||
import {coerceBooleanProperty} from '../core/coersion/boolean-property';
|
||||
|
||||
export interface MdChipEvent {
|
||||
chip: MdChip;
|
||||
}
|
||||
|
||||
/**
|
||||
* A material design styled Chip component. Used inside the ChipList component.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'md-chip, [md-chip]',
|
||||
selector: 'md-basic-chip, [md-basic-chip], md-chip, [md-chip]',
|
||||
template: `<ng-content></ng-content>`,
|
||||
host: {
|
||||
// Properties
|
||||
'class': 'md-chip',
|
||||
'tabindex': '-1',
|
||||
'role': 'option'
|
||||
'role': 'option',
|
||||
|
||||
'[attr.disabled]': 'disabled',
|
||||
'[attr.aria-disabled]': '_isAriaDisabled',
|
||||
|
||||
'(click)': '_handleClick($event)'
|
||||
}
|
||||
})
|
||||
export class MdChip {
|
||||
export class MdChip implements MdFocusable, OnInit, OnDestroy {
|
||||
|
||||
/* Whether or not the chip is disabled. */
|
||||
protected _disabled: boolean = null;
|
||||
|
||||
/**
|
||||
* Emitted when the chip is focused.
|
||||
*/
|
||||
onFocus = new EventEmitter<MdChipEvent>();
|
||||
|
||||
/**
|
||||
* Emitted when the chip is destroyed.
|
||||
*/
|
||||
@Output() destroy = new EventEmitter<MdChipEvent>();
|
||||
|
||||
constructor(protected _renderer: Renderer, protected _elementRef: ElementRef) {}
|
||||
|
||||
ngAfterContentInit(): void {}
|
||||
ngOnInit(): void {
|
||||
let el: HTMLElement = this._elementRef.nativeElement;
|
||||
|
||||
if (el.nodeName.toLowerCase() == 'md-chip' || el.hasAttribute('md-chip')) {
|
||||
el.classList.add('md-chip');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy.emit({ chip: this });
|
||||
}
|
||||
|
||||
/** Whether or not the chip is disabled. */
|
||||
@Input() get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
/** Sets the disabled state of the chip. */
|
||||
set disabled(value: boolean) {
|
||||
this._disabled = coerceBooleanProperty(value) ? true : null;
|
||||
}
|
||||
|
||||
/** A String representation of the current disabled state. */
|
||||
get _isAriaDisabled(): string {
|
||||
return String(coerceBooleanProperty(this.disabled));
|
||||
}
|
||||
|
||||
/** Allows for programmatic focusing of the chip. */
|
||||
focus(): void {
|
||||
this._renderer.invokeElementMethod(this._elementRef.nativeElement, 'focus');
|
||||
this.onFocus.emit({ chip: this });
|
||||
}
|
||||
|
||||
/** Ensures events fire properly upon click. */
|
||||
_handleClick(event: Event) {
|
||||
// Check disabled
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,53 @@
|
||||
$md-chip-vertical-padding: 8px;
|
||||
$md-chip-horizontal-padding: 12px;
|
||||
$md-chip-font-size: 13px;
|
||||
$md-chip-line-height: 16px;
|
||||
|
||||
.md-chip-list {
|
||||
padding: $md-chip-horizontal-padding;
|
||||
$md-chips-chip-margin: $md-chip-horizontal-padding / 4;
|
||||
|
||||
.md-chip-list-wrapper {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
/*
|
||||
* Only apply the margins to chips
|
||||
*/
|
||||
.md-chip {
|
||||
margin: 0 $md-chips-chip-margin 0 $md-chips-chip-margin;
|
||||
|
||||
// Remove the margin from the first element (in both LTR and RTL)
|
||||
&:first-child {
|
||||
margin: {
|
||||
left: 0;
|
||||
right: $md-chips-chip-margin;
|
||||
}
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin: {
|
||||
left: $md-chips-chip-margin;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the margin from the last element (in both LTR and RTL)
|
||||
&:last-child {
|
||||
margin: {
|
||||
left: $md-chips-chip-margin;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin: {
|
||||
left: 0;
|
||||
right: $md-chips-chip-margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.md-chip {
|
||||
@@ -11,6 +56,25 @@ $md-chip-horizontal-padding: 12px;
|
||||
$md-chip-vertical-padding $md-chip-horizontal-padding;
|
||||
border-radius: $md-chip-horizontal-padding * 2;
|
||||
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
font-size: $md-chip-font-size;
|
||||
line-height: $md-chip-line-height;
|
||||
}
|
||||
|
||||
.md-chip-list-stacked .md-chip-list-wrapper {
|
||||
display: block;
|
||||
|
||||
.md-chip {
|
||||
display: block;
|
||||
margin: 0;
|
||||
margin-bottom: $md-chip-vertical-padding;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin: 0;
|
||||
margin-bottom: $md-chip-vertical-padding;
|
||||
}
|
||||
|
||||
&:last-child, [dir='rtl'] &:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class FakeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe('ListKeyManager', () => {
|
||||
let keyManager: ListKeyManager;
|
||||
let itemList: FakeQueryList<FakeFocusable>;
|
||||
@@ -210,6 +209,16 @@ describe('ListKeyManager', () => {
|
||||
expect(itemList.items[1].focus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should allow setting the focused item without calling focus', () => {
|
||||
expect(keyManager.focusedItemIndex)
|
||||
.toBe(0, `Expected focus to be on the first item of the list.`);
|
||||
|
||||
keyManager.updateFocusedItemIndex(1);
|
||||
expect(keyManager.focusedItemIndex)
|
||||
.toBe(1, `Expected focusedItemIndex to be updated after calling updateFocusedItemIndex().`);
|
||||
expect(itemList.items[1].focus).not.toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should focus the first item when focusFirstItem() is called', () => {
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
keyManager.onKeydown(DOWN_ARROW_EVENT);
|
||||
|
||||
@@ -32,7 +32,11 @@ export class ListKeyManager {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the focus of the list to the item at the index specified. */
|
||||
/**
|
||||
* Sets the focus of the list to the item at the index specified.
|
||||
*
|
||||
* @param index The index of the item to be focused.
|
||||
*/
|
||||
setFocus(index: number): void {
|
||||
this._focusedItemIndex = index;
|
||||
this._items.toArray()[index].focus();
|
||||
@@ -89,6 +93,11 @@ export class ListKeyManager {
|
||||
return this._focusedItemIndex;
|
||||
}
|
||||
|
||||
/** Allows setting of the focusedItemIndex without focusing the item. */
|
||||
updateFocusedItemIndex(index: number) {
|
||||
this._focusedItemIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable that emits any time the TAB key is pressed, so components can react
|
||||
* when focus is shifted off of the list.
|
||||
|
||||
@@ -20,3 +20,5 @@ export const SPACE = 32;
|
||||
export const TAB = 9;
|
||||
|
||||
export const ESCAPE = 27;
|
||||
export const BACKSPACE = 8;
|
||||
export const DELETE = 46;
|
||||
|
||||
@@ -16,7 +16,7 @@ export function config(config) {
|
||||
require('karma-browserstack-launcher'),
|
||||
require('karma-sauce-launcher'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-firefox-launcher'),
|
||||
require('karma-firefox-launcher')
|
||||
],
|
||||
files: [
|
||||
{pattern: 'dist/vendor/core-js/client/core.js', included: true, watched: false},
|
||||
|
||||
Reference in New Issue
Block a user