Skip to content

Commit 7166610

Browse files
committed
feat(cdk-experimental/listbox): readonly mode
1 parent eed6441 commit 7166610

File tree

5 files changed

+100
-6
lines changed

5 files changed

+100
-6
lines changed

src/cdk-experimental/listbox/listbox.ts

+3
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ export class CdkListbox<V> {
8989
/** Whether the listbox is disabled. */
9090
disabled = input(false, {transform: booleanAttribute});
9191

92+
/** Whether the listbox is readonly. */
93+
readonly = input(false, {transform: booleanAttribute});
94+
9295
/** The values of the current selected items. */
9396
value = model<V[]>([]);
9497

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

+77-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {signal} from '@angular/core';
9+
import {signal, WritableSignal} from '@angular/core';
1010
import {ListboxInputs, ListboxPattern} from './listbox';
1111
import {OptionPattern} from './option';
1212
import {createKeyboardEvent} from '@angular/cdk/testing/private';
@@ -33,6 +33,7 @@ describe('Listbox Pattern', () => {
3333
activeIndex: inputs.activeIndex ?? signal(0),
3434
typeaheadDelay: inputs.typeaheadDelay ?? signal(0.5),
3535
wrap: inputs.wrap ?? signal(true),
36+
readonly: inputs.readonly ?? signal(false),
3637
disabled: inputs.disabled ?? signal(false),
3738
skipDisabled: inputs.skipDisabled ?? signal(true),
3839
multi: inputs.multi ?? signal(false),
@@ -148,6 +149,18 @@ describe('Listbox Pattern', () => {
148149
listbox.onKeydown(end());
149150
expect(listbox.inputs.activeIndex()).toBe(8);
150151
});
152+
153+
it('should be able to navigate in readonly mode', () => {
154+
const {listbox} = getDefaultPatterns();
155+
listbox.onKeydown(down());
156+
expect(listbox.inputs.activeIndex()).toBe(1);
157+
listbox.onKeydown(up());
158+
expect(listbox.inputs.activeIndex()).toBe(0);
159+
listbox.onKeydown(end());
160+
expect(listbox.inputs.activeIndex()).toBe(8);
161+
listbox.onKeydown(home());
162+
expect(listbox.inputs.activeIndex()).toBe(0);
163+
});
151164
});
152165

153166
describe('Keyboard Selection', () => {
@@ -178,6 +191,22 @@ describe('Listbox Pattern', () => {
178191
expect(listbox.inputs.activeIndex()).toBe(0);
179192
expect(listbox.inputs.value()).toEqual(['Apple']);
180193
});
194+
195+
it('should not be able to change selection when in readonly mode', () => {
196+
const {listbox} = getDefaultPatterns({
197+
value: signal(['Apple']),
198+
readonly: signal(true),
199+
multi: signal(false),
200+
selectionMode: signal('follow'),
201+
});
202+
203+
expect(listbox.inputs.activeIndex()).toBe(0);
204+
expect(listbox.inputs.value()).toEqual(['Apple']);
205+
206+
listbox.onKeydown(down());
207+
expect(listbox.inputs.activeIndex()).toBe(1);
208+
expect(listbox.inputs.value()).toEqual(['Apple']);
209+
});
181210
});
182211

183212
describe('explicit focus & single select', () => {
@@ -207,6 +236,17 @@ describe('Listbox Pattern', () => {
207236
listbox.onKeydown(enter());
208237
expect(listbox.inputs.value()).toEqual(['Apricot']);
209238
});
239+
240+
it('should not be able to change selection when in readonly mode', () => {
241+
const readonly = listbox.inputs.readonly as WritableSignal<boolean>;
242+
readonly.set(true);
243+
listbox.onKeydown(space());
244+
expect(listbox.inputs.value()).toEqual([]);
245+
246+
listbox.onKeydown(down());
247+
listbox.onKeydown(enter());
248+
expect(listbox.inputs.value()).toEqual([]);
249+
});
210250
});
211251

212252
describe('explicit focus & multi select', () => {
@@ -277,6 +317,29 @@ describe('Listbox Pattern', () => {
277317
listbox.onKeydown(end({control: true, shift: true}));
278318
expect(listbox.inputs.value()).toEqual(['Cantaloupe', 'Cherry', 'Clementine', 'Cranberry']);
279319
});
320+
321+
it('should not be able to change selection when in readonly mode', () => {
322+
const readonly = listbox.inputs.readonly as WritableSignal<boolean>;
323+
readonly.set(true);
324+
listbox.onKeydown(space());
325+
expect(listbox.inputs.value()).toEqual([]);
326+
327+
listbox.onKeydown(down());
328+
listbox.onKeydown(enter());
329+
expect(listbox.inputs.value()).toEqual([]);
330+
331+
listbox.onKeydown(up({shift: true}));
332+
expect(listbox.inputs.value()).toEqual([]);
333+
334+
listbox.onKeydown(down({shift: true}));
335+
expect(listbox.inputs.value()).toEqual([]);
336+
337+
listbox.onKeydown(end({control: true, shift: true}));
338+
expect(listbox.inputs.value()).toEqual([]);
339+
340+
listbox.onKeydown(home({control: true, shift: true}));
341+
expect(listbox.inputs.value()).toEqual([]);
342+
});
280343
});
281344

282345
describe('follows focus & multi select', () => {
@@ -361,6 +424,19 @@ describe('Listbox Pattern', () => {
361424
listbox.onKeydown(end({control: true, shift: true}));
362425
expect(listbox.inputs.value()).toEqual(['Cantaloupe', 'Cherry', 'Clementine', 'Cranberry']);
363426
});
427+
428+
it('should not be able to change selection when in readonly mode', () => {
429+
const readonly = listbox.inputs.readonly as WritableSignal<boolean>;
430+
readonly.set(true);
431+
listbox.onKeydown(down());
432+
expect(listbox.inputs.value()).toEqual(['Apple']);
433+
434+
listbox.onKeydown(up());
435+
expect(listbox.inputs.value()).toEqual(['Apple']);
436+
437+
listbox.onKeydown(space({control: true}));
438+
expect(listbox.inputs.value()).toEqual(['Apple']);
439+
});
364440
});
365441
});
366442
});

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

