Skip to content

Commit b4faf34

Browse files
committed
feat(cdk/a11y): add tests and fixes for expand/collapse interactions
1 parent 4cceae3 commit b4faf34

File tree

2 files changed

+300
-8
lines changed

2 files changed

+300
-8
lines changed

src/cdk/a11y/key-manager/tree-key-manager.spec.ts

Lines changed: 296 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ import {createKeyboardEvent} from '../../testing/private';
1414
import {QueryList} from '@angular/core';
1515
import {fakeAsync, tick} from '@angular/core/testing';
1616
import {take} from 'rxjs/operators';
17-
import {FocusOrigin} from '../focus-monitor/focus-monitor';
1817
import {TreeKeyManager, TreeKeyManagerItem} from './tree-key-manager';
19-
import {Observable, of as observableOf} from 'rxjs';
18+
import {Observable, of as observableOf, Subscription} from 'rxjs';
2019

2120
class FakeBaseTreeKeyManagerItem {
2221
public _isExpanded = false;
@@ -69,8 +68,9 @@ interface ItemConstructorTestContext {
6968
}
7069

7170
interface ExpandCollapseKeyEventTestContext {
72-
expandKeyEvent: KeyboardEvent;
73-
collapseKeyEvent: KeyboardEvent;
71+
direction: 'ltr' | 'rtl';
72+
expandKeyEvent: () => KeyboardEvent;
73+
collapseKeyEvent: () => KeyboardEvent;
7474
}
7575

7676
fdescribe('TreeKeyManager', () => {
@@ -445,6 +445,298 @@ fdescribe('TreeKeyManager', () => {
445445
expect(fakeKeyEvents.upArrow.defaultPrevented).toBe(true);
446446
});
447447
});
448+
449+
describe('expand/collapse key events', () => {
450+
const parameters: ExpandCollapseKeyEventTestContext[] = [
451+
{
452+
direction: 'ltr',
453+
expandKeyEvent: () => fakeKeyEvents.rightArrow,
454+
collapseKeyEvent: () => fakeKeyEvents.leftArrow,
455+
},
456+
{
457+
direction: 'rtl',
458+
expandKeyEvent: () => fakeKeyEvents.leftArrow,
459+
collapseKeyEvent: () => fakeKeyEvents.rightArrow,
460+
},
461+
];
462+
463+
for (const param of parameters) {
464+
describe(`in ${param.direction} mode`, () => {
465+
beforeEach(() => {
466+
keyManager = new TreeKeyManager({
467+
items: itemList,
468+
horizontalOrientation: param.direction,
469+
});
470+
for (const item of itemList) {
471+
item._isExpanded = false;
472+
}
473+
});
474+
475+
it('with nothing active, expand key does not expand any items', () => {
476+
expect(itemList.toArray().map(item => item.isExpanded()))
477+
.withContext('item expansion state, for all items')
478+
.toEqual(itemList.toArray().map(_ => false));
479+
480+
keyManager.onKeydown(param.expandKeyEvent());
481+
482+
expect(itemList.toArray().map(item => item.isExpanded()))
483+
.withContext('item expansion state, for all items, after expand event')
484+
.toEqual(itemList.toArray().map(_ => false));
485+
});
486+
487+
it('with nothing active, collapse key does not collapse any items', () => {
488+
for (const item of itemList) {
489+
item._isExpanded = true;
490+
}
491+
expect(itemList.toArray().map(item => item.isExpanded()))
492+
.withContext('item expansion state, for all items')
493+
.toEqual(itemList.toArray().map(_ => true));
494+
495+
keyManager.onKeydown(param.collapseKeyEvent());
496+
497+
expect(itemList.toArray().map(item => item.isExpanded()))
498+
.withContext('item expansion state, for all items')
499+
.toEqual(itemList.toArray().map(_ => true));
500+
});
501+
502+
it('with nothing active, expand key does not change the active item index', () => {
503+
expect(keyManager.getActiveItemIndex())
504+
.withContext('active item index, initial')
505+
.toEqual(-1);
506+
507+
keyManager.onKeydown(param.expandKeyEvent());
508+
509+
expect(keyManager.getActiveItemIndex())
510+
.withContext('active item index, after expand event')
511+
.toEqual(-1);
512+
});
513+
514+
it('with nothing active, collapse key does not change the active item index', () => {
515+
for (const item of itemList) {
516+
item._isExpanded = true;
517+
}
518+
519+
expect(keyManager.getActiveItemIndex())
520+
.withContext('active item index, initial')
521+
.toEqual(-1);
522+
523+
keyManager.onKeydown(param.collapseKeyEvent());
524+
525+
expect(keyManager.getActiveItemIndex())
526+
.withContext('active item index, after collapse event')
527+
.toEqual(-1);
528+
});
529+
530+
describe('if the current item is expanded', () => {
531+
let spy: jasmine.Spy;
532+
let subscription: Subscription;
533+
534+
beforeEach(() => {
535+
keyManager.onClick(parentItem);
536+
parentItem._isExpanded = true;
537+
538+
spy = jasmine.createSpy('change spy');
539+
subscription = keyManager.change.subscribe(spy);
540+
});
541+
542+
afterEach(() => {
543+
subscription.unsubscribe();
544+
});
545+
546+
it('when the expand key is pressed, moves to the first child', () => {
547+
keyManager.onKeydown(param.expandKeyEvent());
548+
549+
expect(keyManager.getActiveItemIndex())
550+
.withContext('active item index, after one expand key event.')
551+
.toBe(1);
552+
expect(spy).not.toHaveBeenCalledWith(parentItem);
553+
expect(spy).toHaveBeenCalledWith(childItem);
554+
});
555+
556+
it(
557+
'when the expand key is pressed, and the first child is disabled, ' +
558+
'moves to the first non-disabled child',
559+
() => {
560+
childItem.isDisabled = true;
561+
562+
keyManager.onKeydown(param.expandKeyEvent());
563+
564+
expect(keyManager.getActiveItemIndex())
565+
.withContext('active item index, after one expand key event.')
566+
.toBe(3);
567+
expect(spy).not.toHaveBeenCalledWith(parentItem);
568+
expect(spy).not.toHaveBeenCalledWith(childItem);
569+
expect(spy).toHaveBeenCalledWith(childItemWithNoChildren);
570+
},
571+
);
572+
573+
it(
574+
'when the expand key is pressed, and all children are disabled, ' +
575+
'does not change the active item',
576+
() => {
577+
childItem.isDisabled = true;
578+
childItemWithNoChildren.isDisabled = true;
579+
580+
keyManager.onKeydown(param.expandKeyEvent());
581+
582+
expect(keyManager.getActiveItemIndex())
583+
.withContext('active item index, after one expand key event.')
584+
.toBe(0);
585+
expect(spy).not.toHaveBeenCalled();
586+
},
587+
);
588+
589+
it('when the collapse key is pressed, collapses the item', () => {
590+
expect(parentItem.isExpanded())
591+
.withContext('active item initial expansion state')
592+
.toBe(true);
593+
594+
keyManager.onKeydown(param.collapseKeyEvent());
595+
596+
expect(parentItem.isExpanded())
597+
.withContext('active item expansion state, after collapse key')
598+
.toBe(false);
599+
});
600+
601+
it('when the collapse key is pressed, does not change the active item', () => {
602+
expect(keyManager.getActiveItemIndex())
603+
.withContext('active item index, initial')
604+
.toBe(0);
605+
606+
keyManager.onKeydown(param.collapseKeyEvent());
607+
608+
expect(keyManager.getActiveItemIndex())
609+
.withContext('active item index, after one collapse key event.')
610+
.toBe(0);
611+
expect(spy).not.toHaveBeenCalled();
612+
});
613+
});
614+
615+
describe('if the current item is expanded, and there are no children', () => {
616+
let spy: jasmine.Spy;
617+
let subscription: Subscription;
618+
619+
beforeEach(() => {
620+
keyManager.onClick(childItemWithNoChildren);
621+
childItemWithNoChildren._isExpanded = true;
622+
623+
spy = jasmine.createSpy('change spy');
624+
subscription = keyManager.change.subscribe(spy);
625+
});
626+
627+
afterEach(() => {
628+
subscription.unsubscribe();
629+
});
630+
631+
it('when the expand key is pressed, does not change the active item', () => {
632+
keyManager.onKeydown(param.expandKeyEvent());
633+
634+
expect(keyManager.getActiveItemIndex())
635+
.withContext('active item index, after one expand key event.')
636+
.toBe(3);
637+
expect(spy).not.toHaveBeenCalled();
638+
});
639+
});
640+
641+
describe('if the current item is collapsed, and has a parent item', () => {
642+
let spy: jasmine.Spy;
643+
let subscription: Subscription;
644+
645+
beforeEach(() => {
646+
keyManager.onClick(childItem);
647+
childItem._isExpanded = false;
648+
649+
spy = jasmine.createSpy('change spy');
650+
subscription = keyManager.change.subscribe(spy);
651+
});
652+
653+
afterEach(() => {
654+
subscription.unsubscribe();
655+
});
656+
657+
it('when the expand key is pressed, expands the current item', () => {
658+
expect(childItem.isExpanded())
659+
.withContext('active item initial expansion state')
660+
.toBe(false);
661+
662+
keyManager.onKeydown(param.expandKeyEvent());
663+
664+
expect(childItem.isExpanded())
665+
.withContext('active item expansion state, after expand key')
666+
.toBe(true);
667+
});
668+
669+
it('when the expand key is pressed, does not change active item', () => {
670+
expect(keyManager.getActiveItemIndex())
671+
.withContext('active item index, initial')
672+
.toBe(1);
673+
674+
keyManager.onKeydown(param.expandKeyEvent());
675+
676+
expect(keyManager.getActiveItemIndex())
677+
.withContext('active item index, after one collapse key event.')
678+
.toBe(1);
679+
expect(spy).not.toHaveBeenCalled();
680+
});
681+
682+
it('when the collapse key is pressed, moves the active item to the parent', () => {
683+
expect(keyManager.getActiveItemIndex())
684+
.withContext('active item index, initial')
685+
.toBe(1);
686+
687+
keyManager.onKeydown(param.collapseKeyEvent());
688+
689+
expect(keyManager.getActiveItemIndex())
690+
.withContext('active item index, after one collapse key event.')
691+
.toBe(0);
692+
});
693+
694+
it('when the collapse key is pressed, and the parent is disabled, does nothing', () => {
695+
expect(keyManager.getActiveItemIndex())
696+
.withContext('active item index, initial')
697+
.toBe(1);
698+
699+
parentItem.isDisabled = true;
700+
keyManager.onKeydown(param.collapseKeyEvent());
701+
702+
expect(keyManager.getActiveItemIndex())
703+
.withContext('active item index, after one collapse key event.')
704+
.toBe(1);
705+
});
706+
});
707+
708+
describe('if the current item is collapsed, and has no parent items', () => {
709+
let spy: jasmine.Spy;
710+
let subscription: Subscription;
711+
712+
beforeEach(() => {
713+
keyManager.onClick(parentItem);
714+
parentItem._isExpanded = false;
715+
716+
spy = jasmine.createSpy('change spy');
717+
subscription = keyManager.change.subscribe(spy);
718+
});
719+
720+
afterEach(() => {
721+
subscription.unsubscribe();
722+
});
723+
724+
it('when the collapse key is pressed, does nothing', () => {
725+
expect(keyManager.getActiveItemIndex())
726+
.withContext('active item index, initial')
727+
.toBe(0);
728+
729+
keyManager.onKeydown(param.collapseKeyEvent());
730+
731+
expect(keyManager.getActiveItemIndex())
732+
.withContext('active item index, after one collapse key event.')
733+
.toBe(0);
734+
expect(spy).not.toHaveBeenCalledWith(parentItem);
735+
});
736+
});
737+
});
738+
}
739+
});
448740
});
449741
}
450742

src/cdk/a11y/key-manager/tree-key-manager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,11 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
329329
return;
330330
}
331331

332-
if (!this._isCurrentItemExpanded()) {
332+
if (this._isCurrentItemExpanded()) {
333333
this._activeItem.collapse();
334334
} else {
335335
const parent = this._activeItem.getParent();
336-
if (!parent) {
336+
if (!parent || this._isItemDisabled(parent)) {
337337
return;
338338
}
339339
this._setActiveItem(parent as T);
@@ -354,7 +354,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
354354
coerceObservable(this._activeItem.getChildren())
355355
.pipe(take(1))
356356
.subscribe(children => {
357-
const firstChild = children[0];
357+
const firstChild = children.find(child => !this._isItemDisabled(child));
358358
if (!firstChild) {
359359
return;
360360
}
@@ -372,7 +372,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
372372
: this._activeItem.isExpanded();
373373
}
374374

375-
private _isItemDisabled(item: T) {
375+
private _isItemDisabled(item: TreeKeyManagerItem) {
376376
return typeof item.isDisabled === 'boolean' ? item.isDisabled : item.isDisabled?.();
377377
}
378378

0 commit comments

Comments
 (0)