Skip to content

Commit 1335a0c

Browse files
committed
fix(selection-list): restore focus if active item is destroyed
* Removes the `destroyed` and `onFocus` observables and instead just calls methods on the injected `MdSelectionList` instance. * Fixes that the active item is not updating if the active item is being destroyed.
1 parent 53c42a4 commit 1335a0c

File tree

2 files changed

+55
-82
lines changed

2 files changed

+55
-82
lines changed

src/lib/list/selection-list.spec.ts

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {DOWN_ARROW, SPACE, UP_ARROW} from '@angular/cdk/keycodes';
22
import {Platform} from '@angular/cdk/platform';
3-
import {createKeyboardEvent} from '@angular/cdk/testing';
3+
import {createKeyboardEvent, dispatchFakeEvent} from '@angular/cdk/testing';
44
import {Component, DebugElement} from '@angular/core';
55
import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing';
66
import {By} from '@angular/platform-browser';
@@ -28,24 +28,29 @@ describe('MatSelectionList', () => {
2828
TestBed.compileComponents();
2929
}));
3030

31+
3132
beforeEach(async(() => {
3233
fixture = TestBed.createComponent(SelectionListWithListOptions);
34+
fixture.detectChanges();
35+
3336
listOption = fixture.debugElement.queryAll(By.directive(MatListOption));
3437
listItemEl = fixture.debugElement.query(By.css('.mat-list-item'));
3538
selectionList = fixture.debugElement.query(By.directive(MatSelectionList));
36-
fixture.detectChanges();
3739
}));
3840

3941
it('should add and remove focus class on focus/blur', () => {
40-
expect(listItemEl.nativeElement.classList).not.toContain('mat-list-item-focus');
42+
// Use the second list item, because the first one is always disabled.
43+
const listItem = listOption[1].nativeElement;
4144

42-
listOption[0].componentInstance._handleFocus();
45+
expect(listItem.classList).not.toContain('mat-list-item-focus');
46+
47+
dispatchFakeEvent(listItem, 'focus');
4348
fixture.detectChanges();
44-
expect(listItemEl.nativeElement.className).toContain('mat-list-item-focus');
49+
expect(listItem.className).toContain('mat-list-item-focus');
4550

46-
listOption[0].componentInstance._handleBlur();
51+
dispatchFakeEvent(listItem, 'blur');
4752
fixture.detectChanges();
48-
expect(listItemEl.nativeElement.className).not.toContain('mat-list-item-focus');
53+
expect(listItem.className).not.toContain('mat-list-item-focus');
4954
});
5055

