From 3f2db2750e95cb387eca53603382d242511408af Mon Sep 17 00:00:00 2001 From: Topher Fangio Date: Wed, 21 Dec 2016 19:46:50 -0600 Subject: [PATCH] feat(chips): Add left/right arrow functionality. (#2332) In order to maintain consistentcy with the md1 chips, add the ability to use the left/right arrow keys to navigate chips. References #120. --- src/demo-app/chips/chips-demo.scss | 2 +- src/lib/chips/_chips-theme.scss | 4 +- src/lib/chips/chip-list.spec.ts | 66 ++++++++++++++++++++++++++++-- src/lib/chips/chip-list.ts | 39 ++++++++++++------ src/lib/chips/chip.spec.ts | 9 +++- src/lib/chips/chip.ts | 8 +++- src/lib/chips/chips.scss | 6 +-- 7 files changed, 107 insertions(+), 27 deletions(-) diff --git a/src/demo-app/chips/chips-demo.scss b/src/demo-app/chips/chips-demo.scss index f0ba734b465f..d9392867ba1b 100644 --- a/src/demo-app/chips/chips-demo.scss +++ b/src/demo-app/chips/chips-demo.scss @@ -17,7 +17,7 @@ } } - md-basic-chip { + .md-basic-chip { margin: auto 10px; } } \ No newline at end of file diff --git a/src/lib/chips/_chips-theme.scss b/src/lib/chips/_chips-theme.scss index 3dd5dccfc8e2..9f4932bc0fbc 100644 --- a/src/lib/chips/_chips-theme.scss +++ b/src/lib/chips/_chips-theme.scss @@ -22,12 +22,12 @@ $selected-background: if($is-dark-theme, md-color($background, app-bar), #808080); $selected-foreground: if($is-dark-theme, md-color($foreground, text), $light-selected-foreground); - .md-chip { + .md-chip:not(.md-basic-chip) { background-color: $unselected-background; color: $unselected-foreground; } - .md-chip.md-chip-selected { + .md-chip.md-chip-selected:not(.md-basic-chip) { background-color: $selected-background; color: $selected-foreground; diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts index 5e5073bccf52..927fa1ab19e2 100644 --- a/src/lib/chips/chip-list.spec.ts +++ b/src/lib/chips/chip-list.spec.ts @@ -4,7 +4,15 @@ import {By} from '@angular/platform-browser'; import {MdChip, MdChipList, MdChipsModule} from './index'; import {ListKeyManager} from '../core/a11y/list-key-manager'; import {FakeEvent} from '../core/a11y/list-key-manager.spec'; -import {SPACE} from '../core/keyboard/keycodes'; +import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes'; + +class FakeKeyboardEvent extends FakeEvent { + constructor(keyCode: number, protected target: HTMLElement) { + super(keyCode); + + this.target = target; + } +} describe('MdChipList', () => { let fixture: ComponentFixture; @@ -101,6 +109,50 @@ describe('MdChipList', () => { }); 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 = new FakeKeyboardEvent(LEFT_ARROW, lastNativeChip) as any; + let array = chips.toArray(); + let lastIndex = array.length - 1; + let lastItem = array[lastIndex]; + + // Focus the last item in the array + lastItem.focus(); + expect(manager.focusedItemIndex).toEqual(lastIndex); + + // Press the LEFT arrow + chipListInstance._keydown(LEFT_EVENT); + fixture.detectChanges(); + + // It focuses the next-to-last item + expect(manager.focusedItemIndex).toEqual(lastIndex - 1); + }); + + it('right arrow focuses next item', () => { + let nativeChips = chipListNativeElement.querySelectorAll('md-chip'); + let firstNativeChip = nativeChips[0] as HTMLElement; + + let RIGHT_EVENT: KeyboardEvent = new FakeKeyboardEvent(RIGHT_ARROW, firstNativeChip) as any; + let array = chips.toArray(); + let firstItem = array[0]; + + // Focus the last item in the array + firstItem.focus(); + expect(manager.focusedItemIndex).toEqual(0); + + // Press the RIGHT arrow + chipListInstance._keydown(RIGHT_EVENT); + fixture.detectChanges(); + + // It focuses the next-to-last item + expect(manager.focusedItemIndex).toEqual(1); + }); describe('when selectable is true', () => { beforeEach(() => { @@ -109,7 +161,10 @@ describe('MdChipList', () => { }); it('SPACE selects/deselects the currently focused chip', () => { - let SPACE_EVENT: KeyboardEvent = new FakeEvent(SPACE) as KeyboardEvent; + let nativeChips = chipListNativeElement.querySelectorAll('md-chip'); + let firstNativeChip = nativeChips[0] as HTMLElement; + + let SPACE_EVENT: KeyboardEvent = new FakeKeyboardEvent(SPACE, firstNativeChip) as any; let firstChip: MdChip = chips.toArray()[0]; spyOn(testComponent, 'chipSelect'); @@ -181,6 +236,9 @@ class StaticChipList { selectable: boolean = true; remove: Number; - chipSelect(index: Number) {} - chipDeselect(index: Number) {} + chipSelect(index: Number) { + } + + chipDeselect(index: Number) { + } } diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts index e6d90e55f63f..0d3ff0b91261 100644 --- a/src/lib/chips/chip-list.ts +++ b/src/lib/chips/chip-list.ts @@ -14,7 +14,7 @@ import { import {MdChip} from './chip'; import {ListKeyManager} from '../core/a11y/list-key-manager'; import {coerceBooleanProperty} from '../core/coercion/boolean-property'; -import {SPACE} from '../core/keyboard/keycodes'; +import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes'; /** * A material design chips component (named ChipList for it's similarity to the List component). @@ -98,18 +98,31 @@ export class MdChipList implements AfterContentInit { /** Passes relevant key presses to our key manager. */ _keydown(event: KeyboardEvent) { - 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; - default: - this._keyManager.onKeydown(event); + 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('md-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.focusPreviousItem(); + event.preventDefault(); + break; + case RIGHT_ARROW: + this._keyManager.focusNextItem(); + event.preventDefault(); + break; + default: + this._keyManager.onKeydown(event); + } } } diff --git a/src/lib/chips/chip.spec.ts b/src/lib/chips/chip.spec.ts index 9797909d7c65..08cb622323bf 100644 --- a/src/lib/chips/chip.spec.ts +++ b/src/lib/chips/chip.spec.ts @@ -38,8 +38,9 @@ describe('Chips', () => { document.body.removeChild(chipNativeElement); }); - it('does not add the `md-chip` class', () => { - expect(chipNativeElement.classList).not.toContain('md-chip'); + it('adds the `md-basic-chip` class', () => { + expect(chipNativeElement.classList).toContain('md-chip'); + expect(chipNativeElement.classList).toContain('md-basic-chip'); }); }); @@ -69,6 +70,10 @@ describe('Chips', () => { expect(chipNativeElement.classList).toContain('md-chip'); }); + it('does not add the `md-basic-chip` class', () => { + expect(chipNativeElement.classList).not.toContain('md-basic-chip'); + }); + it('emits focus on click', () => { spyOn(chipInstance, 'focus').and.callThrough(); diff --git a/src/lib/chips/chip.ts b/src/lib/chips/chip.ts index 7d39a22f31e7..2c9fdae96fbc 100644 --- a/src/lib/chips/chip.ts +++ b/src/lib/chips/chip.ts @@ -136,8 +136,12 @@ export class MdChip implements Focusable, OnInit, OnDestroy { private _addDefaultCSSClass() { let el: HTMLElement = this._elementRef.nativeElement; - if (el.nodeName.toLowerCase() == 'md-chip' || el.hasAttribute('md-chip')) { - el.classList.add('md-chip'); + // Always add the `md-chip` class + el.classList.add('md-chip'); + + // If we are a basic chip, also add the `md-basic-chip` class for :not() targeting + if (el.nodeName.toLowerCase() == 'md-basic-chip' || el.hasAttribute('md-basic-chip')) { + el.classList.add('md-basic-chip'); } } diff --git a/src/lib/chips/chips.scss b/src/lib/chips/chips.scss index 379277e9daab..da4a1255dfbc 100644 --- a/src/lib/chips/chips.scss +++ b/src/lib/chips/chips.scss @@ -15,7 +15,7 @@ $md-chips-chip-margin: $md-chip-horizontal-padding / 4; /* * Only apply the margins to chips */ - .md-chip { + .md-chip:not(.md-basic-chip) { margin: 0 $md-chips-chip-margin 0 $md-chips-chip-margin; // Remove the margin from the first element (in both LTR and RTL) @@ -50,7 +50,7 @@ $md-chips-chip-margin: $md-chip-horizontal-padding / 4; } } -.md-chip { +.md-chip:not(.md-basic-chip) { display: inline-block; padding: $md-chip-vertical-padding $md-chip-horizontal-padding $md-chip-vertical-padding $md-chip-horizontal-padding; @@ -63,7 +63,7 @@ $md-chips-chip-margin: $md-chip-horizontal-padding / 4; .md-chip-list-stacked .md-chip-list-wrapper { display: block; - .md-chip { + .md-chip:not(.md-basic-chip) { display: block; margin: 0; margin-bottom: $md-chip-vertical-padding;