Skip to content

Commit

Permalink
fix(cdk/tree): add injectable key manager and opt-out
Browse files Browse the repository at this point in the history
Make backwards compatibility improvements to cdk-tree-revamp regarding
focus management, the key manager and tabindex attribute.

 * Add TreeKeyMangerStrategy interface
 * Add injection toekn for tree key manager
 * Add LegacyTreeKeyManager

Provide LegacyTreeKeyManager to use legacy tabindex behavior from before
TreeKeyManager was introducted.

This commit message will be squashed away.
  • Loading branch information
zarend committed Oct 26, 2023
1 parent 732665d commit 7f74be0
Show file tree
Hide file tree
Showing 19 changed files with 943 additions and 205 deletions.
87 changes: 87 additions & 0 deletions src/cdk/a11y/key-manager/legacy-tree-key-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {Subject} from 'rxjs';
import {
TREE_KEY_MANAGER,
TreeKeyManagerFactory,
TreeKeyManagerItem,
TreeKeyManagerStrategy,
} from './tree-key-manager';

/**
* @docs-private
*
* @deprecated LegacyTreeKeyManager deprecated. Use TreeKeyManager or inject a
* TreeKeyManagerStrategy instead. To be removed in a future version.
*
* @breaking-change 19.0.0
*/
// LegacyTreeKeyManager is a "noop" implementation of TreeKeyMangerStrategy. Methods are noops. Does
// not emit to streams.
//
// Used for applications built before TreeKeyManager to opt-out of TreeKeyManager and revert to
// legacy behavior.
export class LegacyTreeKeyManager<T extends TreeKeyManagerItem>
implements TreeKeyManagerStrategy<T>
{
get _isLegacyTreeKeyManager() {
return true;
}

// Provide change as required by TreeKeyManagerStrategy. LegacyTreeKeyManager is a "noop"
// implementation that does not emit to streams.
readonly change = new Subject<T | null>();

onKeydown() {
// noop
}

getActiveItemIndex() {
// Always return null. LegacyTreeKeyManager is a "noop" implementation that does not maintain
// the active item.
return null;
}

getActiveItem() {
// Always return null. LegacyTreeKeyManager is a "noop" implementation that does not maintain
// the active item.
return null;
}

onInitialFocus() {
// noop
}

focusItem() {
// noop
}

setActiveItem() {
// noop
}
}

/**
* @docs-private
*
* @deprecated LegacyTreeKeyManager deprecated. Use TreeKeyManager or inject a
* TreeKeyManagerStrategy instead. To be removed in a future version.
*
* @breaking-change 19.0.0
*/
export function LEGACY_TREE_KEY_MANAGER_FACTORY<
T extends TreeKeyManagerItem,
>(): TreeKeyManagerFactory<T> {
return () => new LegacyTreeKeyManager<T>();
}

/**
* @docs-private
*
* @deprecated LegacyTreeKeyManager deprecated. Use TreeKeyManager or inject a
* TreeKeyManagerStrategy instead. To be removed in a future version.
*
* @breaking-change 19.0.0
*/
export const LEGACY_TREE_KEY_MANAGER_FACTORY_PROVIDER = {
provide: TREE_KEY_MANAGER,
useFactory: LEGACY_TREE_KEY_MANAGER_FACTORY,
};
126 changes: 6 additions & 120 deletions src/cdk/a11y/key-manager/tree-key-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,7 @@ describe('TreeKeyManager', () => {
]);
keyManager = new TreeKeyManager<
FakeObservableTreeKeyManagerItem | FakeArrayTreeKeyManagerItem
>({
items: itemList,
});
>(itemList, {});
});

