-
Notifications
You must be signed in to change notification settings - Fork 6.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
26eb7ce
commit 1d96636
Showing
13 changed files
with
597 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,10 @@ | ||
.chips-demo { | ||
.md-chip-list-stacked { | ||
display: block; | ||
max-width: 200px; | ||
} | ||
|
||
md-basic-chip { | ||
margin: auto 10px; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,40 @@ | ||
import {Component} from '@angular/core'; | ||
|
||
export interface Person { | ||
name: string; | ||
} | ||
|
||
@Component({ | ||
moduleId: module.id, | ||
selector: 'chips-demo', | ||
templateUrl: 'chips-demo.html', | ||
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' } | ||
]; | ||
|
||
static alert(message: string): void { | ||
alert(message); | ||
} | ||
|
||
add(input: HTMLInputElement): void { | ||
if (input.value && input.value.trim() != '') { | ||
this.people.push({ name: input.value.trim() }); | ||
input.value = ''; | ||
} | ||
} | ||
|
||
toggleVisible(): void { | ||
this.visible = false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import {QueryList} from '@angular/core'; | ||
import {async, TestBed} from '@angular/core/testing'; | ||
import {MdBasicChip} from './index'; | ||
import {ChipListKeyManager} from './chip-list-key-manager'; | ||
|
||
/* | ||
* Create a fake Chip class so we don't have to test actual HTML elements. | ||
*/ | ||
class FakeChip extends MdBasicChip { | ||
constructor() { | ||
// Pass in null for the renderer/elementRef | ||
super(null, null); | ||
} | ||
|
||
// Override the required focus() method to NOT call the underlying renderer (which is null) | ||
focus() { | ||
this.didfocus.emit(); | ||
} | ||
} | ||
|
||
describe('ChipListKeyManager', () => { | ||
let items: QueryList<MdBasicChip>; | ||
let manager: ChipListKeyManager; | ||
|
||
beforeEach(async(() => { | ||
items = new QueryList<MdBasicChip>(); | ||
items.reset([ | ||
new FakeChip(), | ||
new FakeChip(), | ||
new FakeChip(), | ||
new FakeChip(), | ||
new FakeChip() | ||
]); | ||
|
||
manager = new ChipListKeyManager(items); | ||
|
||
TestBed.compileComponents(); | ||
})); | ||
|
||
describe('basic behaviors', () => { | ||
it('watches for chip focus', () => { | ||
let array = items.toArray(); | ||
let lastIndex = array.length - 1; | ||
let lastItem = array[lastIndex]; | ||
|
||
lastItem.focus(); | ||
|
||
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 | ||
midItem.destroy.emit(); | ||
|
||
// 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 | ||
lastItem.destroy.emit(); | ||
|
||
// It focuses the next-to-last item | ||
expect(manager.focusedItemIndex).toEqual(lastIndex - 1); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import {QueryList} from '@angular/core'; | ||
import {ListKeyManager} from '../core/a11y/list-key-manager'; | ||
import {MdBasicChip} from './chip'; | ||
|
||
/** | ||
* Manages keyboard events for the chip list and its chips. When instantiated | ||
* with a QueryList of MdBasicChip (i.e. any chip), it will ensure focus and | ||
* keyboard navigation are properly handled. | ||
*/ | ||
export class ChipListKeyManager extends ListKeyManager { | ||
private _subscribed: MdBasicChip[] = []; | ||
|
||
constructor(private _chips: QueryList<MdBasicChip>) { | ||
super(_chips); | ||
|
||
// 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<MdBasicChip>) => { | ||
this.subscribeChips(chips); | ||
}); | ||
} | ||
|
||
/** | ||
* 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<MdBasicChip>): void { | ||
chips.forEach((chip: MdBasicChip) => { | ||
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: MdBasicChip) { | ||
// If we've already been subscribed to a parent, do nothing | ||
if (this._subscribed.indexOf(chip) > -1) { | ||
return; | ||
} | ||
|
||
// Watch for focus events outside of the keyboard navigation | ||
chip.didfocus.subscribe(() => { | ||
let chipIndex: number = this._chips.toArray().indexOf(chip); | ||
|
||
if (this.isValidIndex(chipIndex)) { | ||
this.setFocus(chipIndex, false); | ||
} | ||
}); | ||
|
||
// 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.setFocus(chipIndex); | ||
} else if (chipIndex - 1 >= 0) { | ||
this.setFocus(chipIndex - 1); | ||
} | ||
} | ||
|
||
this._subscribed.splice(this._subscribed.indexOf(chip), 1); | ||
chip.destroy.unsubscribe(); | ||
}); | ||
|
||
this._subscribed.push(chip); | ||
} | ||
|
||
/** | ||
* 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; | ||
} | ||
} |
Oops, something went wrong.