Skip to content

Commit 358712e

Browse files
committed
refactor(aria/ui-patterns): allow non-selectable items
1 parent 7cf8f5f commit 358712e

File tree

13 files changed

+347
-38
lines changed

13 files changed

+347
-38
lines changed

src/aria/tree/tree.spec.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ describe('Tree', () => {
114114
if (config.label !== undefined) node.label = config.label;
115115
if (config.children !== undefined) node.children = config.children;
116116
if (config.disabled !== undefined) node.disabled = config.disabled;
117+
if (config.selectable !== undefined) node.selectable = config.selectable;
117118
updateTree({nodes: newNodes});
118119
return;
119120
}
@@ -309,6 +310,16 @@ describe('Tree', () => {
309310
expect(appleItem.getAttribute('aria-current')).toBe('location');
310311
});
311312

313+
it('should not set aria-current when not selectable', () => {
314+
expandAll();
315+
updateTree({nav: true, value: ['apple']});
316+
const appleItem = getTreeItemElementByValue('apple')!;
317+
expect(appleItem.getAttribute('aria-current')).toBe('page');
318+
319+
updateTreeItemByValue('apple', {selectable: false});
320+
expect(appleItem.hasAttribute('aria-current')).toBe(false);
321+
});
322+
312323
it('should not set aria-selected when nav="true"', () => {
313324
expandAll();
314325

@@ -319,6 +330,16 @@ describe('Tree', () => {
319330
updateTree({nav: false});
320331
expect(appleItem.getAttribute('aria-selected')).toBe('true');
321332
});
333+
334+
it('should not set aria-selected when not selectable', () => {
335+
expandAll();
336+
updateTree({value: ['apple']});
337+
const appleItem = getTreeItemElementByValue('apple')!;
338+
expect(appleItem.getAttribute('aria-selected')).toBe('true');
339+
340+
updateTreeItemByValue('apple', {selectable: false});
341+
expect(appleItem.hasAttribute('aria-selected')).toBe(false);
342+
});
322343
});
323344

324345
describe('roving focus mode (focusMode="roving")', () => {
@@ -492,6 +513,18 @@ describe('Tree', () => {
492513
click(appleEl);
493514
expect(treeInstance.value()).toEqual(['banana']);
494515
});
516+
517+
describe('selectable=false', () => {
518+
it('should not select an item on click', () => {
519+
updateTree({value: ['banana']});
520+
updateTreeItemByValue('apple', {selectable: false});
521+
const appleEl = getTreeItemElementByValue('apple')!;
522+
523+
click(appleEl);
524+
expect(treeInstance.value()).not.toContain('apple');
525+
expect(treeInstance.value()).toContain('banana');
526+
});
527+
});
495528
});
496529

497530
describe('selectionMode="follow"', () => {
@@ -560,6 +593,39 @@ describe('Tree', () => {
560593
'broccoli',
561594
]);
562595
});
596+
597+
describe('selectable=false', () => {
598+
it('should not select a range with shift+click if an item is not selectable', () => {
599+
updateTreeItemByValue('banana', {selectable: false});
600+
const appleEl = getTreeItemElementByValue('apple')!;
601+
const berriesEl = getTreeItemElementByValue('berries')!;
602+
603+
click(appleEl);
604+
shiftClick(berriesEl);
605+
606+
expect(treeInstance.value()).not.toContain('banana');
607+
expect(treeInstance.value()).toContain('apple');
608+
expect(treeInstance.value()).toContain('berries');
609+
});
610+
611+
it('should not toggle selection of an item on simple click', () => {
612+
updateTreeItemByValue('apple', {selectable: false});
613+
const appleEl = getTreeItemElementByValue('apple')!;
614+
615+
click(appleEl);
616+
expect(treeInstance.value()).not.toContain('apple');
617+
});
618+
619+
it('should not add to selection with ctrl+click', () => {
620+
updateTree({value: ['banana']});
621+
updateTreeItemByValue('apple', {selectable: false});
622+
const appleEl = getTreeItemElementByValue('apple')!;
623+
624+
ctrlClick(appleEl);
625+
expect(treeInstance.value()).not.toContain('apple');
626+
expect(treeInstance.value()).toContain('banana');
627+
});
628+
});
563629
});
564630
});
565631
});
@@ -607,6 +673,20 @@ describe('Tree', () => {
607673
enter();
608674
expect(treeInstance.value()).toEqual(['grains']);
609675
});
676+
677+
describe('selectable=false', () => {
678+
it('should not select the focused item with Enter', () => {
679+
updateTreeItemByValue('fruits', {selectable: false});
680+
enter();
681+
expect(treeInstance.value()).toEqual([]);
682+
});
683+
684+
it('should not select the focused item with Space', () => {
685+
updateTreeItemByValue('fruits', {selectable: false});
686+
space();
687+
expect(treeInstance.value()).toEqual([]);
688+
});
689+
});
610690
});
611691

