Skip to content

Commit 1ba4988

Browse files
committed
feat(cdk-experimental/nav): create CdkNav and CdkLink
1 parent 501e45f commit 1ba4988

File tree

6 files changed

+223
-0
lines changed

6 files changed

+223
-0
lines changed

.ng-dev/commit-message.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const commitMessage: CommitMessageConfig = {
1212
'cdk-experimental/column-resize',
1313
'cdk-experimental/combobox',
1414
'cdk-experimental/listbox',
15+
'cdk-experimental/nav',
1516
'cdk-experimental/popover-edit',
1617
'cdk-experimental/scrolling',
1718
'cdk-experimental/selection',

src/cdk-experimental/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [
44
"combobox",
55
"deferred-content",
66
"listbox",
7+
"nav",
78
"popover-edit",
89
"scrolling",
910
"selection",

src/cdk-experimental/nav/BUILD.bazel

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
load("//tools:defaults.bzl", "ng_project")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_project(
6+
name = "listbox",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//:node_modules/@angular/core",
13+
"//src/cdk-experimental/ui-patterns",
14+
"//src/cdk/a11y",
15+
"//src/cdk/bidi",
16+
],
17+
)

src/cdk-experimental/nav/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export * from './public-api';

src/cdk-experimental/nav/nav.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Directionality} from '@angular/cdk/bidi';
10+
import {_IdGenerator} from '@angular/cdk/a11y';
11+
import {
12+
AfterViewInit,
13+
booleanAttribute,
14+
computed,
15+
contentChildren,
16+
Directive,
17+
effect,
18+
ElementRef,
19+
inject,
20+
input,
21+
linkedSignal,
22+
model,
23+
signal,
24+
WritableSignal,
25+
} from '@angular/core';
26+
import {toSignal} from '@angular/core/rxjs-interop';
27+
import {LinkPattern, NavPattern} from '../ui-patterns';
28+
29+
/**
30+
* A Nav container.
31+
*
32+
* Represents a list of navigational links. The CdkNav is a container meant to be used with
33+
* CdkLink as follows:
34+
*
35+
* ```html
36+
* <nav cdkNav [(value)]="selectedRoute">
37+
* <a [value]="'/home'" cdkLink>Home</a>
38+
* <a [value]="'/settings'" cdkLink>Settings</a>
39+
* <a [value]="'/profile'" cdkLink [disabled]="true">Profile</a>
40+
* </nav>
41+
* ```
42+
*/
43+
@Directive({
44+
selector: '[cdkNav]',
45+
exportAs: 'cdkNav',
46+
standalone: true,
47+
host: {
48+
'role': 'navigation', // Common role for <nav> elements or nav groups
49+
'class': 'cdk-nav',
50+
'[attr.tabindex]': 'pattern.tabindex()',
51+
'[attr.aria-disabled]': 'pattern.disabled()',
52+
// aria-orientation is not typically used directly on role="navigation"
53+
'[attr.aria-activedescendant]': 'pattern.activedescendant()',
54+
'(keydown)': 'pattern.onKeydown($event)',
55+
'(pointerdown)': 'pattern.onPointerdown($event)',
56+
},
57+
})
58+
export class CdkNav<V> implements AfterViewInit {
59+
/** The directionality (LTR / RTL) context for the application (or a subtree of it). */
60+
private readonly _directionality = inject(Directionality);
61+
62+
/** The CdkLinks nested inside of the CdkNav. */
63+
private readonly _cdkLinks = contentChildren(CdkLink, {descendants: true});
64+
65+
/** A signal wrapper for directionality. */
66+
protected textDirection = toSignal(this._directionality.change, {
67+
initialValue: this._directionality.value,
68+
});
69+
70+
/** The Link UIPatterns of the child CdkLinks. */
71+
protected items = computed(() => this._cdkLinks().map(link => link.pattern as LinkPattern<V>));
72+
73+
/** Whether the nav is vertically or horizontally oriented. Affects Arrow Key navigation. */
74+
orientation = input<'vertical' | 'horizontal'>('vertical');
75+
76+
/** Whether focus should wrap when navigating past the first or last link. */
77+
wrap = input(false, {transform: booleanAttribute});
78+
79+
/** Whether disabled items in the list should be skipped when navigating. */
80+
skipDisabled = input(true, {transform: booleanAttribute});
81+
82+
/** The focus strategy used by the nav ('roving' or 'activedescendant'). */
83+
focusMode = input<'roving' | 'activedescendant'>('roving');
84+
85+
/** Whether the entire nav is disabled. */
86+
disabled = input(false, {transform: booleanAttribute});
87+
88+
/** The value of the currently selected link. */
89+
value = model<V[]>([]);
90+
91+
/** The index of the currently focused link. */
92+
activeIndex = model<number>(0);
93+
94+
/** The internal selection value signal used by the ListSelection behavior (always V[]). */
95+
private readonly _selectionValue: WritableSignal<V[]> = signal([]);
96+
97+
/** The amount of time before the typeahead search is reset. */
98+
typeaheadDelay = input<number>(0.5); // Picked arbitrarily.
99+
100+
/** The Nav UIPattern instance providing the core logic. */
101+
pattern: NavPattern<V> = new NavPattern<V>({
102+
...this,
103+
textDirection: this.textDirection,
104+
items: this.items,
105+
multi: signal(false),
106+
selectionMode: signal('explicit'),
107+
});
108+
109+
/** Whether the listbox has received focus yet. */
110+
private _hasFocused = signal(false);
111+
112+
/** Whether the options in the listbox have been initialized. */
113+
private _isViewInitialized = signal(false);
114+
115+
constructor() {
116+
effect(() => {
117+
if (this._isViewInitialized() && !this._hasFocused()) {
118+
this.pattern.setDefaultState();
119+
}
120+
});
121+
}
122+
123+
ngAfterViewInit() {
124+
this._isViewInitialized.set(true);
125+
}
126+
127+
onFocus() {
128+
this._hasFocused.set(true);
129+
}
130+
}
131+
132+
/** A selectable link within a CdkNav container. */
133+
@Directive({
134+
selector: '[cdkLink]',
135+
exportAs: 'cdkLink',
136+
standalone: true,
137+
host: {
138+
'role': 'link',
139+
'class': 'cdk-link',
140+
// cdk-active reflects focus/active descendant state
141+
'[class.cdk-active]': 'pattern.active()',
142+
'[attr.id]': 'pattern.id()',
143+
'[attr.tabindex]': 'pattern.tabindex()',
144+
// Use aria-current="page" for the selected/activated link, common for navigation
145+
'[attr.aria-current]': 'pattern.selected() ? "page" : null',
146+
'[attr.aria-disabled]': 'pattern.disabled()',
147+
},
148+
})
149+
export class CdkLink<V> {
150+
/** A reference to the host link element. */
151+
private readonly _elementRef = inject(ElementRef<HTMLElement>);
152+
153+
/** The parent CdkNav instance. */
154+
private readonly _cdkNav = inject(CdkNav<V>);
155+
156+
/** A unique identifier for the link, lazily generated. */
157+
private readonly _idSignal = signal(inject(_IdGenerator).getId('cdk-link-'));
158+
159+
/** The parent Nav UIPattern from the CdkNav container. */
160+
protected nav = computed(() => this._cdkNav.pattern);
161+
162+
/** A signal reference to the host link element. */
163+
protected element = computed(() => this._elementRef.nativeElement);
164+
165+
/** Whether the link is disabled. Disabled links cannot be selected or navigated to. */
166+
disabled = input(false, {transform: booleanAttribute});
167+
168+
/** The unique value associated with this link (e.g., a route path or identifier). */
169+
value = input.required<V>();
170+
171+
/** Optional text used for typeahead matching. Defaults to the element's textContent. */
172+
label = input<string>();
173+
174+
/** The text used by the typeahead functionality. */
175+
protected searchTerm = computed(() => this.label() ?? this.element().textContent?.trim() ?? '');
176+
177+
/** The Link UIPattern instance providing the core logic for this link. */
178+
pattern: LinkPattern<V> = new LinkPattern<V>({
179+
id: this._idSignal,
180+
value: this.value,
181+
disabled: this.disabled,
182+
searchTerm: this.searchTerm,
183+
nav: this.nav,
184+
element: this.element,
185+
});
186+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export {CdkNav, CdkLink} from './nav';

0 commit comments

Comments
 (0)