+16-4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type ListboxInputs<V> = ListNavigationInputs<OptionPattern<V>> &
3434
ListTypeaheadInputs &
3535
ListFocusInputs<OptionPattern<V>> & {
3636
disabled: SignalLike<boolean>;
37+
readonly: SignalLike<boolean>;
3738
};
3839

3940
/** Controls the state of a listbox. */
@@ -94,6 +95,15 @@ export class ListboxPattern<V> {
9495
keydown = computed(() => {
9596
const manager = new KeyboardEventManager();
9697

98+
if (this.inputs.readonly()) {
99+
return manager
100+
.on(this.prevKey, () => this.prev())
101+
.on(this.nextKey, () => this.next())
102+
.on('Home', () => this.first())
103+
.on('End', () => this.last())
104+
.on(this.typeaheadRegexp, e => this.search(e.key));
105+
}
106+
97107
if (!this.followFocus()) {
98108
manager
99109
.on(this.prevKey, () => this.prev())
@@ -150,15 +160,17 @@ export class ListboxPattern<V> {
150160
pointerdown = computed(() => {
151161
const manager = new PointerEventManager();
152162

163+
if (this.inputs.readonly()) {
164+
manager.on(e => this.goto(e));
165+
}
166+
153167
if (this.inputs.multi()) {
154-
manager
168+
return manager
155169
.on(e => this.goto(e, {toggle: true}))
156170
.on(Modifier.Shift, e => this.goto(e, {selectFromActive: true}));
157-
} else {
158-
manager.on(e => this.goto(e, {toggleOne: true}));
159171
}
160172

161-
return manager;
173+
return manager.on(e => this.goto(e, {toggleOne: true}));
162174
});
163175

164176
constructor(readonly inputs: ListboxInputs<V>) {

src/components-examples/cdk-experimental/listbox/cdk-listbox/cdk-listbox-example.html

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<mat-checkbox [formControl]="wrap">Wrap</mat-checkbox>
33
<mat-checkbox [formControl]="multi">Multi</mat-checkbox>
44
<mat-checkbox [formControl]="disabled">Disabled</mat-checkbox>
5+
<mat-checkbox [formControl]="readonly">Readonly</mat-checkbox>
56
<mat-checkbox [formControl]="skipDisabled">Skip Disabled</mat-checkbox>
67

78
<mat-form-field subscriptSizing="dynamic" appearance="outline">
@@ -33,8 +34,9 @@
3334
<ul
3435
cdkListbox
3536
[wrap]="wrap.value"
36-
[disabled]="disabled.value"
3737
[multi]="multi.value"
38+
[readonly]="readonly.value"
39+
[disabled]="disabled.value"
3840
[skipDisabled]="skipDisabled.value"
3941
[orientation]="orientation"
4042
[focusMode]="focusMode"

src/components-examples/cdk-experimental/listbox/cdk-listbox/cdk-listbox-example.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class CdkListboxExample {
3030
wrap = new FormControl(true, {nonNullable: true});
3131
multi = new FormControl(false, {nonNullable: true});
3232
disabled = new FormControl(false, {nonNullable: true});
33+
readonly = new FormControl(false, {nonNullable: true});
3334
skipDisabled = new FormControl(true, {nonNullable: true});
3435

3536
fruits = [

0 commit comments

Comments
 (0)