diff --git a/docs/src/app/shared/doc-viewer/code-block-copy-button/code-block-copy-button.ts b/docs/src/app/shared/doc-viewer/code-block-copy-button/code-block-copy-button.ts new file mode 100644 index 000000000000..e2fac2de686d --- /dev/null +++ b/docs/src/app/shared/doc-viewer/code-block-copy-button/code-block-copy-button.ts @@ -0,0 +1,39 @@ +/** + * @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 {Component, inject} from '@angular/core'; +import {MatIconButton} from '@angular/material/button'; +import {MatIcon} from '@angular/material/icon'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {MatTooltip} from '@angular/material/tooltip'; +import {Clipboard} from '@angular/cdk/clipboard'; + +@Component({ + selector: 'code-block-copy-button', + imports: [MatIconButton, MatIcon, MatTooltip], + template: ` + + `, +}) +export class CodeBlockCopyButton { + private _clipboard = inject(Clipboard); + private _snackbar = inject(MatSnackBar); + + /** Code snippet that will be copied */ + code = ''; + + copy(): void { + const message = this._clipboard.copy(this.code) + ? 'Copied code snippet' + : 'Failed to copy code snippet'; + + this._snackbar.open(message, undefined, {duration: 2500}); + } +} diff --git a/docs/src/app/shared/doc-viewer/doc-viewer.spec.ts b/docs/src/app/shared/doc-viewer/doc-viewer.spec.ts index 2328ec7f85f5..a5951cab926b 100644 --- a/docs/src/app/shared/doc-viewer/doc-viewer.spec.ts +++ b/docs/src/app/shared/doc-viewer/doc-viewer.spec.ts @@ -211,6 +211,28 @@ describe('DocViewer', () => { expect(clipboardSpy.copy).toHaveBeenCalled(); }); + it('should show copy icon button for code blocks', () => { + const fixture = TestBed.createComponent(DocViewerTestComponent); + fixture.componentInstance.documentUrl = `http://material.angular.io/doc-with-code-block.html`; + fixture.detectChanges(); + + const url = fixture.componentInstance.documentUrl; + http.expectOne(url).flush(FAKE_DOCS[url]); + + const docViewer = fixture.debugElement.query(By.directive(DocViewer)); + expect(docViewer).not.toBeNull(); + + // Query all copy buttons within code blocks + const iconButtons = fixture.debugElement.queryAll(By.directive(MatIconButton)); + // At least one icon button for copying code should exist + expect(iconButtons.length).toBeGreaterThan(0); + + // Click on the first icon button to trigger copying the code + iconButtons[0].nativeNode.dispatchEvent(new MouseEvent('click')); + fixture.detectChanges(); + expect(clipboardSpy.copy).toHaveBeenCalledWith('const example = "test code";'); + }); + // TODO(mmalerba): Add test that example-viewer is instantiated. }); @@ -262,6 +284,10 @@ const FAKE_DOCS: {[key: string]: string} = { data-docs-api-module-import-button="import {MatIconModule} from '@angular/material/icon';"> `, + 'http://material.angular.io/doc-with-code-block.html': ` +
const example = "test code";
+ tags that contain elements (markdown code blocks)
+ const codeBlockElements = this._elementRef.nativeElement.querySelectorAll(
+ '.docs-markdown pre:has(code)',
+ );
+
+ [...codeBlockElements].forEach((element: HTMLElement) => {
+ // Extract the text content from the code block
+ const codeElement = element.querySelector('code');
+ const codeSnippet = codeElement?.textContent || '';
+
+ const elementPortalOutlet = new DomPortalOutlet(element, this._appRef, this._injector);
+ const codeBlockCopyButtonPortal = new ComponentPortal(
+ CodeBlockCopyButton,
+ this._viewContainerRef,
+ );
+ const codeBlockCopyButtonOutlet = elementPortalOutlet.attach(codeBlockCopyButtonPortal);
+
+ if (codeSnippet) {
+ codeBlockCopyButtonOutlet.instance.code = codeSnippet;
+ }
+
+ this._portalHosts.push(elementPortalOutlet);
+ });
+ }
}
diff --git a/docs/src/styles/_markdown.scss b/docs/src/styles/_markdown.scss
index 99c056889a22..16a3af7dd86a 100644
--- a/docs/src/styles/_markdown.scss
+++ b/docs/src/styles/_markdown.scss
@@ -72,15 +72,21 @@
overflow-x: auto;
padding: 20px;
white-space: pre-wrap;
-
border: solid 1px var(--mat-sys-outline-variant);
border-radius: 12px;
+ position: relative;
code {
background: transparent;
padding: 0;
font-size: 100%;
}
+
+ code-block-copy-button {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ }
}
code {