diff --git a/src/app/shared/doc-viewer/deprecated-tooltip.ts b/src/app/shared/doc-viewer/deprecated-tooltip.ts new file mode 100644 index 00000000..78cafe38 --- /dev/null +++ b/src/app/shared/doc-viewer/deprecated-tooltip.ts @@ -0,0 +1,36 @@ +import {Component} from '@angular/core'; +import {MatTooltipModule} from '@angular/material/tooltip'; + +/** + * This component is responsible for showing the + * deprecated fields throughout API from material repo, + * + * When deprecated docs content is generated like: + * + *
+ * Deprecated + *
+ * + * It uses `title` attribute to show information regarding + * deprecation and other information regarding deprecation + * isnt shown either. + * + * We are gonna use this component to show deprecation + * information using the `material/tooltip`, the information + * would contain when the field is being deprecated and what + * are the alternatives to it which both are extracted from + * `breaking-change` and `deprecated`. + */ +@Component({ + selector: 'deprecated-field', + template: `
+
`, + standalone: true, + imports: [MatTooltipModule], +}) +export class DeprecatedFieldComponent { + /** Message regarding the deprecation */ + message = ''; +} diff --git a/src/app/shared/doc-viewer/doc-viewer-module.ts b/src/app/shared/doc-viewer/doc-viewer-module.ts index 9c3f51c0..36f5a9ad 100644 --- a/src/app/shared/doc-viewer/doc-viewer-module.ts +++ b/src/app/shared/doc-viewer/doc-viewer-module.ts @@ -9,6 +9,7 @@ import {PortalModule} from '@angular/cdk/portal'; import {NgModule} from '@angular/core'; import {HeaderLink} from './header-link'; import {CodeSnippet} from '../example-viewer/code-snippet'; +import {DeprecatedFieldComponent} from './deprecated-tooltip'; // ExampleViewer is included in the DocViewerModule because they have a circular dependency. @@ -23,8 +24,9 @@ import {CodeSnippet} from '../example-viewer/code-snippet'; DocViewer, ExampleViewer, HeaderLink, - CodeSnippet + CodeSnippet, + DeprecatedFieldComponent ], - exports: [DocViewer, ExampleViewer, HeaderLink] + exports: [DocViewer, ExampleViewer, HeaderLink, DeprecatedFieldComponent] }) export class DocViewerModule { } diff --git a/src/app/shared/doc-viewer/doc-viewer.spec.ts b/src/app/shared/doc-viewer/doc-viewer.spec.ts index c8b0c290..6878aca9 100644 --- a/src/app/shared/doc-viewer/doc-viewer.spec.ts +++ b/src/app/shared/doc-viewer/doc-viewer.spec.ts @@ -6,7 +6,7 @@ import {DocsAppTestingModule} from '../../testing/testing-module'; import {DocViewer} from './doc-viewer'; import {DocViewerModule} from './doc-viewer-module'; import {ExampleViewer} from '../example-viewer/example-viewer'; - +import {MatTooltip} from '@angular/material/tooltip'; describe('DocViewer', () => { let http: HttpTestingController; @@ -149,6 +149,36 @@ describe('DocViewer', () => { expect(console.error).toHaveBeenCalledTimes(1); }); + it('should show tooltip for deprecated symbol', () => { + const fixture = TestBed.createComponent(DocViewerTestComponent); + fixture.componentInstance.documentUrl = `http://material.angular.io/deprecated.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(); + + // we have five deprecated symbols: class, constant, type alias, interface + // and properties. + expect(docViewer.children.length).toBe(5); + + // it should have "Deprecated" as its inner text + const deprecatedSymbol = docViewer.children.shift()!; + expect(deprecatedSymbol.nativeElement.innerText).toBe('Deprecated'); + + // should contain the tooltip component + const tooltipElement = deprecatedSymbol.children.shift()!; + expect(tooltipElement.nativeElement).toBeTruthy(); + + // should show tooltip on hovering the element + tooltipElement.nativeNode.dispatchEvent(new MouseEvent('hover')); + fixture.detectChanges(); + expect(deprecatedSymbol.query(By.directive(MatTooltip))).toBeTruthy(); + }); + // TODO(mmalerba): Add test that example-viewer is instantiated. }); @@ -177,6 +207,21 @@ const FAKE_DOCS: {[key: string]: string} = { '
', 'http://material.angular.io/whole-snippet-example.html': '
', + 'http://material.angular.io/deprecated.html': + `
Deprecated
+ +
Deprecated
+ +
Deprecated
+ +
Deprecated
+ +
Deprecated
`, /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/app/shared/doc-viewer/doc-viewer.ts b/src/app/shared/doc-viewer/doc-viewer.ts index a4e23d64..a4f63327 100644 --- a/src/app/shared/doc-viewer/doc-viewer.ts +++ b/src/app/shared/doc-viewer/doc-viewer.ts @@ -27,6 +27,7 @@ import {Observable, Subscription} from 'rxjs'; import {shareReplay, take, tap} from 'rxjs/operators'; import {ExampleViewer} from '../example-viewer/example-viewer'; import {HeaderLink} from './header-link'; +import {DeprecatedFieldComponent} from './deprecated-tooltip'; @Injectable({providedIn: 'root'}) class DocFetcher { @@ -144,6 +145,9 @@ export class DocViewer implements OnDestroy { this._loadComponents('material-docs-example', ExampleViewer); this._loadComponents('header-link', HeaderLink); + // Create tooltips for the deprecated fields + this._createTooltipsForDeprecated(); + // Resolving and creating components dynamically in Angular happens synchronously, but since // we want to emit the output if the components are actually rendered completely, we wait // until the Angular zone becomes stable. @@ -189,4 +193,38 @@ export class DocViewer implements OnDestroy { this._clearLiveExamples(); this._documentFetchSubscription?.unsubscribe(); } + + _createTooltipsForDeprecated() { + // all of the deprecated symbols end with `deprecated-marker` + // class name on their element. + // for example: + //
Deprecated
, + // these can vary for each deprecated symbols such for class, interface, + // type alias, constants or properties: + // .docs-api-class-interface-marker, docs-api-type-alias-deprecated-marker + // .docs-api-constant-deprecated-marker, .some-more + // so instead of manually writing each deprecated class, we just query + // elements that ends with `deprecated-marker` in their class name. + const deprecatedElements = + this._elementRef.nativeElement.querySelectorAll(`[class$=deprecated-marker]`); + + [...deprecatedElements].forEach((element: Element) => { + // the deprecation message, it will include alternative to deprecated item + // and breaking change if there is one included. + const deprecationTitle = element.getAttribute('deprecated-message'); + + const elementPortalOutlet = new DomPortalOutlet( + element, this._componentFactoryResolver, this._appRef, this._injector); + + const tooltipPortal = new ComponentPortal(DeprecatedFieldComponent, this._viewContainerRef); + const tooltipOutlet = elementPortalOutlet.attach(tooltipPortal); + + + if (deprecationTitle) { + tooltipOutlet.instance.message = deprecationTitle; + } + + this._portalHosts.push(elementPortalOutlet); + }); + } } diff --git a/src/styles/_api.scss b/src/styles/_api.scss index 6d59e384..a74bbf65 100644 --- a/src/styles/_api.scss +++ b/src/styles/_api.scss @@ -131,8 +131,19 @@ .docs-api-interface-deprecated-marker { display: inline-block; font-weight: bold; - - &[title] { + position: relative; + + // We want to set width and height according to our parent + // deprecated marker element because the component that presents + // the tooltip for depcreated message is empty by default and + // empty element can not be able to show up therefore the tooltip + // wont show either. This makes sure that our tooltip component + // is aligned with deprecated marker in position and size. + & .deprecated-content { + position: absolute; + width: 100%; + height: 100%; + top: 0; border-bottom: 1px dotted grey; cursor: help; }