Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ng-dev/commit-message.mts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const commitMessage: CommitMessageConfig = {
'cdk-experimental/selection',
'cdk-experimental/table-scroll-container',
'cdk-experimental/tabs',
'cdk-experimental/toolbar',
'cdk-experimental/tree',
'cdk-experimental/ui-patterns',
'cdk/a11y',
Expand Down
1 change: 1 addition & 0 deletions src/cdk-experimental/radio-group/radio-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export class CdkRadioGroup<V> {
value: this._value,
activeItem: signal(undefined),
textDirection: this.textDirection,
toolbar: signal(undefined),
});

/** Whether the radio group has received focus yet. */
Expand Down
1 change: 1 addition & 0 deletions src/cdk-experimental/ui-patterns/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ts_project(
"//src/cdk-experimental/ui-patterns/listbox",
"//src/cdk-experimental/ui-patterns/radio-group",
"//src/cdk-experimental/ui-patterns/tabs",
"//src/cdk-experimental/ui-patterns/toolbar",
"//src/cdk-experimental/ui-patterns/tree",
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@ export class ListFocus<T extends ListFocusItem> {
prevActiveItem = signal<T | undefined>(undefined);

/** The index of the last item that was active. */
prevActiveIndex = computed(() => this.prevActiveItem()?.index() ?? -1);
prevActiveIndex = computed(() => {
return this.prevActiveItem() ? this.inputs.items().indexOf(this.prevActiveItem()!) : -1;
});

/** The current active index in the list. */
activeIndex = computed(() => this.inputs.activeItem()?.index() ?? -1);
activeIndex = computed(() => {
return this.inputs.activeItem() ? this.inputs.items().indexOf(this.inputs.activeItem()!) : -1;
});

constructor(readonly inputs: ListFocusInputs<T>) {}

Expand Down
1 change: 1 addition & 0 deletions src/cdk-experimental/ui-patterns/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './radio-group/radio-button';
export * from './behaviors/signal-like/signal-like';
export * from './tabs/tabs';
export * from './accordion/accordion';
export * from './toolbar/toolbar';
23 changes: 21 additions & 2 deletions src/cdk-experimental/ui-patterns/radio-group/radio-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,30 @@ import {computed} from '@angular/core';
import {SignalLike} from '../behaviors/signal-like/signal-like';
import {List, ListItem} from '../behaviors/list/list';

/**
* Represents the properties exposed by a toolbar widget that need to be accessed by a radio group.
* This exists to avoid circular dependency errors between the toolbar and radio button.
*/
type ToolbarWidgetLike = {
id: SignalLike<string>;
index: SignalLike<number>;
element: SignalLike<HTMLElement>;
disabled: SignalLike<boolean>;
searchTerm: SignalLike<any>;
value: SignalLike<any>;
};

/**
* Represents the properties exposed by a radio group that need to be accessed by a radio button.
* This exists to avoid circular dependency errors between the radio group and radio button.
*/
interface RadioGroupLike<V> {
/** The list behavior for the radio group. */
listBehavior: List<RadioButtonPattern<V>, V>;
listBehavior: List<RadioButtonPattern<V> | ToolbarWidgetLike, V>;
/** Whether the list is readonly */
readonly: SignalLike<boolean>;
/** Whether the radio group is disabled. */
disabled: SignalLike<boolean>;
}

/** Represents the required inputs for a radio button in a radio group. */
Expand All @@ -34,7 +51,9 @@ export class RadioButtonPattern<V> {
value: SignalLike<V>;

/** The position of the radio button within the group. */
index = computed(() => this.group()?.listBehavior.inputs.items().indexOf(this) ?? -1);
index: SignalLike<number> = computed(
() => this.group()?.listBehavior.inputs.items().indexOf(this) ?? -1,
);

/** Whether the radio button is currently the active one (focused). */
active = computed(() => this.group()?.listBehavior.inputs.activeItem() === this);
Expand Down
49 changes: 43 additions & 6 deletions src/cdk-experimental/ui-patterns/radio-group/radio-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,37 @@ export type RadioGroupInputs<V> = Omit<

/** Whether the radio group is readonly. */
readonly: SignalLike<boolean>;
/** Parent toolbar of radio group */
toolbar: SignalLike<ToolbarLike<V> | undefined>;
};

/**
* Represents the properties exposed by a toolbar widget that need to be accessed by a radio group.
* This exists to avoid circular dependency errors between the toolbar and radio button.
*/
type ToolbarWidgetLike = {
id: SignalLike<string>;
index: SignalLike<number>;
element: SignalLike<HTMLElement>;
disabled: SignalLike<boolean>;
searchTerm: SignalLike<any>;
value: SignalLike<any>;
};

/**
* Represents the properties exposed by a toolbar that need to be accessed by a radio group.
* This exists to avoid circular dependency errors between the toolbar and radio button.
*/
interface ToolbarLike<V> {
listBehavior: List<RadioButtonPattern<V> | ToolbarWidgetLike, V>;
orientation: SignalLike<'vertical' | 'horizontal'>;
disabled: SignalLike<boolean>;
}

/** Controls the state of a radio group. */
export class RadioGroupPattern<V> {
/** The list behavior for the radio group. */
readonly listBehavior: List<RadioButtonPattern<V>, V>;
readonly listBehavior: List<RadioButtonPattern<V> | ToolbarWidgetLike, V>;

/** Whether the radio group is vertically or horizontally oriented. */
orientation: SignalLike<'vertical' | 'horizontal'>;
Expand All @@ -41,8 +66,8 @@ export class RadioGroupPattern<V> {
/** Whether the radio group is readonly. */
readonly = computed(() => this.selectedItem()?.disabled() || this.inputs.readonly());

/** The tabindex of the radio group (if using activedescendant). */
tabindex = computed(() => this.listBehavior.tabindex());
/** The tabindex of the radio group (if using activedescendant or if in toolbar). */
tabindex = computed(() => (this.inputs.toolbar() ? -1 : this.listBehavior.tabindex()));

/** The id of the current active radio button (if using activedescendant). */
activedescendant = computed(() => this.listBehavior.activedescendant());
Expand All @@ -67,6 +92,11 @@ export class RadioGroupPattern<V> {
keydown = computed(() => {
const manager = new KeyboardEventManager();

// When within a toolbar relinquish keyboard control
if (this.inputs.toolbar()) {
return manager;
}

// Readonly mode allows navigation but not selection changes.
if (this.readonly()) {
return manager
Expand All @@ -91,6 +121,11 @@ export class RadioGroupPattern<V> {
pointerdown = computed(() => {
const manager = new PointerEventManager();

// When within a toolbar relinquish pointer control
if (this.inputs.toolbar()) {
return manager;
}

if (this.readonly()) {
// Navigate focus only in readonly mode.
return manager.on(e => this.listBehavior.goto(this._getItem(e)!));
Expand All @@ -101,13 +136,15 @@ export class RadioGroupPattern<V> {
});

constructor(readonly inputs: RadioGroupInputs<V>) {
this.orientation = inputs.orientation;
this.orientation =
inputs.toolbar() !== undefined ? inputs.toolbar()!.orientation : inputs.orientation;

this.listBehavior = new List({
...inputs,
wrap: () => false,
activeItem: inputs.toolbar()?.listBehavior.inputs.activeItem ?? inputs.activeItem,
wrap: () => !!inputs.toolbar(),
multi: () => false,
selectionMode: () => 'follow',
selectionMode: () => (inputs.toolbar() ? 'explicit' : 'follow'),
typeaheadDelay: () => 0, // Radio groups do not support typeahead.
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('RadioGroup Pattern', () => {
focusMode: inputs.focusMode ?? signal('roving'),
textDirection: inputs.textDirection ?? signal('ltr'),
orientation: inputs.orientation ?? signal('vertical'),
toolbar: inputs.toolbar ?? signal(undefined),
});
}

Expand Down
19 changes: 19 additions & 0 deletions src/cdk-experimental/ui-patterns/toolbar/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("//tools:defaults.bzl", "ts_project")

package(default_visibility = ["//visibility:public"])

ts_project(
name = "toolbar",
srcs = [
"toolbar.ts",
],
deps = [
"//:node_modules/@angular/core",
"//src/cdk-experimental/ui-patterns/behaviors/event-manager",
"//src/cdk-experimental/ui-patterns/behaviors/list-focus",
"//src/cdk-experimental/ui-patterns/behaviors/list-navigation",
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
"//src/cdk-experimental/ui-patterns/radio-group",
"//src/cdk/bidi",
],
)
Loading