docs(docs-infra): allow collapse code example (#63559)

PR Close #63559
This commit is contained in:
Cheng-Hsuan Tsai 2025-09-03 00:36:17 +00:00 committed by Andrew Scott
parent 0b47781c31
commit 8401f8940f
11 changed files with 137 additions and 42 deletions

View file

@ -109,7 +109,10 @@ describe('DocViewer', () => {
expect(exampleViewer).not.toBeNull();
expect(exampleViewer.componentInstance.view()).toBe(CodeExampleViewMode.SNIPPET);
const checkIcon = fixture.debugElement.query(By.directive(IconComponent));
const copySourceCodeButton = fixture.debugElement.query(By.directive(CopySourceCodeButton));
expect(copySourceCodeButton).not.toBeNull();
const checkIcon = copySourceCodeButton.query(By.directive(IconComponent));
expect((checkIcon.nativeElement as HTMLElement).classList).toContain(
`material-symbols-outlined`,
);

View file

@ -188,6 +188,7 @@ export class DocViewer {
path: string,
): Promise<void> {
const preview = Boolean(placeholder.getAttribute('preview'));
const hideCode = Boolean(placeholder.getAttribute('hideCode'));
const title = placeholder.getAttribute('header') ?? undefined;
const firstCodeSnippetTitle =
snippets.length > 0 ? (snippets[0].title ?? snippets[0].name) : undefined;
@ -199,6 +200,7 @@ export class DocViewer {
path,
files: snippets,
preview,
hideCode,
id: this.countOfExamples,
});

View file

@ -1,18 +1,35 @@
<div class="docs-example-viewer" role="group">
<header class="docs-example-viewer-actions">
@if (view() === CodeExampleViewMode.SNIPPET) {
<span>{{ exampleMetadata()?.title }}</span>
}
@if (showCode()) {
@if (view() === CodeExampleViewMode.SNIPPET) {
<span>{{ exampleMetadata()?.title }}</span>
}
@if (view() === CodeExampleViewMode.MULTI_FILE) {
<mat-tab-group #codeTabs animationDuration="0ms" mat-stretch-tabs="false">
@for (tab of tabs(); track tab) {
<mat-tab [label]="tab.name" />
}
</mat-tab-group>
@if (view() === CodeExampleViewMode.MULTI_FILE) {
<mat-tab-group #codeTabs animationDuration="0ms" mat-stretch-tabs="false">
@for (tab of tabs(); track tab) {
<mat-tab [label]="tab.name" />
}
</mat-tab-group>
}
} @else {
<!-- Title placeholder -->
<span aria-hidden="true">&nbsp;</span>
}
<div class="docs-example-viewer-icons">
<button
type="button"
role="switch"
class="docs-example-code-toggle"
[attr.aria-checked]="showCode()"
[attr.aria-label]="showCode() ? 'Hide code' : 'Show code'"
(click)="showCode.set(!showCode())"
[matTooltip]="showCode() ? 'Hide code' : 'Show code'"
matTooltipPosition="above"
>
<docs-icon>{{showCode() ? 'code_off' : 'code'}}</docs-icon>
</button>
<button
type="button"
class="docs-example-copy-link"
@ -80,16 +97,18 @@
</div>
</header>
<div
class="docs-example-viewer-code-wrapper"
[class.docs-example-viewer-snippet]="view() === CodeExampleViewMode.SNIPPET"
[class.docs-example-viewer-multi-file]="view() === CodeExampleViewMode.MULTI_FILE"
>
<button docs-copy-source-code></button>
@if (snippetCode()?.sanitizedContent; as content) {
<div [innerHTML]="content"></div>
}
</div>
@if (showCode()) {
<div
class="docs-example-viewer-code-wrapper"
[class.docs-example-viewer-snippet]="view() === CodeExampleViewMode.SNIPPET"
[class.docs-example-viewer-multi-file]="view() === CodeExampleViewMode.MULTI_FILE"
>
<button docs-copy-source-code></button>
@if (snippetCode()?.sanitizedContent; as content) {
<div [innerHTML]="content"></div>
}
</div>
}
@if (exampleComponent) {
<div class="docs-example-viewer-preview">

View file

@ -57,25 +57,25 @@
display: flex;
gap: 0.75rem;
svg {
fill: var(--gray-400);
}
}
a,
button {
padding: 0;
margin: 0;
cursor: pointer;
height: 24px;
width: 24px;
color: var(--gray-400);
a,
button {
padding: 0;
margin: 0;
cursor: pointer;
height: 24px;
width: 24px;
path {
transition: fill 0.3s ease;
}
path {
transition: fill 0.3s ease;
}
&:hover {
svg {
fill: var(--tertiary-contrast);
fill: currentColor;
}
&:hover {
color: var(--tertiary-contrast);
}
}
}

View file

@ -272,6 +272,50 @@ describe('ExampleViewer', () => {
button.click();
expect(spy.calls.argsFor(0)[0].trim()).toBe(`${window.location.href}#example-1`);
});
it('should hide code content when `hideCode` is true', async () => {
componentRef.setInput(
'metadata',
getMetadata({
hideCode: true,
}),
);
await component.renderExample();
fixture.detectChanges();
// Initially, the code should be hidden.
expect(component.showCode()).toBeFalse();
let codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
expect(codeContainer).toBeNull();
});
it('should expand/collapse code content with toggle button.', async () => {
componentRef.setInput('metadata', getMetadata());
await component.renderExample();
fixture.detectChanges();
// Initially, the code should be visible.
expect(component.showCode()).toBeTrue();
let codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
expect(codeContainer).not.toBeNull();
const codeToggleButton = fixture.debugElement.query(By.css('.docs-example-code-toggle'));
codeToggleButton.nativeElement.click();
fixture.detectChanges();
expect(component.showCode()).toBeFalse();
codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
expect(codeContainer).toBeNull();
codeToggleButton.nativeElement.click();
fixture.detectChanges();
expect(component.showCode()).toBeTrue();
codeContainer = fixture.debugElement.query(By.css('.docs-example-viewer-code-wrapper'));
expect(codeContainer).not.toBeNull();
});
});
const getMetadata = (value: Partial<ExampleMetadata> = {}): ExampleMetadata => {
@ -282,6 +326,7 @@ const getMetadata = (value: Partial<ExampleMetadata> = {}): ExampleMetadata => {
{name: 'example.css', sanitizedContent: ''},
],
preview: false,
hideCode: false,
...value,
};
};

View file

@ -25,6 +25,7 @@ import {CommonModule, DOCUMENT} from '@angular/common';
import {MatTabGroup, MatTabsModule} from '@angular/material/tabs';
import {Clipboard} from '@angular/cdk/clipboard';
import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component';
import {IconComponent} from '../../icon/icon.component';
import {ExampleMetadata, Snippet} from '../../../interfaces/index';
import {EXAMPLE_VIEWER_CONTENT_LOADER} from '../../../providers/index';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
@ -42,7 +43,7 @@ export const HIDDEN_CLASS_NAME = 'hidden';
@Component({
selector: 'docs-example-viewer',
imports: [CommonModule, CopySourceCodeButton, MatTabsModule, MatTooltipModule],
imports: [CommonModule, CopySourceCodeButton, MatTabsModule, MatTooltipModule, IconComponent],
templateUrl: './example-viewer.component.html',
styleUrls: ['./example-viewer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
@ -75,6 +76,7 @@ export class ExampleViewer {
readonly expandable = signal<boolean>(false);
readonly expanded = signal<boolean>(false);
readonly snippetCode = signal<Snippet | undefined>(undefined);
readonly showCode = signal<boolean>(true);
readonly tabs = computed(() =>
this.exampleMetadata()?.files.map((file) => ({
name:
@ -98,6 +100,10 @@ export class ExampleViewer {
this.snippetCode.set(this.exampleMetadata()?.files[0]);
if (this.exampleMetadata()?.hideCode) {
this.showCode.set(false);
}
afterNextRender(
() => {
// Several function below query the DOM directly, we need to wait until the DOM is rendered.

View file

@ -38,4 +38,6 @@ export interface ExampleMetadata {
files: Snippet[];
/** True when ExampleViewer should have preview */
preview: boolean;
/** Whether to hide code example by default. */
hideCode: boolean;
}

View file

@ -20,6 +20,8 @@ export interface DocsCodeMultifileToken extends Tokens.Generic {
paneTokens: Token[];
// True if we should display preview
preview: boolean;
/** Whether to hide code example by default. */
hideCode: boolean;
}
// Capture group 1: all attributes on the opening tag
@ -28,6 +30,7 @@ const multiFileCodeRule = /^\s*<docs-code-multifile(.*?)>(.*?)<\/docs-code-multi
const pathRule = /path="([^"]*)"/;
const previewRule = /preview/;
const hideCodeRule = /hideCode/;
export const docsCodeMultifileExtension = {
name: 'docs-code-multifile',
@ -42,6 +45,7 @@ export const docsCodeMultifileExtension = {
const attr = match[1].trim();
const path = pathRule.exec(attr);
const preview = previewRule.exec(attr) ? true : false;
const hideCode = hideCodeRule.exec(attr) ? true : false;
const token: DocsCodeMultifileToken = {
type: 'docs-code-multifile',
@ -50,6 +54,7 @@ export const docsCodeMultifileExtension = {
panes: match[2].trim(),
paneTokens: [],
preview: preview,
hideCode,
};
this.lexer.blockTokens(token.panes, token.paneTokens);
return token;
@ -69,6 +74,9 @@ export const docsCodeMultifileExtension = {
if (token.preview) {
el.setAttribute('preview', `${token.preview}`);
}
if (token.hideCode) {
el.setAttribute('hideCode', 'true');
}
return el.outerHTML;
},

View file

@ -32,6 +32,7 @@ const languageRule = /language="([^"]*)"/;
const visibleLinesRule = /visibleLines="([^"]*)"/;
const visibleRegionRule = /visibleRegion="([^"]*)"/;
const previewRule = /preview/;
const hideCodeRule = /hideCode/;
export const docsCodeExtension = {
name: 'docs-code',
@ -53,6 +54,7 @@ export const docsCodeExtension = {
const visibleLines = visibleLinesRule.exec(attr);
const visibleRegion = visibleRegionRule.exec(attr);
const preview = previewRule.exec(attr) ? true : false;
const hideCode = hideCodeRule.exec(attr) ? true : false;
const classes = classRule.exec(attr);
let code = match[2]?.trim() ?? '';
@ -76,6 +78,7 @@ export const docsCodeExtension = {
visibleLines: visibleLines?.[1],
visibleRegion: visibleRegion?.[1],
preview: preview,
hideCode,
classes: classes?.[1]?.split(' '),
};
return token;

View file

@ -36,6 +36,8 @@ export interface CodeToken extends Tokens.Generic {
visibleRegion?: string;
/* Whether we should display preview */
preview?: boolean;
/** Whether to hide code example by default. */
hideCode?: boolean;
/* The lines to display highlighting on */
highlight?: string;
@ -121,6 +123,9 @@ function applyContainerAttributesAndClasses(el: Element, token: CodeToken) {
if (token.preview) {
el.setAttribute('preview', 'true');
}
if (token.hideCode) {
el.setAttribute('hideCode', 'true');
}
if (token.language === 'mermaid') {
el.setAttribute('mermaid', 'true');
}

View file

@ -175,6 +175,7 @@ We also have styling for the terminal, just set the language as `shell`:
| `visibleLines` | `string of number[]` | range of lines for collapse mode |
| `visibleRegion` | `string` | **DEPRECATED** FOR `visibleLines` |
| `preview` | `boolean` | (False) display preview |
| `hideCode` | `boolean` | (False) Whether to collapse code example by default. |
### Multifile examples
@ -198,11 +199,12 @@ You can create multifile examples by wrapping the examples inside a `<docs-code-
#### `<docs-code-multifile>` Attributes
| Attributes | Type | Details |
|:--- |:--- |:--- |
| body contents | `string` | nested tabs of `docs-code` examples |
| `path` | `string` | Path to code example for preview and external link |
| `preview` | `boolean` | (False) display preview |
| Attributes | Type | Details |
|:--- |:--- |:--- |
| body contents | `string` | nested tabs of `docs-code` examples |
| `path` | `string` | Path to code example for preview and external link |
| `preview` | `boolean` | (False) display preview |
| `hideCode` | `boolean` | (False) Whether to collapse code example by default. |
### Adding `preview` to your code example