Skip to content

Commit e05f939

Browse files
devversionandrewseguin
authored andcommitted
fix(selection-list): restore focus if active item is destroyed (#7125)
* 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 26788f1 commit e05f939

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);
@@ -432,11 +441,12 @@ describe('MatSelectionList', () => {
432441
<mat-list-option checkboxPosition="before" value="sent-mail">
433442
Sent Mail
434443
</mat-list-option>
435-
<mat-list-option checkboxPosition="before" value="drafts">
444+
<mat-list-option checkboxPosition="before" value="drafts" *ngIf="showLastOption">
436445
Drafts
437446
</mat-list-option>
438447
</mat-selection-list>`})
439448
class SelectionListWithListOptions {
449+
showLastOption: boolean = true;
440450
}
441451

442452
@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,
@@ -38,8 +37,6 @@ import {
3837
mixinDisabled,
3938
mixinDisableRipple,
4039
} from '@angular/material/core';
41-
import {merge} from 'rxjs/observable/merge';
42-
import {Subscription} from 'rxjs/Subscription';
4340

4441

4542
/** @docs-private */
@@ -55,8 +52,6 @@ export interface MatSelectionListOptionEvent {
5552
option: MatListOption;
5653
}
5754

58-
const FOCUSED_STYLE: string = 'mat-list-item-focus';
59-
6055
/**
6156
* Component for list-options of selection-list. Each list-option can automatically
6257
* generate a checkbox and can put current item into the selectionModel of selection-list
@@ -70,10 +65,11 @@ const FOCUSED_STYLE: string = 'mat-list-item-focus';
7065
'role': 'option',
7166
'class': 'mat-list-item mat-list-option',
7267
'(focus)': '_handleFocus()',
73-
'(blur)': '_handleBlur()',
68+
'(blur)': '_hasFocus = false',
7469
'(click)': '_handleClick()',
7570
'tabindex': '-1',
7671
'[class.mat-list-item-disabled]': 'disabled',
72+
'[class.mat-list-item-focus]': '_hasFocus',
7773
'[attr.aria-selected]': 'selected.toString()',
7874
'[attr.aria-disabled]': 'disabled.toString()',
7975
},
@@ -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,
@@ -144,7 +134,7 @@ export class MatListOption extends _MatListOptionMixinBase
144134
}
145135

146136
ngOnDestroy(): void {
147-
this.destroyed.emit({option: this});
137+
this.selectionList._removeOptionFromList(this);
148138
}
149139

150140
/** Toggles the selection state of the option. */
@@ -157,7 +147,6 @@ export class MatListOption extends _MatListOptionMixinBase
157147
/** Allows for programmatic focusing of the option. */
158148
focus(): void {
159149
this._element.nativeElement.focus();
160-
this.onFocus.emit({option: this});
161150
}
162151

163152
/** Whether this list item should show a ripple effect when clicked. */
@@ -173,11 +162,7 @@ export class MatListOption extends _MatListOptionMixinBase
173162

174163
_handleFocus() {
175164
this._hasFocus = true;
176-
this._renderer.addClass(this._element.nativeElement, FOCUSED_STYLE);
177-
}
178-
179-
_handleBlur() {
180-
this._renderer.removeClass(this._element.nativeElement, FOCUSED_STYLE);
165+
this.selectionList._setFocusedOption(this);
181166
}
182167

183168
/** Retrieves the DOM element of the component host. */
@@ -208,17 +193,11 @@ export class MatListOption extends _MatListOptionMixinBase
208193
changeDetection: ChangeDetectionStrategy.OnPush
209194
})
210195
export class MatSelectionList extends _MatSelectionListMixinBase
211-
implements FocusableOption, CanDisable, CanDisableRipple, AfterContentInit, OnDestroy {
196+
implements FocusableOption, CanDisable, CanDisableRipple, AfterContentInit {
212197

213198
/** Tab index for the selection-list. */
214199
_tabIndex = 0;
215200

216-
/** Subscription to all list options' onFocus events */
217-
private _optionFocusSubscription = Subscription.EMPTY;
218-
219-
/** Subscription to all list options' destroy events */
220-
private _optionDestroyStream = Subscription.EMPTY;
221-
222201
/** The FocusKeyManager which handles focus. */
223202
_keyManager: FocusKeyManager<MatListOption>;
224203

@@ -238,14 +217,6 @@ export class MatSelectionList extends _MatSelectionListMixinBase
238217
if (this.disabled) {
239218
this._tabIndex = -1;
240219
}
241-
242-
this._optionFocusSubscription = this._onFocusSubscription();
243-
this._optionDestroyStream = this._onDestroySubscription();
244-
}
245-
246-
ngOnDestroy(): void {
247-
this._optionDestroyStream.unsubscribe();
248-
this._optionFocusSubscription.unsubscribe();
249220
}
250221

251222
/** Focus the selection-list. */
@@ -271,36 +242,23 @@ export class MatSelectionList extends _MatSelectionListMixinBase
271242
});
272243
}
273244

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

294-
/** Map all the options' onFocus event subscriptions and merge them into one stream. */
295-
private _onFocusSubscription(): Subscription {
296-
return RxChain.from(this.options.changes)
297-
.call(startWith, this.options)
298-
.call(switchMap, (options: MatListOption[]) => {
299-
return merge(...options.map(option => option.onFocus));
300-
}).subscribe((e: MatSelectionListOptionEvent) => {
301-
let optionIndex: number = this.options.toArray().indexOf(e.option);
302-
this._keyManager.updateActiveItemIndex(optionIndex);
303-
});
250+
/** Removes an option from the selection list and updates the active item. */
251+
_removeOptionFromList(option: MatListOption) {
252+
if (option._hasFocus) {
253+
const optionIndex = this._getOptionIndex(option);
254+
255+
// Check whether the option is the last item
256+
if (optionIndex > 0) {
257+
this._keyManager.setPreviousItemActive();
258+
} else if (optionIndex === 0 && this.options.length > 1) {
259+
this._keyManager.setNextItemActive();
260+
}
261+
}
304262
}
305263

306264
/** Passes relevant key presses to our key manager. */
@@ -338,4 +296,9 @@ export class MatSelectionList extends _MatSelectionListMixinBase
338296
private _isValidIndex(index: number): boolean {
339297
return index >= 0 && index < this.options.length;
340298
}
299+
300+
/** Returns the index of the specified list option. */
301+
private _getOptionIndex(option: MatListOption): number {
302+
return this.options.toArray().indexOf(option);
303+
}
341304
}

0 commit comments

Comments
 (0)