612692
describe('selectionMode="follow"', () => {
@@ -737,6 +817,35 @@ describe('Tree', () => {
737817
up({ctrlKey: true});
738818
expect(treeInstance.value()).toEqual(['fruits']);
739819
});
820+
821+
describe('selectable=false', () => {
822+
it('should not toggle selection of the focused item with Space', () => {
823+
updateTreeItemByValue('fruits', {selectable: false});
824+
space();
825+
expect(treeInstance.value()).toEqual([]);
826+
});
827+
828+
it('should not extend selection with Shift+ArrowDown', () => {
829+
updateTreeItemByValue('vegetables', {selectable: false});
830+
shift();
831+
down({shiftKey: true});
832+
down({shiftKey: true});
833+
expect(treeInstance.value()).not.toContain('vegetables');
834+
expect(treeInstance.value().sort()).toEqual(['fruits', 'grains']);
835+
});
836+
837+
it('Ctrl+A should not select non-selectable items', () => {
838+
expandAll();
839+
updateTreeItemByValue('apple', {selectable: false});
840+
updateTreeItemByValue('carrot', {selectable: false});
841+
keydown('A', {ctrlKey: true});
842+
const value = treeInstance.value();
843+
expect(value).not.toContain('apple');
844+
expect(value).not.toContain('carrot');
845+
expect(value).toContain('banana');
846+
expect(value).toContain('broccoli');
847+
});
848+
});
740849
});
741850

742851
describe('selectionMode="follow"', () => {
@@ -864,6 +973,37 @@ describe('Tree', () => {
864973
expect(getFocusedTreeItemValue()).toBe('vegetables');
865974
});
866975

976+
describe('selectable=false', () => {
977+
it('should not select an item on ArrowDown', () => {
978+
updateTreeItemByValue('vegetables', {selectable: false});
979+
down();
980+
expect(treeInstance.value()).not.toContain('vegetables');
981+
expect(treeInstance.value()).toEqual([]);
982+
});
983+
984+
it('should not toggle selection of the focused item on Ctrl+Space', () => {
985+
updateTreeItemByValue('fruits', {selectable: false});
986+
space({ctrlKey: true});
987+
expect(treeInstance.value()).toEqual([]);
988+
});
989+
990+
it('should not extend selection with Shift+ArrowDown', () => {
991+
updateTreeItemByValue('vegetables', {selectable: false});
992+
shift();
993+
down({shiftKey: true});
994+
down({shiftKey: true});
995+
expect(treeInstance.value()).not.toContain('vegetables');
996+
expect(treeInstance.value().sort()).toEqual(['fruits', 'grains']);
997+
});
998+
999+
it('typeahead should not select the focused item', () => {
1000+
updateTreeItemByValue('vegetables', {selectable: false});
1001+
type('v');
1002+
expect(getFocusedTreeItemValue()).toBe('vegetables');
1003+
expect(treeInstance.value()).not.toContain('vegetables');
1004+
});
1005+
});
1006+
8671007
it('should not select disabled items during Shift+ArrowKey navigation even if skipDisabled is false', () => {
8681008
right(); // Expands fruits
8691009
updateTreeItemByValue('banana', {disabled: true});
@@ -1302,6 +1442,7 @@ interface TestTreeNode<V = string> {
13021442
value: V;
13031443
label: string;
13041444
disabled?: boolean;
1445+
selectable?: boolean;
13051446
children?: TestTreeNode<V>[];
13061447
}
13071448

@@ -1332,6 +1473,7 @@ interface TestTreeNode<V = string> {
13321473
[value]="node.value"
13331474
[label]="node.label"
13341475
[disabled]="!!node.disabled"
1476+
[selectable]="node.selectable ?? true"
13351477
[parent]="parent"
13361478
[attr.data-value]="node.value"
13371479
#treeItem="ngTreeItem"

src/aria/tree/tree.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ export class TreeItem<V> extends DeferredContentAware implements OnInit, OnDestr
245245
/** Whether the tree item is disabled. */
246246
readonly disabled = input(false, {transform: booleanAttribute});
247247

248+
/** Whether the tree item is selectable. */
249+
readonly selectable = input<boolean>(true);
250+
248251
/** Optional label for typeahead. Defaults to the element's textContent. */
249252
readonly label = input<string>();
250253

0 commit comments

Comments
 (0)