Skip to content
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

feat(cdk-experimental/ui-patterns): tabs ui pattern #30568

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
36 changes: 36 additions & 0 deletions src/cdk-experimental/deferred-content/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
load("//tools:defaults.bzl", "ng_module", "ng_web_test_suite")
load("//tools:defaults2.bzl", "ts_project")

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

ng_module(
name = "deferred-content",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
deps = [
"@npm//@angular/core",
],
)

ts_project(
name = "unit_test_sources",
testonly = True,
srcs = glob(
["**/*.spec.ts"],
exclude = ["**/*.e2e.spec.ts"],
),
interop_deps = [
":deferred-content",
],
deps = [
"//:node_modules/@angular/core",
"//:node_modules/@angular/platform-browser",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_test_sources"],
)
87 changes: 87 additions & 0 deletions src/cdk-experimental/deferred-content/deferred-content.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {Component, DebugElement, Directive, effect, inject, signal} from '@angular/core';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {DeferredContent, DeferredContentAware} from './deferred-content';
import {By} from '@angular/platform-browser';

describe('DeferredContent', () => {
let fixture: ComponentFixture<TestComponent>;
let collapsible: DebugElement;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [TestComponent],
});
}));

beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
collapsible = fixture.debugElement.query(By.directive(Collapsible));
});

it('removes the content when hidden.', async () => {
collapsible.injector.get(Collapsible).contentVisible.set(false);
await fixture.whenStable();
expect(collapsible.nativeElement.innerText).toBe('');
});

it('creates the content when visible.', async () => {
collapsible.injector.get(Collapsible).contentVisible.set(true);
await fixture.whenStable();
expect(collapsible.nativeElement.innerText).toBe('Lazy Content');
});

describe('with preserveContent', () => {
let component: TestComponent;

beforeEach(() => {
component = fixture.componentInstance;
component.preserveContent.set(true);
});

it('creates the content when hidden.', async () => {
collapsible.injector.get(Collapsible).contentVisible.set(false);
await fixture.whenStable();
expect(collapsible.nativeElement.innerText).toBe('Lazy Content');
});

it('creates the content when visible.', async () => {
collapsible.injector.get(Collapsible).contentVisible.set(true);
await fixture.whenStable();
expect(collapsible.nativeElement.innerText).toBe('Lazy Content');
});
});
});

@Directive({
selector: '[collapsible]',
hostDirectives: [{directive: DeferredContentAware, inputs: ['preserveContent']}],
})
class Collapsible {
private readonly _deferredContentAware = inject(DeferredContentAware);

contentVisible = signal(true);

constructor() {
effect(() => this._deferredContentAware.contentVisible.set(this.contentVisible()));
}
}

@Directive({
selector: 'ng-template[collapsibleContent]',
hostDirectives: [DeferredContent],
})
class CollapsibleContent {}

@Component({
template: `
<div collapsible [preserveContent]="preserveContent()">
<ng-template collapsibleContent>
Lazy Content
</ng-template>
</div>
`,
imports: [Collapsible, CollapsibleContent],
})
class TestComponent {
preserveContent = signal(false);
}
67 changes: 67 additions & 0 deletions src/cdk-experimental/deferred-content/deferred-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @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,
Directive,
effect,
inject,
input,
TemplateRef,
signal,
ViewContainerRef,
} from '@angular/core';

/**
* A container directive controls the visibility of its content.
*/
@Directive()
export class DeferredContentAware {
contentVisible = signal(false);
readonly preserveContent = input(false);
}

/**
* DeferredContent loads/unloads the content based on the visibility.
* The visibilty signal is sent from a parent directive implements
* DeferredContentAware.
*
* Use this directive as a host directive. For example:
*
* ```ts
* @Directive({
* selector: 'ng-template[cdkAccordionContent]',
* hostDirectives: [DeferredContent],
* })
* class CdkAccordionContent {}
* ```
*/
@Directive()
export class DeferredContent {
private readonly _deferredContentAware = inject(DeferredContentAware);
private readonly _templateRef = inject(TemplateRef);
private readonly _viewContainerRef = inject(ViewContainerRef);
private _isRendered = false;

constructor() {
effect(() => {
if (
this._deferredContentAware.preserveContent() ||
this._deferredContentAware.contentVisible()
) {
if (this._isRendered) return;
this._viewContainerRef.clear();
this._viewContainerRef.createEmbeddedView(this._templateRef);
this._isRendered = true;
} else {
this._viewContainerRef.clear();
this._isRendered = false;
}
});
}
}
9 changes: 9 additions & 0 deletions src/cdk-experimental/deferred-content/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';
9 changes: 9 additions & 0 deletions src/cdk-experimental/deferred-content/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 {DeferredContentAware, DeferredContent} from './deferred-content';
17 changes: 17 additions & 0 deletions src/cdk-experimental/tabs/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("//tools:defaults.bzl", "ng_module")

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

ng_module(
name = "tabs",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
deps = [
"//src/cdk-experimental/deferred-content",
"//src/cdk-experimental/ui-patterns",
"//src/cdk/a11y",
"//src/cdk/bidi",
],
)
9 changes: 9 additions & 0 deletions src/cdk-experimental/tabs/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';
9 changes: 9 additions & 0 deletions src/cdk-experimental/tabs/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 {CdkTabs, CdkTablist, CdkTab, CdkTabpanel, CdkTabcontent} from './tabs';
Loading
Loading