Skip to content

Commit 905f27f

Browse files
committed
fixup! fix(cdk-experimental/listbox): change shift+nav behavior
1 parent d159f66 commit 905f27f

File tree

4 files changed

+222
-48
lines changed

4 files changed

+222
-48
lines changed

src/cdk-experimental/ui-patterns/behaviors/list-selection/list-selection.spec.ts

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,36 @@ describe('List Selection', () => {
157157
});
158158
});
159159

160+
describe('#toggleOne', () => {
161+
it('should select an unselected item', () => {
162+
const items = getItems([0, 1, 2, 3, 4]);
163+
const nav = getNavigation(items);
164+
const selection = getSelection(items, nav);
165+
166+
selection.toggleOne(); // [0]
167+
expect(selection.inputs.value()).toEqual([0]);
168+
});
169+
170+
it('should deselect a selected item', () => {
171+
const items = getItems([0, 1, 2, 3, 4]);
172+
const nav = getNavigation(items);
173+
const selection = getSelection(items, nav);
174+
selection.select(); // [0]
175+
selection.toggleOne(); // []
176+
expect(selection.inputs.value().length).toBe(0);
177+
});
178+
179+
it('should only leave one item selected', () => {
180+
const items = getItems([0, 1, 2, 3, 4]);
181+
const nav = getNavigation(items);
182+
const selection = getSelection(items, nav);
183+
selection.select(); // [0]
184+
nav.next();
185+
selection.toggleOne(); // [1]
186+
expect(selection.inputs.value()).toEqual([1]);
187+
});
188+
});
189+
160190
describe('#selectAll', () => {
161191
it('should select all items', () => {
162192
const items = getItems([0, 1, 2, 3, 4]);
@@ -185,7 +215,60 @@ describe('List Selection', () => {
185215
});
186216
});
187217

188-
describe('#selectFromAnchor', () => {
218+
describe('#toggleAll', () => {
219+
it('should select all items', () => {
220+
const items = getItems([0, 1, 2, 3, 4]);
221+
const nav = getNavigation(items);
222+
const selection = getSelection(items, nav);
223+
selection.toggleAll();
224+
expect(selection.inputs.value()).toEqual([0, 1, 2, 3, 4]);
225+
});
226+
227+
it('should deselect all if all items are selected', () => {
228+
const items = getItems([0, 1, 2, 3, 4]);
229+
const nav = getNavigation(items);
230+
const selection = getSelection(items, nav);
231+
selection.selectAll();
232+
selection.toggleAll();
233+
expect(selection.inputs.value()).toEqual([]);
234+
});
235+
});
236+
237+
describe('#selectOne', () => {
238+
it('should select a single item', () => {
239+
const items = getItems([0, 1, 2, 3, 4]);
240+
const nav = getNavigation(items);
241+
const selection = getSelection(items, nav);
242+
243+
selection.selectOne(); // [0]
244+
nav.next();
245+
selection.selectOne(); // [1]
246+
expect(selection.inputs.value()).toEqual([1]);
247+
});
248+
249+
it('should not select disabled items', () => {
250+
const items = getItems([0, 1, 2, 3, 4]);
251+
const nav = getNavigation(items);
252+
const selection = getSelection(items, nav);
253+
items()[0].disabled.set(true);
254+
255+
selection.select(); // []
256+
expect(selection.inputs.value()).toEqual([]);
257+
});
258+
259+
it('should do nothing to already selected items', () => {
260+
const items = getItems([0, 1, 2, 3, 4]);
261+
const nav = getNavigation(items);
262+
const selection = getSelection(items, nav);
263+
264+
selection.selectOne(); // [0]
265+
selection.selectOne(); // [0]
266+
267+
expect(selection.inputs.value()).toEqual([0]);
268+
});
269+
});
270+
271+
describe('#selectRange', () => {
189272
it('should select all items from an anchor at a lower index', () => {
190273
const items = getItems([0, 1, 2, 3, 4]);
191274
const nav = getNavigation(items);
@@ -194,7 +277,7 @@ describe('List Selection', () => {
194277
selection.select(); // [0]
195278
nav.next();
196279
nav.next();
197-
selection.selectFromPrevSelectedItem(); // [0, 1, 2]
280+
selection.selectRange(); // [0, 1, 2]
198281

199282
expect(selection.inputs.value()).toEqual([0, 1, 2]);
200283
});
@@ -209,10 +292,46 @@ describe('List Selection', () => {
209292
selection.select(); // [3]
210293
nav.prev();
211294
nav.prev();
212-
selection.selectFromPrevSelectedItem(); // [3, 1, 2]
295+
selection.selectRange(); // [3, 2, 1]
296+
297+
expect(selection.inputs.value()).toEqual([3, 2, 1]);
298+
});
299+
300+
it('should deselect items within the range when the range is changed', () => {
301+
const items = getItems([0, 1, 2, 3, 4]);
302+
const nav = getNavigation(items);
303+
const selection = getSelection(items, nav);
304+
305+
nav.next();
306+
nav.next();
307+
selection.select(); // [2]
308+
expect(selection.inputs.value()).toEqual([2]);
309+
310+
nav.next();
311+
nav.next();
312+
selection.selectRange(); // [2, 3, 4]
313+
expect(selection.inputs.value()).toEqual([2, 3, 4]);
213314

214-
// TODO(wagnermaciel): Order the values when inserting them.
215-
expect(selection.inputs.value()).toEqual([3, 1, 2]);
315+
nav.first();
316+
selection.selectRange(); // [2, 1, 0]
317+
expect(selection.inputs.value()).toEqual([2, 1, 0]);
318+
});
319+
});
320+
321+
describe('#beginRangeSelection', () => {
322+
it('should set where a range is starting from', () => {
323+
const items = getItems([0, 1, 2, 3, 4]);
324+
const nav = getNavigation(items);
325+
const selection = getSelection(items, nav);
326+
327+
nav.next();
328+
nav.next();
329+
selection.beginRangeSelection();
330+
expect(selection.inputs.value()).toEqual([]);
331+
nav.next();
332+
nav.next();
333+
selection.selectRange(); // [2, 3, 4]
334+
expect(selection.inputs.value()).toEqual([2, 3, 4]);
216335
});
217336
});
218337
});

src/cdk-experimental/ui-patterns/behaviors/list-selection/list-selection.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ export class ListSelection<T extends ListSelectionItem<V>, V> {
132132
* selected range that are now outside of the selected range
133133
*/
134134
selectRange() {
135-
const itemsInRange = this._getItemsFromActive(this.rangeStartIndex());
136-
const itemsOutOfRange = this._getItemsFromActive(this.rangeEndIndex()).filter(
135+
const itemsInRange = this._getItemsFromIndex(this.rangeStartIndex());
136+
const itemsOutOfRange = this._getItemsFromIndex(this.rangeEndIndex()).filter(
137137
i => !itemsInRange.includes(i),
138138
);
139139

@@ -152,14 +152,14 @@ export class ListSelection<T extends ListSelectionItem<V>, V> {
152152
}
153153
}
154154

155-
/** Sets the anchor to the current active index. */
155+
/** Marks the given index as the start of a range selection. */
156156
beginRangeSelection(index: number = this.navigation.inputs.activeIndex()) {
157157
this.rangeStartIndex.set(index);
158158
this.rangeEndIndex.set(index);
159-
console.log('range start:', index);
160159
}
161160

162-
private _getItemsFromActive(index: number) {
161+
/** Returns the items in the list starting from the given index. */
162+
private _getItemsFromIndex(index: number) {
163163
if (index === -1) {
164164
return [];
165165
}

src/cdk-experimental/ui-patterns/listbox/listbox.spec.ts

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ describe('Listbox Pattern', () => {
301301
expect(listbox.inputs.value()).toEqual(['Apple']);
302302
});
303303

304-
it('should select contiguous items from the most recently selected item to the focused item on Shift + Space (or Enter)', () => {
304+
it('should select a range of options on Shift + Space (or Enter)', () => {
305305
listbox.onKeydown(down());
306306
listbox.onKeydown(space()); // Apricot
307307
listbox.onKeydown(down());
@@ -310,12 +310,31 @@ describe('Listbox Pattern', () => {
310310
expect(listbox.inputs.value()).toEqual(['Apricot', 'Banana', 'Blackberry']);
311311
});
312312

313+
it('should deselect options outside the range on subsequent on Shift + Space (or Enter)', () => {
314+
listbox.onKeydown(down());
315+
listbox.onKeydown(down());
316+
listbox.onKeydown(space());
317+
expect(listbox.inputs.value()).toEqual(['Banana']);
318+
319+
listbox.onKeydown(down());
320+
listbox.onKeydown(down());
321+
listbox.onKeydown(space({shift: true}));
322+
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry']);
323+
324+
listbox.onKeydown(up());
325+
listbox.onKeydown(up());
326+
listbox.onKeydown(up());
327+
listbox.onKeydown(up());
328+
listbox.onKeydown(space({shift: true}));
329+
expect(listbox.inputs.value()).toEqual(['Banana', 'Apricot', 'Apple']);
330+
});
331+
313332
it('should select the focused option and all options up to the first option on Ctrl + Shift + Home', () => {
314333
listbox.onKeydown(down());
315334
listbox.onKeydown(down());
316335
listbox.onKeydown(down());
317336
listbox.onKeydown(home({control: true, shift: true}));
318-
expect(listbox.inputs.value()).toEqual(['Apple', 'Apricot', 'Banana', 'Blackberry']);
337+
expect(listbox.inputs.value()).toEqual(['Blackberry', 'Banana', 'Apricot', 'Apple']);
319338
});
320339

321340
it('should select the focused option and all options down to the last option on Ctrl + Shift + End', () => {
@@ -414,22 +433,39 @@ describe('Listbox Pattern', () => {
414433
expect(listbox.inputs.value()).toEqual(['Apple']);
415434
});
416435

417-
it('should select contiguous items from the most recently selected item to the focused item on Shift + Space (or Enter)', () => {
436+
it('should select a range of options on Shift + Space (or Enter)', () => {
437+
listbox.onKeydown(down());
438+
listbox.onKeydown(down({control: true}));
439+
listbox.onKeydown(down({control: true}));
440+
listbox.onKeydown(space({shift: true}));
441+
expect(listbox.inputs.value()).toEqual(['Apricot', 'Banana', 'Blackberry']);
442+
});
443+
444+
it('should deselect options outside the range on subsequent on Shift + Space (or Enter)', () => {
418445
listbox.onKeydown(down({control: true}));
419446
listbox.onKeydown(down({control: true}));
420-
listbox.onKeydown(down()); // Blackberry
447+
listbox.onKeydown(space());
448+
expect(listbox.inputs.value()).toEqual(['Banana']);
449+
421450
listbox.onKeydown(down({control: true}));
422451
listbox.onKeydown(down({control: true}));
423452
listbox.onKeydown(space({shift: true}));
424-
expect(listbox.inputs.value()).toEqual(['Blackberry', 'Blueberry', 'Cantaloupe']);
453+
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry']);
454+
455+
listbox.onKeydown(up({control: true}));
456+
listbox.onKeydown(up({control: true}));
457+
listbox.onKeydown(up({control: true}));
458+
listbox.onKeydown(up({control: true}));
459+
listbox.onKeydown(space({shift: true}));
460+
expect(listbox.inputs.value()).toEqual(['Banana', 'Apricot', 'Apple']);
425461
});
426462

427463
it('should select the focused option and all options up to the first option on Ctrl + Shift + Home', () => {
428464
listbox.onKeydown(down({control: true}));
429465
listbox.onKeydown(down({control: true}));
430466
listbox.onKeydown(down());
431467
listbox.onKeydown(home({control: true, shift: true}));
432-
expect(listbox.inputs.value()).toEqual(['Blackberry', 'Apple', 'Apricot', 'Banana']);
468+
expect(listbox.inputs.value()).toEqual(['Blackberry', 'Banana', 'Apricot', 'Apple']);
433469
});
434470

435471
it('should select the focused option and all options down to the last option on Ctrl + Shift + End', () => {
@@ -528,15 +564,16 @@ describe('Listbox Pattern', () => {
528564
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry', 'Cantaloupe']);
529565
});
530566

531-
it('should deselect options from anchor on shift + click', () => {
567+
it('should deselect options outside the range on subsequent shift + clicks', () => {
532568
const {listbox, options} = getDefaultPatterns({
533569
multi: signal(true),
534570
selectionMode: signal('explicit'),
535571
});
536572
listbox.onPointerdown(click(options, 2));
537-
listbox.onPointerdown(click(options, 5));
538-
listbox.onPointerdown(click(options, 2, {shift: true}));
539-
expect(listbox.inputs.value()).toEqual([]);
573+
listbox.onPointerdown(click(options, 5, {shift: true}));
574+
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry', 'Cantaloupe']);
575+
listbox.onPointerdown(click(options, 0, {shift: true}));
576+
expect(listbox.inputs.value()).toEqual(['Banana', 'Apricot', 'Apple']);
540577
});
541578
});
542579

@@ -578,7 +615,7 @@ describe('Listbox Pattern', () => {
578615
expect(listbox.inputs.value()).toEqual([]);
579616
});
580617

581-
it('should select options from anchor on shift + click', () => {
618+
it('should select a range of options on shift + click', () => {
582619
const {listbox, options} = getDefaultPatterns({
583620
multi: signal(true),
584621
selectionMode: signal('follow'),
@@ -588,15 +625,16 @@ describe('Listbox Pattern', () => {
588625
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry', 'Cantaloupe']);
589626
});
590627

591-
it('should deselect options from anchor on shift + click', () => {
628+
it('should deselect options outside the range on subsequent shift + clicks', () => {
592629
const {listbox, options} = getDefaultPatterns({
593630
multi: signal(true),
594631
selectionMode: signal('follow'),
595632
});
596633
listbox.onPointerdown(click(options, 2));
597-
listbox.onPointerdown(click(options, 5, {control: true}));
598-
listbox.onPointerdown(click(options, 2, {shift: true}));
599-
expect(listbox.inputs.value()).toEqual([]);
634+
listbox.onPointerdown(click(options, 5, {shift: true}));
635+
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry', 'Cantaloupe']);
636+
listbox.onPointerdown(click(options, 0, {shift: true}));
637+
expect(listbox.inputs.value()).toEqual(['Banana', 'Apricot', 'Apple']);
600638
});
601639
});
602640

@@ -609,5 +647,16 @@ describe('Listbox Pattern', () => {
609647
listbox.onPointerdown(click(options, 2));
610648
expect(listbox.inputs.value()).toEqual([]);
611649
});
650+
651+
it('should maintain the range selection between pointer and keyboard', () => {
652+
const {listbox, options} = getDefaultPatterns({multi: signal(true)});
653+
listbox.onPointerdown(click(options, 2));
654+
listbox.onKeydown(down());
655+
listbox.onKeydown(down());
656+
listbox.onKeydown(space({shift: true}));
657+
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry']);
658+
listbox.onPointerdown(click(options, 0, {shift: true}));
659+
expect(listbox.inputs.value()).toEqual(['Banana', 'Apricot', 'Apple']);
660+
});
612661
});
613662
});

0 commit comments

Comments
 (0)