5156
it('should be able to set a value on a list option', () => {
@@ -144,29 +149,36 @@ describe('MatSelectionList', () => {
144149
createKeyboardEvent('keydown', SPACE, testListItem);
145150
let selectList =
146151
selectionList.injector.get<MatSelectionList>(MatSelectionList).selectedOptions;
147-
let options = selectionList.componentInstance.options;
148-
let array = options.toArray();
149-
let focusItem = array[1];
150152
expect(selectList.selected.length).toBe(0);
151153

152-
focusItem.focus();
154+
dispatchFakeEvent(testListItem, 'focus');
153155
selectionList.componentInstance._keydown(SPACE_EVENT);
154156

155157
fixture.detectChanges();
156158

157159
expect(selectList.selected.length).toBe(1);
158160
});
159161

162+
it('should restore focus if active option is destroyed', () => {
163+
const manager = selectionList.componentInstance._keyManager;
164+
165+
listOption[3].componentInstance._handleFocus();
166+
167+
expect(manager.activeItemIndex).toBe(3);
168+
169+
fixture.componentInstance.showLastOption = false;
170+
fixture.detectChanges();
171+
172+
expect(manager.activeItemIndex).toBe(2);
173+
});
174+
160175
it('should focus previous item when press UP ARROW', () => {
161176
let testListItem = listOption[2].nativeElement as HTMLElement;
162177
let UP_EVENT: KeyboardEvent =
163178
createKeyboardEvent('keydown', UP_ARROW, testListItem);
164-
let options = selectionList.componentInstance.options;
165-
let array = options.toArray();
166-
let focusItem = array[2];
167179
let manager = selectionList.componentInstance._keyManager;
168180

169-
focusItem.focus();
181+
dispatchFakeEvent(listOption[2].nativeElement, 'focus');
170182
expect(manager.activeItemIndex).toEqual(2);
171183

172184
selectionList.componentInstance._keydown(UP_EVENT);
@@ -180,12 +192,9 @@ describe('MatSelectionList', () => {
180192
let testListItem = listOption[2].nativeElement as HTMLElement;
181193
let DOWN_EVENT: KeyboardEvent =
182194
createKeyboardEvent('keydown', DOWN_ARROW, testListItem);
183-
let options = selectionList.componentInstance.options;
184-
let array = options.toArray();
185-
let focusItem = array[2];
186195
let manager = selectionList.componentInstance._keyManager;
187196

188-
focusItem.focus();
197+
dispatchFakeEvent(listOption[2].nativeElement, 'focus');
189198
expect(manager.activeItemIndex).toEqual(2);
190199

191200
selectionList.componentInstance._keydown(DOWN_EVENT);
@@ -404,11 +413,12 @@ describe('MatSelectionList', () => {
404413
<mat-list-option checkboxPosition="before" value="sent-mail">
405414
Sent Mail
406415
</mat-list-option>
407-
<mat-list-option checkboxPosition="before" value="drafts">
416+
<mat-list-option checkboxPosition="before" value="drafts" *ngIf="showLastOption">
408417
Drafts
409418
</mat-list-option>
410419
</mat-selection-list>`})
411420
class SelectionListWithListOptions {
421+
showLastOption: boolean = true;
412422
}
413423

414424
@Component({template: `

src/lib/list/selection-list.ts

Lines changed: 25 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {FocusableOption, FocusKeyManager} from '@angular/cdk/a11y';
1010
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1111
import {SelectionModel} from '@angular/cdk/collections';
1212
import {SPACE} from '@angular/cdk/keycodes';
13-
import {RxChain, startWith, switchMap} from '@angular/cdk/rxjs';
1413
import {
1514
AfterContentInit,
1615
ChangeDetectionStrategy,
@@ -37,8 +36,6 @@ import {
3736
mixinDisabled,
3837
mixinDisableRipple,
3938
} from '@angular/material/core';
40-
import {merge} from 'rxjs/observable/merge';
41-
import {Subscription} from 'rxjs/Subscription';
4239

4340

4441
/** @docs-private */
@@ -54,8 +51,6 @@ export interface MatSelectionListOptionEvent {
5451
option: MatListOption;
5552
}
5653

57-
const FOCUSED_STYLE: string = 'mat-list-item-focus';
58-
5954
/**
6055
* Component for list-options of selection-list. Each list-option can automatically
6156
* generate a checkbox and can put current item into the selectionModel of selection-list
@@ -69,10 +64,11 @@ const FOCUSED_STYLE: string = 'mat-list-item-focus';
6964
'role': 'option',
7065
'class': 'mat-list-item mat-list-option',
7166
'(focus)': '_handleFocus()',
72-
'(blur)': '_handleBlur()',
67+
'(blur)': '_hasFocus = false',
7368
'(click)': '_handleClick()',
7469
'tabindex': '-1',
7570
'[class.mat-list-item-disabled]': 'disabled',
71+
'[class.mat-list-item-focus]': '_hasFocus',
7672
'[attr.aria-selected]': 'selected.toString()',
7773
'[attr.aria-disabled]': 'disabled.toString()',
7874
},
@@ -109,18 +105,12 @@ export class MatListOption extends _MatListOptionMixinBase
109105
get selected() { return this._selected; }
110106
set selected(value: boolean) { this._selected = coerceBooleanProperty(value); }
111107

112-
/** Emitted when the option is focused. */
113-
onFocus = new EventEmitter<MatSelectionListOptionEvent>();
114-
115108
/** Emitted when the option is selected. */
116109
@Output() selectChange = new EventEmitter<MatSelectionListOptionEvent>();
117110

118111
/** Emitted when the option is deselected. */
119112
@Output() deselected = new EventEmitter<MatSelectionListOptionEvent>();
120113

121-
/** Emitted when the option is destroyed. */
122-
@Output() destroyed = new EventEmitter<MatSelectionListOptionEvent>();
123-
124114
constructor(private _renderer: Renderer2,
125115
private _element: ElementRef,
126116
private _changeDetector: ChangeDetectorRef,
@@ -138,7 +128,7 @@ export class MatListOption extends _MatListOptionMixinBase
138128
}
139129

140130
ngOnDestroy(): void {
141-
this.destroyed.emit({option: this});
131+
this.selectionList._removeOptionFromList(this);
142132
}
143133

144134
/** Toggles the selection state of the option. */
@@ -151,7 +141,6 @@ export class MatListOption extends _MatListOptionMixinBase
151141
/** Allows for programmatic focusing of the option. */
152142
focus(): void {
153143
this._element.nativeElement.focus();
154-
this.onFocus.emit({option: this});
155144
}
156145

157146
/** Whether this list item should show a ripple effect when clicked. */
@@ -167,11 +156,7 @@ export class MatListOption extends _MatListOptionMixinBase
167156

168157
_handleFocus() {
169158
this._hasFocus = true;
170-
this._renderer.addClass(this._element.nativeElement, FOCUSED_STYLE);
171-
}
172-
173-
_handleBlur() {
174-
this._renderer.removeClass(this._element.nativeElement, FOCUSED_STYLE);
159+
this.selectionList._setFocusedOption(this);
175160
}
176161

177162
/** Retrieves the DOM element of the component host. */
@@ -202,17 +187,11 @@ export class MatListOption extends _MatListOptionMixinBase
202187
changeDetection: ChangeDetectionStrategy.OnPush
203188
})
204189
export class MatSelectionList extends _MatSelectionListMixinBase
205-
implements FocusableOption, CanDisable, CanDisableRipple, AfterContentInit, OnDestroy {
190+
implements FocusableOption, CanDisable, CanDisableRipple, AfterContentInit {
206191

207192
/** Tab index for the selection-list. */
208193
_tabIndex = 0;
209194

210-
/** Subscription to all list options' onFocus events */
211-
private _optionFocusSubscription = Subscription.EMPTY;
212-
213-
/** Subscription to all list options' destroy events */
214-
private _optionDestroyStream = Subscription.EMPTY;
215-
216195
/** The FocusKeyManager which handles focus. */
217196
_keyManager: FocusKeyManager<MatListOption>;
218197

@@ -232,14 +211,6 @@ export class MatSelectionList extends _MatSelectionListMixinBase
232211
if (this.disabled) {
233212
this._tabIndex = -1;
234213
}
235-
236-
this._optionFocusSubscription = this._onFocusSubscription();
237-
this._optionDestroyStream = this._onDestroySubscription();
238-
}
239-
240-
ngOnDestroy(): void {
241-
this._optionDestroyStream.unsubscribe();
242-
this._optionFocusSubscription.unsubscribe();
243214
}
244215

245216
/** Focus the selection-list. */
@@ -265,36 +236,23 @@ export class MatSelectionList extends _MatSelectionListMixinBase
265236
});
266237
}
267238

268-
/** Map all the options' destroy event subscriptions and merge them into one stream. */
269-
private _onDestroySubscription(): Subscription {
270-
return RxChain.from(this.options.changes)
271-
.call(startWith, this.options)
272-
.call(switchMap, (options: MatListOption[]) => {
273-
return merge(...options.map(option => option.destroyed));
274-
}).subscribe((e: MatSelectionListOptionEvent) => {
275-
let optionIndex: number = this.options.toArray().indexOf(e.option);
276-
if (e.option._hasFocus) {
277-
// Check whether the option is the last item
278-
if (optionIndex < this.options.length - 1) {
279-
this._keyManager.setActiveItem(optionIndex);
280-
} else if (optionIndex - 1 >= 0) {
281-
this._keyManager.setActiveItem(optionIndex - 1);
282-
}
283-
}
284-
e.option.destroyed.unsubscribe();
285-
});
239+
/** Sets the focused option of the selection-list. */
240+
_setFocusedOption(option: MatListOption) {
241+
this._keyManager.updateActiveItemIndex(this._getOptionIndex(option));
286242
}
287243

288-
/** Map all the options' onFocus event subscriptions and merge them into one stream. */
289-
private _onFocusSubscription(): Subscription {
290-
return RxChain.from(this.options.changes)
291-
.call(startWith, this.options)
292-
.call(switchMap, (options: MatListOption[]) => {
293-
return merge(...options.map(option => option.onFocus));
294-
}).subscribe((e: MatSelectionListOptionEvent) => {
295-
let optionIndex: number = this.options.toArray().indexOf(e.option);
296-
this._keyManager.updateActiveItemIndex(optionIndex);
297-
});
244+
/** Removes an option from the selection list and updates the active item. */
245+
_removeOptionFromList(option: MatListOption) {
246+
if (option._hasFocus) {
247+
const optionIndex = this._getOptionIndex(option);
248+
249+
// Check whether the option is the last item
250+
if (optionIndex > 0) {
251+
this._keyManager.setPreviousItemActive();
252+
} else if (optionIndex === 0 && this.options.length > 1) {
253+
this._keyManager.setNextItemActive();
254+
}
255+
}
298256
}
299257

300258
/** Passes relevant key presses to our key manager. */
@@ -332,4 +290,9 @@ export class MatSelectionList extends _MatSelectionListMixinBase
332290
private _isValidIndex(index: number): boolean {
333291
return index >= 0 && index < this.options.length;
334292
}
293+
294+
/** Returns the index of the specified list option. */
295+
private _getOptionIndex(option: MatListOption): number {
296+
return this.options.toArray().indexOf(option);
297+
}
335298
}

0 commit comments

Comments
 (0)