Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chips): Add left/right arrow functionality. #2332

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/demo-app/chips/chips-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
}
}

md-basic-chip {
.md-basic-chip {
margin: auto 10px;
}
}
4 changes: 2 additions & 2 deletions src/lib/chips/_chips-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
66 changes: 62 additions & 4 deletions src/lib/chips/chip-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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');
Expand Down Expand Up @@ -181,6 +236,9 @@ class StaticChipList {
selectable: boolean = true;
remove: Number;

chipSelect(index: Number) {}
chipDeselect(index: Number) {}
chipSelect(index: Number) {
}

chipDeselect(index: Number) {
}
}
39 changes: 26 additions & 13 deletions src/lib/chips/chip-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/lib/chips/chip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand Down Expand Up @@ -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();

Expand Down
8 changes: 6 additions & 2 deletions src/lib/chips/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/lib/chips/chips.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down