it('should start off the activeItem as null', () => {
Expand All @@ -180,24 +178,6 @@ describe('TreeKeyManager', () => {
});

describe('Key events', () => {
it('should emit tabOut when tab key is pressed', () => {
const spy = jasmine.createSpy('tabOut spy');
keyManager.tabOut.pipe(take(1)).subscribe(spy);
keyManager.onKeydown(fakeKeyEvents.tab);

expect(spy).toHaveBeenCalled();
});

it('should emit tabOut when the tab key is pressed with a modifier', () => {
const spy = jasmine.createSpy('tabOut spy');
keyManager.tabOut.pipe(take(1)).subscribe(spy);

Object.defineProperty(fakeKeyEvents.tab, 'shiftKey', {get: () => true});
keyManager.onKeydown(fakeKeyEvents.tab);

expect(spy).toHaveBeenCalled();
});

it('should emit an event whenever the active item changes', () => {
keyManager.setActiveItem(itemList.get(0)!);

Expand Down Expand Up @@ -486,8 +466,7 @@ describe('TreeKeyManager', () => {
for (const param of parameters) {
describe(`in ${param.direction} mode`, () => {
beforeEach(() => {
keyManager = new TreeKeyManager({
items: itemList,
keyManager = new TreeKeyManager(itemList, {
horizontalOrientation: param.direction,
});
for (const item of itemList) {
Expand Down Expand Up @@ -765,8 +744,7 @@ describe('TreeKeyManager', () => {
const debounceInterval = 300;

beforeEach(() => {
keyManager = new TreeKeyManager({
items: itemList,
keyManager = new TreeKeyManager(itemList, {
typeAheadDebounceInterval: debounceInterval,
});
});
Expand All @@ -777,8 +755,7 @@ describe('TreeKeyManager', () => {

expect(
() =>
new TreeKeyManager({
items: invalidQueryList,
new TreeKeyManager(invalidQueryList, {
typeAheadDebounceInterval: true,
}),
).toThrowError(/must implement/);
Expand Down Expand Up @@ -810,8 +787,7 @@ describe('TreeKeyManager', () => {

it('uses a default debounce interval', fakeAsync(() => {
const defaultInterval = 200;
keyManager = new TreeKeyManager({
items: itemList,
keyManager = new TreeKeyManager(itemList, {
typeAheadDebounceInterval: true,
});

Expand Down Expand Up @@ -1018,99 +994,9 @@ describe('TreeKeyManager', () => {
});
});

describe('focusFirstItem', () => {
beforeEach(() => {
keyManager.onInitialFocus();
});

it('should focus the first item', () => {
keyManager.onKeydown(fakeKeyEvents.downArrow);
keyManager.onKeydown(fakeKeyEvents.downArrow);
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(2);

keyManager.focusFirstItem();
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0);
});

it('should set the active item to the second item if the first one is disabled', () => {
itemList.get(0)!.isDisabled = true;

keyManager.focusFirstItem();
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(1);
});
});

describe('focusLastItem', () => {
beforeEach(() => {
keyManager.onInitialFocus();
});

it('should focus the last item', () => {
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0);

keyManager.focusLastItem();
expect(keyManager.getActiveItemIndex())
.withContext('active item index')
.toBe(itemList.length - 1);
});

it('should set the active item to the second-to-last item if the last is disabled', () => {
itemList.get(itemList.length - 1)!.isDisabled = true;

keyManager.focusLastItem();
expect(keyManager.getActiveItemIndex())
.withContext('active item index')
.toBe(itemList.length - 2);
});
});

describe('focusNextItem', () => {
beforeEach(() => {
keyManager.onInitialFocus();
});

it('should focus the next item', () => {
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0);

keyManager.focusNextItem();
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(1);
});

it('should skip disabled items', () => {
itemList.get(1)!.isDisabled = true;

keyManager.focusNextItem();
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(2);
});
});

describe('focusPreviousItem', () => {
beforeEach(() => {
keyManager.onInitialFocus();
});

it('should focus the previous item', () => {
keyManager.onKeydown(fakeKeyEvents.downArrow);
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(1);

keyManager.focusPreviousItem();
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0);
});

it('should skip disabled items', () => {
itemList.get(1)!.isDisabled = true;
keyManager.onKeydown(fakeKeyEvents.downArrow);
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(2);

keyManager.focusPreviousItem();
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0);
});
});

describe('skip predicate', () => {
beforeEach(() => {
keyManager = new TreeKeyManager({
items: itemList,
keyManager = new TreeKeyManager(itemList, {
skipPredicate: item => item.skipItem ?? false,
});
keyManager.onInitialFocus();
Expand Down
Loading

0 comments on commit 7f74be0

Please sign in to comment.