Skip to content

Nav #31033

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft

Nav #31033

Changes from all 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
@@ -12,6 +12,7 @@ export const commitMessage: CommitMessageConfig = {
'cdk-experimental/column-resize',
'cdk-experimental/combobox',
'cdk-experimental/listbox',
'cdk-experimental/nav',
'cdk-experimental/popover-edit',
'cdk-experimental/scrolling',
'cdk-experimental/selection',
1 change: 1 addition & 0 deletions src/cdk-experimental/config.bzl
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [
"combobox",
"deferred-content",
"listbox",
"nav",
"popover-edit",
"scrolling",
"selection",
17 changes: 17 additions & 0 deletions src/cdk-experimental/nav/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("//tools:defaults.bzl", "ng_project")

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

ng_project(
name = "nav",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
deps = [
"//:node_modules/@angular/core",
"//src/cdk-experimental/ui-patterns",
"//src/cdk/a11y",
"//src/cdk/bidi",
],
)
9 changes: 9 additions & 0 deletions src/cdk-experimental/nav/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

export * from './public-api';
186 changes: 186 additions & 0 deletions src/cdk-experimental/nav/nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {Directionality} from '@angular/cdk/bidi';
import {_IdGenerator} from '@angular/cdk/a11y';
import {
AfterViewInit,
booleanAttribute,
computed,
contentChildren,
Directive,
effect,
ElementRef,
inject,
input,
linkedSignal,
model,
signal,
WritableSignal,
} from '@angular/core';
import {toSignal} from '@angular/core/rxjs-interop';
import {LinkPattern, NavPattern} from '../ui-patterns';

/**
* A Nav container.
*
* Represents a list of navigational links. The CdkNav is a container meant to be used with
* CdkLink as follows:
*
* ```html
* <nav cdkNav [(value)]="selectedRoute">
* <a [value]="'/home'" cdkLink>Home</a>
* <a [value]="'/settings'" cdkLink>Settings</a>
* <a [value]="'/profile'" cdkLink [disabled]="true">Profile</a>
* </nav>
* ```
*/
@Directive({
selector: '[cdkNav]',
exportAs: 'cdkNav',
standalone: true,
host: {
'role': 'navigation', // Common role for <nav> elements or nav groups
'class': 'cdk-nav',
'[attr.tabindex]': 'pattern.tabindex()',
'[attr.aria-disabled]': 'pattern.disabled()',
// aria-orientation is not typically used directly on role="navigation"
'[attr.aria-activedescendant]': 'pattern.activedescendant()',
'(keydown)': 'pattern.onKeydown($event)',
'(pointerdown)': 'pattern.onPointerdown($event)',
},
})
export class CdkNav<V> implements AfterViewInit {
/** The directionality (LTR / RTL) context for the application (or a subtree of it). */
private readonly _directionality = inject(Directionality);

/** The CdkLinks nested inside of the CdkNav. */
private readonly _cdkLinks = contentChildren(CdkLink, {descendants: true});

/** A signal wrapper for directionality. */
protected textDirection = toSignal(this._directionality.change, {
initialValue: this._directionality.value,
});

/** The Link UIPatterns of the child CdkLinks. */
protected items = computed(() => this._cdkLinks().map(link => link.pattern as LinkPattern<V>));

/** Whether the nav is vertically or horizontally oriented. Affects Arrow Key navigation. */
orientation = input<'vertical' | 'horizontal'>('vertical');

/** Whether focus should wrap when navigating past the first or last link. */
wrap = input(false, {transform: booleanAttribute});

/** Whether disabled items in the list should be skipped when navigating. */
skipDisabled = input(true, {transform: booleanAttribute});

/** The focus strategy used by the nav ('roving' or 'activedescendant'). */
focusMode = input<'roving' | 'activedescendant'>('roving');

/** Whether the entire nav is disabled. */
disabled = input(false, {transform: booleanAttribute});

/** The value of the currently selected link. */
value = model<V[]>([]);

/** The index of the currently focused link. */
activeIndex = model<number>(0);

/** The internal selection value signal used by the ListSelection behavior (always V[]). */
private readonly _selectionValue: WritableSignal<V[]> = signal([]);

/** The amount of time before the typeahead search is reset. */
typeaheadDelay = input<number>(0.5); // Picked arbitrarily.

/** The Nav UIPattern instance providing the core logic. */
pattern: NavPattern<V> = new NavPattern<V>({
...this,
textDirection: this.textDirection,
items: this.items,
multi: signal(false),
selectionMode: signal('explicit'),
});

/** Whether the listbox has received focus yet. */
private _hasFocused = signal(false);

/** Whether the options in the listbox have been initialized. */
private _isViewInitialized = signal(false);

constructor() {
effect(() => {
if (this._isViewInitialized() && !this._hasFocused()) {
this.pattern.setDefaultState();
}
});
}

ngAfterViewInit() {
this._isViewInitialized.set(true);
}

onFocus() {
this._hasFocused.set(true);
}
}

/** A selectable link within a CdkNav container. */
@Directive({
selector: '[cdkLink]',
exportAs: 'cdkLink',
standalone: true,
host: {
'role': 'link',
'class': 'cdk-link',
// cdk-active reflects focus/active descendant state
'[class.cdk-active]': 'pattern.active()',
'[attr.id]': 'pattern.id()',
'[attr.tabindex]': 'pattern.tabindex()',
// Use aria-current="page" for the selected/activated link, common for navigation
'[attr.aria-current]': 'pattern.selected() ? "page" : null',
'[attr.aria-disabled]': 'pattern.disabled()',
},
})
export class CdkLink<V> {
/** A reference to the host link element. */
private readonly _elementRef = inject(ElementRef<HTMLElement>);

/** The parent CdkNav instance. */
private readonly _cdkNav = inject(CdkNav<V>);

/** A unique identifier for the link, lazily generated. */
private readonly _idSignal = signal(inject(_IdGenerator).getId('cdk-link-'));

/** The parent Nav UIPattern from the CdkNav container. */
protected nav = computed(() => this._cdkNav.pattern);

/** A signal reference to the host link element. */
protected element = computed(() => this._elementRef.nativeElement);

/** Whether the link is disabled. Disabled links cannot be selected or navigated to. */
disabled = input(false, {transform: booleanAttribute});

/** The unique value associated with this link (e.g., a route path or identifier). */
value = input.required<V>();

/** Optional text used for typeahead matching. Defaults to the element's textContent. */
label = input<string>();

/** The text used by the typeahead functionality. */
protected searchTerm = computed(() => this.label() ?? this.element().textContent?.trim() ?? '');

/** The Link UIPattern instance providing the core logic for this link. */
pattern: LinkPattern<V> = new LinkPattern<V>({
id: this._idSignal,
value: this.value,
disabled: this.disabled,
searchTerm: this.searchTerm,
nav: this.nav,
element: this.element,
});
}
9 changes: 9 additions & 0 deletions src/cdk-experimental/nav/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

export {CdkNav, CdkLink} from './nav';
1 change: 1 addition & 0 deletions src/cdk-experimental/ui-patterns/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ ts_project(
"//:node_modules/@angular/core",
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
"//src/cdk-experimental/ui-patterns/listbox",
"//src/cdk-experimental/ui-patterns/nav",
"//src/cdk-experimental/ui-patterns/radio",
"//src/cdk-experimental/ui-patterns/tabs",
],
20 changes: 20 additions & 0 deletions src/cdk-experimental/ui-patterns/nav/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
load("//tools:defaults.bzl", "ts_project")

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

ts_project(
name = "nav",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.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/list-selection",
"//src/cdk-experimental/ui-patterns/behaviors/list-typeahead",
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
],
)
80 changes: 80 additions & 0 deletions src/cdk-experimental/ui-patterns/nav/link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {computed} from '@angular/core';
import {ListSelection, ListSelectionItem} from '../behaviors/list-selection/list-selection';
import {ListTypeaheadItem} from '../behaviors/list-typeahead/list-typeahead';
import {ListNavigation, ListNavigationItem} from '../behaviors/list-navigation/list-navigation';
import {ListFocus, ListFocusItem} from '../behaviors/list-focus/list-focus';
import {SignalLike} from '../behaviors/signal-like/signal-like';

/**
* Represents the properties exposed by a nav that need to be accessed by a link.
* This exists to avoid circular dependency errors between the nav and link.
*/
interface NavPattern<V> {
focusManager: ListFocus<LinkPattern<V>>;
selection: ListSelection<LinkPattern<V>, V>;
navigation: ListNavigation<LinkPattern<V>>;
}

/** Represents the required inputs for a link in a nav. */
export interface LinkInputs<V>
extends ListNavigationItem,
ListSelectionItem<V>,
ListTypeaheadItem,
ListFocusItem {
nav: SignalLike<NavPattern<V> | undefined>;
}

/** Represents a link in a nav. */
export class LinkPattern<V> {
/** A unique identifier for the link. */
id: SignalLike<string>;

/** The value of the link, typically a URL or route path. */
value: SignalLike<V>;

/** The position of the link in the list. */
index = computed(
() =>
this.nav()
?.navigation.inputs.items()
.findIndex(i => i.id() === this.id()) ?? -1,
);

/** Whether the link is active (focused). */
active = computed(() => this.nav()?.focusManager.activeItem() === this);

/** Whether the link is selected (activated). */
selected = computed(() => this.nav()?.selection.inputs.value().includes(this.value()));

/** Whether the link is disabled. */
disabled: SignalLike<boolean>;

/** The text used by the typeahead search. */
searchTerm: SignalLike<string>;

/** A reference to the parent nav. */
nav: SignalLike<NavPattern<V> | undefined>;

/** The tabindex of the link. */
tabindex = computed(() => this.nav()?.focusManager.getItemTabindex(this));

/** The html element that should receive focus. */
element: SignalLike<HTMLElement>;

constructor(args: LinkInputs<V>) {
this.id = args.id;
this.value = args.value;
this.nav = args.nav;
this.element = args.element;
this.disabled = args.disabled;
this.searchTerm = args.searchTerm;
}
}
223 changes: 223 additions & 0 deletions src/cdk-experimental/ui-patterns/nav/nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {ModifierKey as Modifier} from '../behaviors/event-manager/event-manager';
import {KeyboardEventManager} from '../behaviors/event-manager/keyboard-event-manager';
import {PointerEventManager} from '../behaviors/event-manager/pointer-event-manager';
import {LinkPattern} from './link';
import {ListSelection, ListSelectionInputs} from '../behaviors/list-selection/list-selection';
import {ListTypeahead, ListTypeaheadInputs} from '../behaviors/list-typeahead/list-typeahead';
import {ListNavigation, ListNavigationInputs} from '../behaviors/list-navigation/list-navigation';
import {ListFocus, ListFocusInputs} from '../behaviors/list-focus/list-focus';
import {computed, signal} from '@angular/core';

/** The selection operations that the nav can perform. */
interface SelectOptions {
selectOne?: boolean;
}

/** Represents the required inputs for a nav. */
export type NavInputs<V> = ListNavigationInputs<LinkPattern<V>> &
ListSelectionInputs<LinkPattern<V>, V> &
ListTypeaheadInputs<LinkPattern<V>> &
ListFocusInputs<LinkPattern<V>>;

/** Controls the state of a nav. */
export class NavPattern<V> {
/** Controls navigation for the nav. */
navigation: ListNavigation<LinkPattern<V>>;

/** Controls selection for the nav. */
selection: ListSelection<LinkPattern<V>, V>;

/** Controls typeahead for the nav. */
typeahead: ListTypeahead<LinkPattern<V>>;

/** Controls focus for the nav. */
focusManager: ListFocus<LinkPattern<V>>;

/** Whether the nav is disabled. */
disabled = computed(() => this.focusManager.isListDisabled());

/** The tabindex of the nav. */
tabindex = computed(() => this.focusManager.getListTabindex());

/** The id of the current active item. */
activedescendant = computed(() => this.focusManager.getActiveDescendant());

/** The number of items in the nav. */
setsize = computed(() => this.navigation.inputs.items().length);

/** The key used to navigate to the previous item in the list. */
prevKey = computed(() => {
// Nav is typically vertical, but respect orientation if provided.
if (this.inputs.orientation() === 'horizontal') {
return this.inputs.textDirection() === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
}
return 'ArrowUp';
});

/** The key used to navigate to the next item in the list. */
nextKey = computed(() => {
// Nav is typically vertical, but respect orientation if provided.
if (this.inputs.orientation() === 'horizontal') {
return this.inputs.textDirection() === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
}
return 'ArrowDown';
});

/** Represents the space key. Does nothing when the user is actively using typeahead. */
dynamicSpaceKey = computed(() => (this.typeahead.isTyping() ? '' : ' '));

/** The regexp used to decide if a key should trigger typeahead. */
typeaheadRegexp = /^.$/; // TODO: Ignore spaces?

/** The keydown event manager for the nav. */
keydown = computed(() => {
return new KeyboardEventManager()
.on(this.prevKey, () => this.prev())
.on(this.nextKey, () => this.next())
.on('Home', () => this.first())
.on('End', () => this.last())
.on(this.typeaheadRegexp, e => this.search(e.key))
.on(this.dynamicSpaceKey, () => this.selection.selectOne()) // Activate link
.on('Enter', () => this.selection.selectOne()); // Activate link
});

/** The pointerdown event manager for the nav. */
pointerdown = computed(() => {
const manager = new PointerEventManager();
manager.on(e => this.goto(e, {selectOne: true}));
return manager;
});

constructor(readonly inputs: NavInputs<V>) {
this.focusManager = new ListFocus(inputs);
// Nav always uses 'follow' selection mode and is single-select.
this.selection = new ListSelection({
...inputs,
focusManager: this.focusManager,
multi: signal(false),
selectionMode: signal('follow'),
});
this.typeahead = new ListTypeahead({...inputs, focusManager: this.focusManager});
this.navigation = new ListNavigation({
...inputs,
focusManager: this.focusManager,
// Nav wrapping is typically desired.
wrap: computed(() => this.inputs.wrap()),
});
}

/** Handles keydown events for the nav. */
onKeydown(event: KeyboardEvent) {
if (!this.disabled()) {
this.keydown().handle(event);
}
}

/** Handles pointerdown events for the nav. */
onPointerdown(event: PointerEvent) {
if (!this.disabled()) {
this.pointerdown().handle(event);
}
}

/** Navigates to the first link in the nav. */
first(opts?: SelectOptions) {
this._navigate(opts, () => this.navigation.first());
}

/** Navigates to the last link in the nav. */
last(opts?: SelectOptions) {
this._navigate(opts, () => this.navigation.last());
}

/** Navigates to the next link in the nav. */
next(opts?: SelectOptions) {
this._navigate(opts, () => this.navigation.next());
}

/** Navigates to the previous link in the nav. */
prev(opts?: SelectOptions) {
this._navigate(opts, () => this.navigation.prev());
}

/** Navigates to the given link in the nav. */
goto(event: PointerEvent, opts?: SelectOptions) {
const item = this._getItem(event);
this._navigate(opts, () => this.navigation.goto(item));
}

/** Handles typeahead search navigation for the nav. */
search(char: string, opts?: SelectOptions) {
this._navigate(opts, () => this.typeahead.search(char));
}

/**
* Sets the nav to its default initial state.
*
* Sets the active index of the nav to the first focusable selected
* item if one exists. Otherwise, sets focus to the first focusable item.
*
* This method should be called once the nav and its links are properly initialized.
*/
setDefaultState() {
let firstItem: LinkPattern<V> | null = null;

for (const item of this.inputs.items()) {
if (this.focusManager.isFocusable(item)) {
if (!firstItem) {
firstItem = item;
}
if (item.selected()) {
this.inputs.activeIndex.set(item.index());
return;
}
}
}

if (firstItem) {
this.inputs.activeIndex.set(firstItem.index());
}
}

/**
* Safely performs a navigation operation.
*
* Handles boilerplate calling of focus & selection operations. Also ensures these
* additional operations are only called if the navigation operation moved focus to a new link.
*/
private _navigate(opts: SelectOptions = {}, operation: () => boolean) {
const moved = operation();

if (moved) {
this._updateSelection(opts);
}
}

/** Handles updating selection for the nav. */
private _updateSelection(opts: SelectOptions = {}) {
// In nav, navigation always implies selection (activation).
if (opts.selectOne) {
this.selection.selectOne();
}
}

/** Gets the LinkPattern associated with a pointer event target. */
private _getItem(e: PointerEvent) {
if (!(e.target instanceof HTMLElement)) {
return;
}

// Assuming links have a role or specific attribute to identify them.
// Adjust selector as needed based on actual link implementation.
const element = e.target.closest('[role="link"], [cdkLink]');
return this.inputs.items().find(i => i.element() === element);
}
}
2 changes: 2 additions & 0 deletions src/cdk-experimental/ui-patterns/public-api.ts
Original file line number Diff line number Diff line change
@@ -11,4 +11,6 @@ export * from './listbox/option';
export * from './radio/radio-group';
export * from './radio/radio';
export * from './behaviors/signal-like/signal-like';
export * from './nav/nav';
export * from './nav/link';
export * from './tabs/tabs';
29 changes: 29 additions & 0 deletions src/components-examples/cdk-experimental/nav/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
load("//tools:defaults.bzl", "ng_project")

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

ng_project(
name = "nav",
srcs = glob(["**/*.ts"]),
assets = glob([
"**/*.html",
"**/*.css",
]),
deps = [
"//:node_modules/@angular/core",
"//:node_modules/@angular/forms",
"//src/cdk-experimental/nav",
"//src/material/checkbox",
"//src/material/form-field",
"//src/material/select",
],
)

filegroup(
name = "source-files",
srcs = glob([
"**/*.html",
"**/*.css",
"**/*.ts",
]),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.example-nav {
border: 1px solid var(--mat-sys-outline);
padding: 10px;
display: flex;
flex-direction: column;
width: 200px;
gap: 4px; /* Add gap like listbox */
}

.example-link {
padding: 8px 12px;
cursor: pointer;
text-decoration: none;
border: 1px solid var(--mat-sys-outline);
border-radius: var(--mat-sys-corner-extra-small);
display: block; /* Ensure links take full width */
outline: none; /* Remove default browser focus outline */
position: relative; /* For potential future pseudo-elements */
}

/* Style for hover and keyboard focus (cdk-active) */
.example-link.cdk-active:not([aria-disabled='true']),
.example-link:hover:not([aria-disabled='true']) {
outline: 1px solid var(--mat-sys-outline);
background: var(--mat-sys-surface-container);
}

/* Style for focus-visible (programmatic/keyboard focus) */
.example-link:focus-visible:not([aria-disabled='true']) {
outline: 2px solid var(--mat-sys-primary);
background: var(--mat-sys-surface-container);
}

/* Style for the selected link (using aria-current="page") */
.example-link[aria-current='page']:not([aria-disabled='true']) {
background-color: var(--mat-sys-secondary-container);
font-weight: bold;
}

.example-link[aria-disabled='true'] {
color: var(--mat-sys-on-surface);
opacity: 0.3;
cursor: not-allowed;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<nav class="example-nav" cdkNav [(value)]="selectedValue">
@for (link of links; track link.path) {
<a
class="example-link"
cdkLink
[value]="link.path"
[disabled]="link.disabled">
{{ link.label }}
</a>
}
</nav>

<p>
Selected Value: {{ selectedValue().join(', ') }}
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {ChangeDetectionStrategy, Component, model} from '@angular/core';
import {CdkLink, CdkNav} from '@angular/cdk-experimental/nav';

@Component({
selector: 'cdk-nav-example',
templateUrl: 'cdk-nav-example.html',
styleUrl: 'cdk-nav-example.css',
standalone: true,
imports: [CdkNav, CdkLink],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CdkNavExample {
selectedValue = model<string[]>(['/home']); // Default selected value

links = [
{label: 'Home', path: '/home'},
{label: 'Settings', path: '/settings'},
{label: 'Profile', path: '/profile', disabled: true},
{label: 'Admin', path: '/admin'},
];
}
1 change: 1 addition & 0 deletions src/components-examples/cdk-experimental/nav/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {CdkNavExample} from './cdk-nav/cdk-nav-example';
1 change: 1 addition & 0 deletions src/dev-app/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ ng_project(
"//src/dev-app/cdk-dialog",
"//src/dev-app/cdk-experimental-combobox",
"//src/dev-app/cdk-experimental-listbox",
"//src/dev-app/cdk-experimental-nav",
"//src/dev-app/cdk-experimental-tabs",
"//src/dev-app/cdk-listbox",
"//src/dev-app/cdk-menu",
13 changes: 13 additions & 0 deletions src/dev-app/cdk-experimental-nav/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
load("//tools:defaults.bzl", "ng_project")

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

ng_project(
name = "cdk-experimental-nav",
srcs = glob(["**/*.ts"]),
assets = ["cdk-nav-demo.html"],
deps = [
"//:node_modules/@angular/core",
"//src/components-examples/cdk-experimental/nav",
],
)
4 changes: 4 additions & 0 deletions src/dev-app/cdk-experimental-nav/cdk-nav-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div>
<h4>Listbox using UI Patterns</h4>
<cdk-nav-example></cdk-nav-example>
</div>
17 changes: 17 additions & 0 deletions src/dev-app/cdk-experimental-nav/cdk-nav-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {ChangeDetectionStrategy, Component} from '@angular/core';
import {CdkNavExample} from '@angular/components-examples/cdk-experimental/nav';

@Component({
templateUrl: 'cdk-nav-demo.html',
imports: [CdkNavExample],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CdkExperimentalNavDemo {}
1 change: 1 addition & 0 deletions src/dev-app/dev-app/dev-app-layout.ts
Original file line number Diff line number Diff line change
@@ -61,6 +61,7 @@ export class DevAppLayout {
{name: 'CDK Dialog', route: '/cdk-dialog'},
{name: 'CDK Experimental Combobox', route: '/cdk-experimental-combobox'},
{name: 'CDK Experimental Listbox', route: '/cdk-experimental-listbox'},
{name: 'CDK Experimental Nav', route: '/cdk-experimental-nav'},
{name: 'CDK Experimental Tabs', route: '/cdk-experimental-tabs'},
{name: 'CDK Listbox', route: '/cdk-listbox'},
{name: 'CDK Menu', route: '/cdk-menu'},
5 changes: 5 additions & 0 deletions src/dev-app/routes.ts
Original file line number Diff line number Diff line change
@@ -50,6 +50,11 @@ export const DEV_APP_ROUTES: Routes = [
loadComponent: () =>
import('./cdk-experimental-listbox/cdk-listbox-demo').then(m => m.CdkExperimentalListboxDemo),
},
{
path: 'cdk-experimental-nav',
loadComponent: () =>
import('./cdk-experimental-nav/cdk-nav-demo').then(m => m.CdkExperimentalNavDemo),
},
{
path: 'cdk-experimental-tabs',
loadComponent: () =>