Skip to content

Commit 9caed2a

Browse files
authored
feat(cdk-experimental/ui-patterns): tabs ui pattern (#30568)
1 parent 3b329e9 commit 9caed2a

26 files changed

+1048
-0
lines changed

Diff for: src/cdk-experimental/deferred-content/BUILD.bazel

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
load("//tools:defaults.bzl", "ng_web_test_suite")
2+
load("//tools:defaults2.bzl", "ng_project", "ts_project")
3+
4+
package(default_visibility = ["//visibility:public"])
5+
6+
ng_project(
7+
name = "deferred-content",
8+
srcs = [
9+
"deferred-content.ts",
10+
"index.ts",
11+
"public-api.ts",
12+
],
13+
deps = [
14+
"//:node_modules/@angular/core",
15+
],
16+
)
17+
18+
ts_project(
19+
name = "unit_test_sources",
20+
testonly = True,
21+
srcs = [
22+
"deferred-content.spec.ts",
23+
],
24+
deps = [
25+
":deferred-content",
26+
"//:node_modules/@angular/core",
27+
"//:node_modules/@angular/platform-browser",
28+
],
29+
)
30+
31+
ng_web_test_suite(
32+
name = "unit_tests",
33+
deps = [":unit_test_sources"],
34+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {Component, DebugElement, Directive, effect, inject, signal} from '@angular/core';
2+
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
3+
import {DeferredContent, DeferredContentAware} from './deferred-content';
4+
import {By} from '@angular/platform-browser';
5+
6+
describe('DeferredContent', () => {
7+
let fixture: ComponentFixture<TestComponent>;
8+
let collapsible: DebugElement;
9+
10+
beforeEach(waitForAsync(() => {
11+
TestBed.configureTestingModule({
12+
imports: [TestComponent],
13+
});
14+
}));
15+
16+
beforeEach(() => {
17+
fixture = TestBed.createComponent(TestComponent);
18+
collapsible = fixture.debugElement.query(By.directive(Collapsible));
19+
});
20+
21+
it('removes the content when hidden.', async () => {
22+
collapsible.injector.get(Collapsible).contentVisible.set(false);
23+
await fixture.whenStable();
24+
expect(collapsible.nativeElement.innerText).toBe('');
25+
});
26+
27+
it('creates the content when visible.', async () => {
28+
collapsible.injector.get(Collapsible).contentVisible.set(true);
29+
await fixture.whenStable();
30+
expect(collapsible.nativeElement.innerText).toBe('Lazy Content');
31+
});
32+
33+
describe('with preserveContent', () => {
34+
let component: TestComponent;
35+
36+
beforeEach(() => {
37+
component = fixture.componentInstance;
38+
component.preserveContent.set(true);
39+
});
40+
41+
it('creates the content when hidden.', async () => {
42+
collapsible.injector.get(Collapsible).contentVisible.set(false);
43+
await fixture.whenStable();
44+
expect(collapsible.nativeElement.innerText).toBe('Lazy Content');
45+
});
46+
47+
it('creates the content when visible.', async () => {
48+
collapsible.injector.get(Collapsible).contentVisible.set(true);
49+
await fixture.whenStable();
50+
expect(collapsible.nativeElement.innerText).toBe('Lazy Content');
51+
});
52+
});
53+
});
54+
55+
@Directive({
56+
selector: '[collapsible]',
57+
hostDirectives: [{directive: DeferredContentAware, inputs: ['preserveContent']}],
58+
})
59+
class Collapsible {
60+
private readonly _deferredContentAware = inject(DeferredContentAware);
61+
62+
contentVisible = signal(true);
63+
64+
constructor() {
65+
effect(() => this._deferredContentAware.contentVisible.set(this.contentVisible()));
66+
}
67+
}
68+
69+
@Directive({
70+
selector: 'ng-template[collapsibleContent]',
71+
hostDirectives: [DeferredContent],
72+
})
73+
class CollapsibleContent {}
74+
75+
@Component({
76+
template: `
77+
<div collapsible [preserveContent]="preserveContent()">
78+
<ng-template collapsibleContent>
79+
Lazy Content
80+
</ng-template>
81+
</div>
82+
`,
83+
imports: [Collapsible, CollapsibleContent],
84+
})
85+
class TestComponent {
86+
preserveContent = signal(false);
87+
}
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 {
10+
computed,
11+
Directive,
12+
effect,
13+
inject,
14+
input,
15+
TemplateRef,
16+
signal,
17+
ViewContainerRef,
18+
} from '@angular/core';
19+
20+
/**
21+
* A container directive controls the visibility of its content.
22+
*/
23+
@Directive()
24+
export class DeferredContentAware {
25+
contentVisible = signal(false);
26+
readonly preserveContent = input(false);
27+
}
28+
29+
/**
30+
* DeferredContent loads/unloads the content based on the visibility.
31+
* The visibilty signal is sent from a parent directive implements
32+
* DeferredContentAware.
33+
*
34+
* Use this directive as a host directive. For example:
35+
*
36+
* ```ts
37+
* @Directive({
38+
* selector: 'ng-template[cdkAccordionContent]',
39+
* hostDirectives: [DeferredContent],
40+
* })
41+
* class CdkAccordionContent {}
42+
* ```
43+
*/
44+
@Directive()
45+
export class DeferredContent {
46+
private readonly _deferredContentAware = inject(DeferredContentAware);
47+
private readonly _templateRef = inject(TemplateRef);
48+
private readonly _viewContainerRef = inject(ViewContainerRef);
49+
private _isRendered = false;
50+
51+
constructor() {
52+
effect(() => {
53+
if (
54+
this._deferredContentAware.preserveContent() ||
55+
this._deferredContentAware.contentVisible()
56+
) {
57+
if (this._isRendered) return;
58+
this._viewContainerRef.clear();
59+
this._viewContainerRef.createEmbeddedView(this._templateRef);
60+
this._isRendered = true;
61+
} else {
62+
this._viewContainerRef.clear();
63+
this._isRendered = false;
64+
}
65+
});
66+
}
67+
}

Diff for: src/cdk-experimental/deferred-content/index.ts

+9
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';

Diff for: src/cdk-experimental/deferred-content/public-api.ts

+9
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 {DeferredContentAware, DeferredContent} from './deferred-content';

Diff for: src/cdk-experimental/tabs/BUILD.bazel

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
load("//tools:defaults2.bzl", "ng_project")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_project(
6+
name = "tabs",
7+
srcs = [
8+
"index.ts",
9+
"public-api.ts",
10+
"tabs.ts",
11+
],
12+
deps = [
13+
"//src/cdk-experimental/deferred-content",
14+
"//src/cdk-experimental/ui-patterns",
15+
"//src/cdk/a11y",
16+
"//src/cdk/bidi",
17+
],
18+
)

Diff for: src/cdk-experimental/tabs/index.ts

+9
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';

Diff for: src/cdk-experimental/tabs/public-api.ts

+9
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 {CdkTabs, CdkTabList, CdkTab, CdkTabPanel, CdkTabContent} from './tabs';

0 commit comments

Comments
 (0)