Skip to content

Commit ef8a166

Browse files
authored
feat(cdk/tree): implement expansion methods for TreeKeyManager (#26586)
* feat(cdk/a11y): implement expansion methods * feat(cdk/a11y): add tests and fixes for expand/collapse interactions * feat(cdk/a11y): actually fix build * feat(cdk/a11y): use skipPredicate instead of disabled
1 parent 4498435 commit ef8a166

File tree

2 files changed

+375
-7
lines changed

2 files changed

+375
-7
lines changed

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

Lines changed: 299 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {createKeyboardEvent} from '../../testing/private';
1414
import {QueryList} from '@angular/core';
1515
import {take} from 'rxjs/operators';
1616
import {TreeKeyManager, TreeKeyManagerItem} from './tree-key-manager';
17-
import {Observable, of as observableOf} from 'rxjs';
17+
import {Observable, of as observableOf, Subscription} from 'rxjs';
1818

1919
class FakeBaseTreeKeyManagerItem {
2020
_isExpanded = false;
@@ -66,6 +66,12 @@ interface ItemConstructorTestContext {
6666
| FakeObservableTreeKeyManagerItem;
6767
}
6868

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

0 commit comments

Comments
 (0)