Skip to content
Open
Show file tree
Hide file tree
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
44 changes: 22 additions & 22 deletions docs/src/app/pages/component-viewer/component-api.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
@if (componentViewer.componentDocItem | async; as docItem) {
<span class="cdk-visually-hidden" tabindex="-1">
API for {{docItem.id}}
</span>
@let docItem = componentViewer.docItem();

<!--
This container is necessary for the layout to display properly. When
a component has multiple API docs, they need to be joined together in the
same container so that they display one after another.
-->
<div class="docs-component-api">
<doc-viewer [document]="getApiDocumentUrl(docItem)"
class="docs-component-view-text-content"
(contentRendered)="updateTableOfContents(docItem.name, $event)" />
<span class="cdk-visually-hidden" tabindex="-1">
API for {{docItem.id}}
</span>

@for (additionalApiDoc of docItem.additionalApiDocs; track additionalApiDoc) {
<doc-viewer
document="/docs-content/api-docs/{{additionalApiDoc.path}}"
class="docs-component-view-text-content"
(contentRendered)="updateTableOfContents(additionalApiDoc.name, $event, $index + 1)"/>
}
</div>
<!--
This container is necessary for the layout to display properly. When
a component has multiple API docs, they need to be joined together in the
same container so that they display one after another.
-->
<div class="docs-component-api">
<doc-viewer [document]="getApiDocumentUrl(docItem)"
class="docs-component-view-text-content"
(contentRendered)="updateTableOfContents(docItem.name, $event)" />

@if (showToc | async) {
<table-of-contents #toc container=".mat-drawer-content"/>
@for (additionalApiDoc of docItem.additionalApiDocs; track additionalApiDoc) {
<doc-viewer
document="/docs-content/api-docs/{{additionalApiDoc.path}}"
class="docs-component-view-text-content"
(contentRendered)="updateTableOfContents(additionalApiDoc.name, $event, $index + 1)"/>
}
</div>

@if (showToc()) {
<table-of-contents #toc container=".mat-drawer-content"/>
}
108 changes: 108 additions & 0 deletions docs/src/app/pages/component-viewer/component-base-view.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {Component} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {provideHttpClient} from '@angular/common/http';
import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
import {BreakpointObserver, BreakpointState} from '@angular/cdk/layout';
import {provideRouter, RouterOutlet, withComponentInputBinding} from '@angular/router';
import {RouterTestingHarness} from '@angular/router/testing';
import {BehaviorSubject} from 'rxjs';
import {map} from 'rxjs/operators';
import {ComponentOverview, ComponentViewer} from './component-viewer';
import {docItemResolver} from './doc-item-resolver';
import {TableOfContents} from '../../shared/table-of-contents/table-of-contents';

/** Stand-in for the sidenav, which sits between the `:section` and `:id` routes. */
@Component({template: '<router-outlet />', imports: [RouterOutlet]})
class Shell {}

describe('ComponentBaseView', () => {
let harness: RouterTestingHarness;
let httpTestingController: HttpTestingController;

/** Whether the screen is small enough for the table of contents to be hidden. */
let isSmallScreen: BehaviorSubject<boolean>;

beforeEach(async () => {
isSmallScreen = new BehaviorSubject(false);

TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(),
{
provide: BreakpointObserver,
useValue: {
observe: () =>
isSmallScreen.pipe(map(matches => ({matches, breakpoints: {}}) as BreakpointState)),
},
},
provideRouter(
[
{
path: ':section',
children: [
{
path: '',
component: Shell,
children: [
{
path: ':id',
component: ComponentViewer,
resolve: {docItem: docItemResolver},
children: [{path: 'overview', component: ComponentOverview}],
},
],
},
],
},
],
withComponentInputBinding(),
),
],
});

harness = await RouterTestingHarness.create();
httpTestingController = TestBed.inject(HttpTestingController);
});

/** Navigates to a URL and waits for it to render. */
async function navigate(url: string) {
await harness.navigateByUrl(url);
await harness.fixture.whenStable();
harness.detectChanges();
}

/** Responds to the request for a component's overview document. */
function flushOverview(id: string) {
httpTestingController
.expectOne(`/docs-content/overviews/material/${id}/${id}.md.html`)
.flush(`<h2 id="usage">Usage</h2>`);
}

describe('overview', () => {
it('should add the headers to the table of contents when it is shown again', async () => {
isSmallScreen.next(true);
await navigate('/components/button/overview');
flushOverview('button');
await harness.fixture.whenStable();

const addHeadersSpy = spyOn(TableOfContents.prototype, 'addHeaders').and.callThrough();
isSmallScreen.next(false);
harness.detectChanges();
await harness.fixture.whenStable();

expect(addHeadersSpy).toHaveBeenCalledWith('Overview Content', jasmine.any(HTMLElement), 0);
});

it('should reset the table of contents when the doc item changes', async () => {
await navigate('/components/button/overview');
flushOverview('button');
await harness.fixture.whenStable();

const resetHeadersSpy = spyOn(TableOfContents.prototype, 'resetHeaders').and.callThrough();
await navigate('/components/checkbox/overview');

expect(resetHeadersSpy).toHaveBeenCalled();
});
});
});
20 changes: 10 additions & 10 deletions docs/src/app/pages/component-viewer/component-examples.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
@if (componentViewer.componentDocItem | async; as docItem) {
<span class="cdk-visually-hidden" tabindex="-1">
Examples for {{docItem.id}}
</span>
@let docItem = componentViewer.docItem();

@for (example of docItem.examples; track example) {
<example-viewer
[example]="example"
[showCompactToggle]="false"
view="demo"/>
}
<span class="cdk-visually-hidden" tabindex="-1">
Examples for {{docItem.id}}
</span>

@for (example of docItem.examples; track example) {
<example-viewer
[example]="example"
[showCompactToggle]="false"
view="demo"/>
}
27 changes: 13 additions & 14 deletions docs/src/app/pages/component-viewer/component-overview.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
@if (componentViewer.componentDocItem | async; as docItem) {
<h2 class="cdk-visually-hidden" tabindex="-1">
Overview for {{docItem.id}}
</h2>
<doc-viewer
[name]="docItem.id"
[packageName]="docItem.packageName"
[document]="getOverviewDocumentUrl(docItem)"
class="docs-component-view-text-content docs-component-overview"
(contentRendered)="updateTableOfContents('Overview Content', $event)" />
@let docItem = componentViewer.docItem();

@if (showToc | async) {
<table-of-contents #toc container=".mat-drawer-content"/>
}
}
<h2 class="cdk-visually-hidden" tabindex="-1">
Overview for {{docItem.id}}
</h2>
<doc-viewer
[name]="docItem.id"
[packageName]="docItem.packageName"
[document]="getOverviewDocumentUrl(docItem)"
class="docs-component-view-text-content docs-component-overview"
(contentRendered)="updateTableOfContents('Overview Content', $event)" />

@if (showToc()) {
<table-of-contents #toc container=".mat-drawer-content"/>
}
71 changes: 34 additions & 37 deletions docs/src/app/pages/component-viewer/component-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,23 @@
*/

import {BreakpointObserver} from '@angular/cdk/layout';
import {AsyncPipe} from '@angular/common';
import {
ChangeDetectorRef,
Component,
Directive,
OnInit,
ViewEncapsulation,
afterRenderEffect,
computed,
effect,
input,
untracked,
viewChild,
viewChildren,
inject,
DestroyRef,
} from '@angular/core';
import {takeUntilDestroyed, toObservable} from '@angular/core/rxjs-interop';
import {toObservable, toSignal} from '@angular/core/rxjs-interop';
import {RouterLinkActive, RouterLink, RouterOutlet} from '@angular/router';
import {Observable} from 'rxjs';
import {map, skip} from 'rxjs/operators';
import {map} from 'rxjs/operators';
import {DocItem} from '../../shared/documentation-items/documentation-items';
import {TableOfContents} from '../../shared/table-of-contents/table-of-contents';

Expand Down Expand Up @@ -87,43 +85,42 @@ export class ComponentViewer {
* the table of contents headers.
*/
@Directive()
export class ComponentBaseView implements OnInit {
export class ComponentBaseView {
componentViewer = inject(ComponentViewer);
private _changeDetectorRef = inject(ChangeDetectorRef);
private readonly _destroyRef = inject(DestroyRef);

readonly tableOfContents = viewChild<TableOfContents>('toc');
readonly viewers = viewChildren(DocViewer);

showToc: Observable<boolean>;
/** Whether the table of contents should be shown. It's hidden on small screens. */
readonly showToc = toSignal(
inject(BreakpointObserver)
.observe('(max-width: 1200px)')
.pipe(map(result => !result.matches)),
{requireSync: true},
);

constructor() {
const breakpointObserver = inject(BreakpointObserver);

this.showToc = breakpointObserver.observe('(max-width: 1200px)').pipe(
map(result => {
this._changeDetectorRef.detectChanges();
return !result.matches;
}),
);
}
// Reset the table of contents when the doc item changes.
effect(() => {
this.componentViewer.docItem();
untracked(() => this.tableOfContents()?.resetHeaders());
});

ngOnInit() {
this.componentViewer.componentDocItem
.pipe(takeUntilDestroyed(this._destroyRef))
.subscribe(() => {
const tableOfContents = this.tableOfContents();
if (tableOfContents) {
tableOfContents.resetHeaders();
}
});

this.showToc.pipe(skip(1), takeUntilDestroyed(this._destroyRef)).subscribe(() => {
if (this.tableOfContents()) {
this.viewers().forEach(viewer => {
viewer.contentRendered.emit(viewer._elementRef.nativeElement);
});
// The table of contents is removed on small screens. When it's shown again, re-emit
// `contentRendered` for the documents that are already rendered so it picks up their headers.
let hadTableOfContents: boolean | undefined;
afterRenderEffect(() => {
const hasTableOfContents = !!this.tableOfContents();

if (hadTableOfContents === false && hasTableOfContents) {
untracked(() =>
this.viewers().forEach(viewer => {
viewer.contentRendered.emit(viewer._elementRef.nativeElement);
}),
);
}

hadTableOfContents = hasTableOfContents;
});
}

Expand All @@ -140,7 +137,7 @@ export class ComponentBaseView implements OnInit {
selector: 'component-overview',
templateUrl: './component-overview.html',
encapsulation: ViewEncapsulation.None,
imports: [DocViewer, TableOfContents, AsyncPipe],
imports: [DocViewer, TableOfContents],
})
export class ComponentOverview extends ComponentBaseView {
getOverviewDocumentUrl(doc: DocItem) {
Expand All @@ -159,7 +156,7 @@ export class ComponentOverview extends ComponentBaseView {
templateUrl: './component-api.html',
styleUrls: ['./component-api.scss'],
encapsulation: ViewEncapsulation.None,
imports: [DocViewer, TableOfContents, AsyncPipe],
imports: [DocViewer, TableOfContents],
})
export class ComponentApi extends ComponentBaseView {
getApiDocumentUrl(doc: DocItem) {
Expand All @@ -172,6 +169,6 @@ export class ComponentApi extends ComponentBaseView {
selector: 'component-examples',
templateUrl: './component-examples.html',
encapsulation: ViewEncapsulation.None,
imports: [ExampleViewer, AsyncPipe],
imports: [ExampleViewer],
})
export class ComponentExamples extends ComponentBaseView {}
Loading