diff --git a/adev/shared-docs/components/table-of-contents/table-of-contents.component.html b/adev/shared-docs/components/table-of-contents/table-of-contents.component.html index f5b514649f1..e791dd207dd 100644 --- a/adev/shared-docs/components/table-of-contents/table-of-contents.component.html +++ b/adev/shared-docs/components/table-of-contents/table-of-contents.component.html @@ -6,26 +6,26 @@ @if (shouldDisplayScrollToTop()) { - + } diff --git a/adev/shared-docs/components/table-of-contents/table-of-contents.component.ts b/adev/shared-docs/components/table-of-contents/table-of-contents.component.ts index f18ff9fdf39..1cb0f0bcb25 100644 --- a/adev/shared-docs/components/table-of-contents/table-of-contents.component.ts +++ b/adev/shared-docs/components/table-of-contents/table-of-contents.component.ts @@ -14,7 +14,7 @@ import { computed, inject, } from '@angular/core'; -import {RouterLink} from '@angular/router'; +import {Location} from '@angular/common'; import {TableOfContentsLevel} from '../../interfaces/index'; import {TableOfContentsLoader} from '../../services/table-of-contents-loader.service'; import {TableOfContentsScrollSpy} from '../../services/table-of-contents-scroll-spy.service'; @@ -25,11 +25,12 @@ import {IconComponent} from '../icon/icon.component'; changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './table-of-contents.component.html', styleUrls: ['./table-of-contents.component.scss'], - imports: [RouterLink, IconComponent], + imports: [IconComponent], }) export class TableOfContents { // Element that contains the content from which the Table of Contents is built readonly contentSourceElement = input.required(); + readonly location = inject(Location); private readonly scrollSpy = inject(TableOfContentsScrollSpy); private readonly tableOfContentsLoader = inject(TableOfContentsLoader); diff --git a/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts b/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts index 2a7e4104390..a8537908cf5 100644 --- a/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts +++ b/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts @@ -343,7 +343,12 @@ export class DocViewer implements OnChanges { relativeUrl = hrefAttr; } - handleHrefClickEventWithRouter(e, this.router, relativeUrl); + // Unless this is a link to an element within the same page, use the Angular router. + // https://github.com/angular/angular/issues/30139 + const scrollToElementExists = relativeUrl.startsWith(this.location.path() + '#'); + if (!scrollToElementExists) { + handleHrefClickEventWithRouter(e, this.router, relativeUrl); + } }); }); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/styling/css-classes.ts b/adev/shared-docs/pipeline/api-gen/rendering/styling/css-classes.ts index 09a63768f6c..5bb2a5942dd 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/styling/css-classes.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/styling/css-classes.ts @@ -8,13 +8,13 @@ // TODO(jelbourn): all of these CSS classes should use the `docs-` prefix. +export const API_REFERENCE_CONTAINER = 'docs-api'; + export const PARAM_KEYWORD_CLASS_NAME = 'docs-param-keyword'; export const PARAM_GROUP_CLASS_NAME = 'docs-param-group'; -export const REFERENCE_HEADER = 'docs-reference-header'; export const REFERENCE_MEMBERS = 'docs-reference-members'; export const REFERENCE_DEPRECATED = 'docs-reference-deprecated'; -export const REFERENCE_MEMBERS_CONTAINER = 'docs-reference-members-container'; export const REFERENCE_MEMBER_CARD = 'docs-reference-member-card'; export const REFERENCE_MEMBER_CARD_HEADER = 'docs-reference-card-header'; export const REFERENCE_MEMBER_CARD_BODY = 'docs-reference-card-body'; @@ -24,3 +24,6 @@ export const HEADER_CLASS_NAME = 'docs-reference-header'; export const HEADER_ENTRY_CATEGORY = 'docs-reference-category'; export const HEADER_ENTRY_TITLE = 'docs-reference-title'; export const HEADER_ENTRY_LABEL = 'docs-api-item-label'; + +export const SECTION_CONTAINER = 'docs-reference-section'; +export const SECTION_HEADING = 'docs-reference-section-heading'; diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/class-member.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/class-member.tsx index 48d97137926..5635f56cb0a 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/class-member.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/class-member.tsx @@ -13,7 +13,7 @@ import { isPropertyEntry, isSetterEntry, } from '../entities/categorization'; -import {MemberEntryRenderable} from '../entities/renderables'; +import {MemberEntryRenderable, MethodEntryRenderable} from '../entities/renderables'; import { REFERENCE_MEMBER_CARD, REFERENCE_MEMBER_CARD_BODY, @@ -27,20 +27,24 @@ import {getFunctionMetadataRenderable} from '../transforms/function-transforms'; import {CodeSymbol} from './code-symbols'; export function ClassMember(props: {member: MemberEntryRenderable}) { + const member = props.member; + + const renderMethod = (method: MethodEntryRenderable) => { + const signature = method.signatures.length ? method.signatures : [method.implementation]; + return signature.map((sig) => { + const renderableMember = getFunctionMetadataRenderable(sig); + return ; + }); + }; + const body = (
- {isClassMethodEntry(props.member) ? ( - (props.member.signatures.length - ? props.member.signatures - : [props.member.implementation] - ).map((sig) => { - const renderableMember = getFunctionMetadataRenderable(sig); - return ; - }) - ) : props.member.htmlDescription || props.member.deprecationMessage ? ( + {isClassMethodEntry(member) ? ( + renderMethod(member) + ) : member.htmlDescription || member.deprecationMessage ? (
- - + +
) : ( <> @@ -48,23 +52,19 @@ export function ClassMember(props: {member: MemberEntryRenderable}) {
); - const memberName = props.member.name; - const returnType = getMemberType(props.member); + const memberName = member.name; + const returnType = getMemberType(member); return ( -
-
-
-

{memberName}

-
- {isClassMethodEntry(props.member) && props.member.signatures.length > 1 ? ( - {props.member.signatures.length} overloads - ) : returnType ? ( - - ) : ( - <> - )} -
-
+
+
+

{memberName}

+ {isClassMethodEntry(member) && member.signatures.length > 1 ? ( + {member.signatures.length} overloads + ) : returnType ? ( + + ) : ( + <> + )}
{body}
diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/class-reference.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/class-reference.tsx index 3723c0fdae0..185348a753e 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/class-reference.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/class-reference.tsx @@ -10,26 +10,26 @@ import {Fragment, h} from 'preact'; import {ClassEntryRenderable, DecoratorEntryRenderable} from '../entities/renderables'; import {ClassMemberList} from './class-member-list'; import {HeaderApi} from './header-api'; -import {REFERENCE_MEMBERS_CONTAINER} from '../styling/css-classes'; -import {TabDescription} from './tab-description'; -import {TabUsageNotes} from './tab-usage-notes'; -import {TabApi} from './tab-api'; +import {API_REFERENCE_CONTAINER, REFERENCE_MEMBERS} from '../styling/css-classes'; +import {SectionDescription} from './section-description'; +import {SectionUsageNotes} from './section-usage-notes'; +import {SectionApi} from './section-api'; /** Component to render a class API reference document. */ export function ClassReference(entry: ClassEntryRenderable | DecoratorEntryRenderable) { return ( -
+
- - - + {entry.members.length > 0 ? ( -
+
) : ( <> )} + +
); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/cli-card.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/cli-card.tsx index 3212bed0a30..d4cfb69392a 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/cli-card.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/cli-card.tsx @@ -9,17 +9,12 @@ import {Fragment, h} from 'preact'; import {CliCardRenderable} from '../entities/renderables'; import {DeprecatedLabel} from './deprecated-label'; -import { REFERENCE_MEMBER_CARD, REFERENCE_MEMBER_CARD_HEADER } from '../styling/css-classes'; +import {REFERENCE_MEMBER_CARD, REFERENCE_MEMBER_CARD_BODY} from '../styling/css-classes'; export function CliCard(props: {card: CliCardRenderable}) { return ( -
-
-
-

{props.card.type}

-
-
-
+
+
{props.card.items.map((item) => (
{item.deprecated ? : <>} diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/cli-reference.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/cli-reference.tsx index 8d6425ef235..30e7d1c2654 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/cli-reference.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/cli-reference.tsx @@ -6,55 +6,74 @@ * found in the LICENSE file at https://angular.dev/license */ -import { Fragment, h } from 'preact'; -import { CliCommandRenderable } from '../entities/renderables'; -import { REFERENCE_MEMBERS, REFERENCE_MEMBERS_CONTAINER } from '../styling/css-classes'; -import { CliCard } from './cli-card'; -import { HeaderCli } from './header-cli'; -import { RawHtml } from './raw-html'; +import {Fragment, h} from 'preact'; +import {CliCommandRenderable} from '../entities/renderables'; +import {REFERENCE_MEMBERS} from '../styling/css-classes'; +import {CliCard} from './cli-card'; +import {HeaderCli} from './header-cli'; +import {RawHtml} from './raw-html'; +import {SectionHeading} from './section-heading'; /** Component to render a CLI command reference document. */ export function CliCommandReference(entry: CliCommandRenderable) { return ( -
+
- {[entry.name, ...entry.aliases].map((command) => + {[entry.name, ...entry.aliases].map((command) => (
               
                 
ng {commandName(entry, command)} - {entry.argumentsLabel ? : <>} - {entry.hasOptions ? : <>} + {entry.argumentsLabel ? ( + + ) : ( + <> + )} + {entry.hasOptions ? ( + + ) : ( + <> + )}
+ ))} + + {entry.subcommands && entry.subcommands?.length > 0 ? ( + <> +

Sub-commands

+

This command has the following sub-commands

+ + + ) : ( + <> )} - - {entry.subcommands && entry.subcommands?.length > 0 ? <> -

Sub-commands

-

This command has the following sub-commands

- - : <>}
-
-
- {entry.cards.map((card) => )} -
+
+ {entry.cards.map((card) => ( + <> + + + + ))}
); } - function commandName(entry: CliCommandRenderable, command: string) { if (entry.parentCommand?.name) { return `${entry.parentCommand?.name} ${command}`; diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/constant-reference.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/constant-reference.tsx index b18e9ec2963..3819c4562b9 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/constant-reference.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/constant-reference.tsx @@ -9,18 +9,19 @@ import {h} from 'preact'; import {ConstantEntryRenderable} from '../entities/renderables'; import {HeaderApi} from './header-api'; -import {TabDescription} from './tab-description'; -import {TabUsageNotes} from './tab-usage-notes'; -import {TabApi} from './tab-api'; +import {SectionDescription} from './section-description'; +import {SectionUsageNotes} from './section-usage-notes'; +import {SectionApi} from './section-api'; +import {API_REFERENCE_CONTAINER} from '../styling/css-classes'; /** Component to render a constant API reference document. */ export function ConstantReference(entry: ConstantEntryRenderable) { return ( -
+
- - - + + +
); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/docs-reference.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/docs-reference.tsx index 96173978c94..f226ad1a7ae 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/docs-reference.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/docs-reference.tsx @@ -9,14 +9,15 @@ import {h} from 'preact'; import {DocEntryRenderable} from '../entities/renderables'; import {HeaderApi} from './header-api'; -import {TabDescription} from './tab-description'; +import {SectionDescription} from './section-description'; +import {API_REFERENCE_CONTAINER} from '../styling/css-classes'; /** Component to render a block or element API reference document. */ export function DocsReference(entry: DocEntryRenderable) { return ( -
+
- +
); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/enum-reference.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/enum-reference.tsx index 7772ada9fbe..6aa74e642d1 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/enum-reference.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/enum-reference.tsx @@ -9,29 +9,27 @@ import {h, Fragment} from 'preact'; import {EnumEntryRenderable, MemberEntryRenderable} from '../entities/renderables'; import {HeaderApi} from './header-api'; -import {TabDescription} from './tab-description'; -import {TabApi} from './tab-api'; -import {REFERENCE_MEMBERS, REFERENCE_MEMBERS_CONTAINER} from '../styling/css-classes'; +import {SectionDescription} from './section-description'; +import {SectionApi} from './section-api'; +import {API_REFERENCE_CONTAINER, REFERENCE_MEMBERS} from '../styling/css-classes'; import {ClassMember} from './class-member'; /** Component to render a enum API reference document. */ export function EnumReference(entry: EnumEntryRenderable) { return ( -
+
- - - { - entry.members.length > 0 - ? ( -
-
- {entry.members.map((member: MemberEntryRenderable) => ())} -
-
- ) - : (<>) - } + + {entry.members.length > 0 ? ( +
+ {entry.members.map((member: MemberEntryRenderable) => ( + + ))} +
+ ) : ( + <> + )} +
); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/function-reference.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/function-reference.tsx index 9c22f7a6b97..b02e13fee76 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/function-reference.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/function-reference.tsx @@ -6,20 +6,23 @@ * found in the LICENSE file at https://angular.dev/license */ -import {h} from 'preact'; -import {FunctionEntryRenderable, FunctionSignatureMetadataRenderable} from '../entities/renderables'; +import {h, Fragment} from 'preact'; import { + FunctionEntryRenderable, + FunctionSignatureMetadataRenderable, +} from '../entities/renderables'; +import { + API_REFERENCE_CONTAINER, REFERENCE_MEMBERS, - REFERENCE_MEMBERS_CONTAINER, REFERENCE_MEMBER_CARD, REFERENCE_MEMBER_CARD_BODY, REFERENCE_MEMBER_CARD_HEADER, } from '../styling/css-classes'; import {ClassMethodInfo} from './class-method-info'; import {HeaderApi} from './header-api'; -import {TabApi} from './tab-api'; -import {TabDescription} from './tab-description'; -import {TabUsageNotes} from './tab-usage-notes'; +import {SectionApi} from './section-api'; +import {SectionDescription} from './section-description'; +import {SectionUsageNotes} from './section-usage-notes'; import {HighlightTypeScript} from './highlight-ts'; import {printInitializerFunctionSignatureLine} from '../transforms/code-transforms'; import {getFunctionMetadataRenderable} from '../transforms/function-transforms'; @@ -32,8 +35,8 @@ export const signatureCard = ( printSignaturesAsHeader: boolean, ) => { return ( -
-
+
+
{printSignaturesAsHeader ? ( ) : ( -
+ <>

{name}

-
+ )}
@@ -67,25 +70,24 @@ export function FunctionReference(entry: FunctionEntryRenderable) { const printSignaturesAsHeader = entry.signatures.length > 1; return ( -
+
- - - -
-
- {entry.signatures.map((s, i) => - signatureCard( - s.name, - getFunctionMetadataRenderable(s, entry.moduleName), - { - id: `${s.name}_${i}`, - }, - printSignaturesAsHeader, - ), - )} -
+ +
+ {entry.signatures.map((s, i) => + signatureCard( + s.name, + getFunctionMetadataRenderable(s, entry.moduleName), + { + id: `${s.name}_${i}`, + }, + printSignaturesAsHeader, + ), + )}
+ + +
); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/initializer-api-function.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/initializer-api-function.tsx index 5657933283c..bf4ac4e5320 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/initializer-api-function.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/initializer-api-function.tsx @@ -9,9 +9,9 @@ import {h, JSX} from 'preact'; import {InitializerApiFunctionRenderable} from '../entities/renderables'; import {HeaderApi} from './header-api'; -import {TabApi} from './tab-api'; -import {TabUsageNotes} from './tab-usage-notes'; -import {REFERENCE_MEMBERS, REFERENCE_MEMBERS_CONTAINER} from '../styling/css-classes'; +import {SectionApi} from './section-api'; +import {SectionUsageNotes} from './section-usage-notes'; +import {API_REFERENCE_CONTAINER, REFERENCE_MEMBERS} from '../styling/css-classes'; import {getFunctionMetadataRenderable} from '../transforms/function-transforms'; import {signatureCard} from './function-reference'; @@ -34,42 +34,41 @@ export function InitializerApiFunction(entry: InitializerApiFunctionRenderable) } return ( -
+
- - + -
-
- {entry.callFunction.signatures.map((s, i) => - signatureCard( - s.name, - getFunctionMetadataRenderable(s, entry.moduleName), - { - id: `${s.name}_${i}`, - }, - printSignaturesAsHeader, - ), - )} +
+ {entry.callFunction.signatures.map((s, i) => + signatureCard( + s.name, + getFunctionMetadataRenderable(s, entry.moduleName), + { + id: `${s.name}_${i}`, + }, + printSignaturesAsHeader, + ), + )} - {entry.subFunctions.reduce( - (elements, subFunction) => [ - ...elements, - ...subFunction.signatures.map((s, i) => - signatureCard( - `${entry.name}.${s.name}`, - getFunctionMetadataRenderable(s, entry.moduleName), - { - id: `${entry.name}_${s.name}_${i}`, - }, - printSignaturesAsHeader, - ), + {entry.subFunctions.reduce( + (elements, subFunction) => [ + ...elements, + ...subFunction.signatures.map((s, i) => + signatureCard( + `${entry.name}.${s.name}`, + getFunctionMetadataRenderable(s, entry.moduleName), + { + id: `${entry.name}_${s.name}_${i}`, + }, + printSignaturesAsHeader, ), - ], - [] as JSX.Element[], - )} -
+ ), + ], + [] as JSX.Element[], + )}
+ +
); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/section-api.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/section-api.tsx new file mode 100644 index 00000000000..3aebe12eb8c --- /dev/null +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/section-api.tsx @@ -0,0 +1,26 @@ +/*! + * @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 {h} from 'preact'; +import {DocEntryRenderable} from '../entities/renderables'; +import {HasRenderableToc} from '../entities/traits'; +import {CodeTableOfContents} from './code-table-of-contents'; +import {SECTION_CONTAINER} from '../styling/css-classes'; +import {SectionHeading} from './section-heading'; + +const API_SECTION_NAME = 'API'; + +/** Component to render the API section. */ +export function SectionApi(props: {entry: DocEntryRenderable & HasRenderableToc}) { + return ( +
+ + +
+ ); +} diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/section-description.tsx similarity index 73% rename from adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx rename to adev/shared-docs/pipeline/api-gen/rendering/templates/section-description.tsx index 4637a6b62fa..d2d741896ed 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-description.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/section-description.tsx @@ -8,14 +8,15 @@ import {Fragment, h} from 'preact'; import {DocEntryRenderable} from '../entities/renderables'; -import {normalizeTabUrl} from '../transforms/url-transforms'; import {RawHtml} from './raw-html'; import {CodeSymbol} from './code-symbols'; +import {SECTION_CONTAINER} from '../styling/css-classes'; +import {SectionHeading} from './section-heading'; -const DESCRIPTION_TAB_NAME = 'Description'; +const DESCRIPTION_SECTION_NAME = 'Description'; -/** Component to render the description tab. */ -export function TabDescription(props: {entry: DocEntryRenderable}) { +/** Component to render the description section. */ +export function SectionDescription(props: {entry: DocEntryRenderable}) { const exportedBy = props.entry.jsdocTags.filter((t) => t.name === 'ngModule'); if ( (!props.entry.htmlDescription || @@ -26,7 +27,8 @@ export function TabDescription(props: {entry: DocEntryRenderable}) { } return ( -
+
+ {exportedBy.length ? ( diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/section-heading.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/section-heading.tsx new file mode 100644 index 00000000000..2984b2a5eb4 --- /dev/null +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/section-heading.tsx @@ -0,0 +1,25 @@ +/*! + * @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 {h} from 'preact'; +import {convertSectionNameToId} from '../transforms/reference-section-id'; +import {SECTION_HEADING} from '../styling/css-classes'; + +/** Component to render the API section. */ +export function SectionHeading(props: {name: string}) { + const id = convertSectionNameToId(props.name); + const label = 'Link to ' + props.name + ' section'; + + return ( +

+ + {props.name} + +

+ ); +} diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-usage-notes.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/section-usage-notes.tsx similarity index 53% rename from adev/shared-docs/pipeline/api-gen/rendering/templates/tab-usage-notes.tsx rename to adev/shared-docs/pipeline/api-gen/rendering/templates/section-usage-notes.tsx index 56e20007a5f..b38470ea77a 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-usage-notes.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/section-usage-notes.tsx @@ -8,19 +8,21 @@ import {Fragment, h} from 'preact'; import {DocEntryRenderable} from '../entities/renderables'; -import {normalizeTabUrl} from '../transforms/url-transforms'; import {RawHtml} from './raw-html'; +import {SECTION_CONTAINER} from '../styling/css-classes'; +import {SectionHeading} from './section-heading'; -const USAGE_NOTES_TAB_NAME = 'Usage Notes'; +const USAGE_NOTES_SECTION_NAME = 'Usage Notes'; -/** Component to render the usage notes tab. */ -export function TabUsageNotes(props: {entry: DocEntryRenderable}) { +/** Component to render the usage notes section. */ +export function SectionUsageNotes(props: {entry: DocEntryRenderable}) { if (!props.entry.htmlUsageNotes) { - return (<>); + return <>; } return ( -
+
+
); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-api.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-api.tsx deleted file mode 100644 index 0f99d16b9c8..00000000000 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/tab-api.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/*! - * @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 {h} from 'preact'; -import {DocEntryRenderable} from '../entities/renderables'; -import {HasRenderableToc} from '../entities/traits'; -import {normalizeTabUrl} from '../transforms/url-transforms'; -import {CodeTableOfContents} from './code-table-of-contents'; - -const API_TAB_NAME = 'API'; - -/** Component to render the API tab. */ -export function TabApi(props: {entry: DocEntryRenderable & HasRenderableToc}) { - return ( -
-
- -
-
- ); -} diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/type-alias-reference.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/type-alias-reference.tsx index 47723de1a65..d4a70a3c68b 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/type-alias-reference.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/type-alias-reference.tsx @@ -9,18 +9,19 @@ import {h} from 'preact'; import {TypeAliasEntryRenderable} from '../entities/renderables'; import {HeaderApi} from './header-api'; -import {TabDescription} from './tab-description'; -import {TabUsageNotes} from './tab-usage-notes'; -import {TabApi} from './tab-api'; +import {SectionDescription} from './section-description'; +import {SectionUsageNotes} from './section-usage-notes'; +import {SectionApi} from './section-api'; +import {API_REFERENCE_CONTAINER} from '../styling/css-classes'; /** Component to render a type alias API reference document. */ export function TypeAliasReference(entry: TypeAliasEntryRenderable) { return ( -
+
- - - + + +
); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/reference-section-id.ts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/reference-section-id.ts new file mode 100644 index 00000000000..fdf0bb3f85a --- /dev/null +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/reference-section-id.ts @@ -0,0 +1,14 @@ +/*! + * @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 const convertSectionNameToId = (sectionName: string): string => { + return sectionName + .toLowerCase() + .replace(/\s|\//g, '-') // remove spaces and slashes + .replace(/[^0-9a-z\-]/g, ''); // only keep letters, digits & dashes +}; diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/url-transforms.ts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/url-transforms.ts index 4a504bf71fe..8489636733f 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/url-transforms.ts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/url-transforms.ts @@ -19,15 +19,3 @@ export const normalizePath = (path: string): string => { } return path; }; - -export const normalizeTabUrl = (tabName: string): string => { - return tabName - .toLowerCase() - .replace(/(.*?)<\/code>/g, '$1') // remove - .replace(/(.*?)<\/strong>/g, '$1') // remove - .replace(/(.*?)<\/em>/g, '$1') // remove - .replace(/\s|\//g, '-') // remove spaces and slashes - .replace(/gt;|lt;/g, '') // remove escaped < and > - .replace(/&#\d+;/g, '') // remove HTML entities - .replace(/[^0-9a-zA-Z\-]/g, ''); // only keep letters, digits & dashes -}; diff --git a/adev/shared-docs/pipeline/guides/extensions/docs-workflow/docs-step.ts b/adev/shared-docs/pipeline/guides/extensions/docs-workflow/docs-step.ts index 781fe7550df..36b1fcab641 100644 --- a/adev/shared-docs/pipeline/guides/extensions/docs-workflow/docs-step.ts +++ b/adev/shared-docs/pipeline/guides/extensions/docs-workflow/docs-step.ts @@ -7,7 +7,7 @@ */ import {Token, Tokens, RendererThis, TokenizerThis} from 'marked'; -import {formatHeading, headingRender} from '../../tranformations/heading'; +import {formatHeading} from '../../tranformations/heading'; interface DocsStepToken extends Tokens.Generic { type: 'docs-step'; diff --git a/adev/shared-docs/services/table-of-contents-loader.service.ts b/adev/shared-docs/services/table-of-contents-loader.service.ts index 40b76424ff6..281cc1c5c6f 100644 --- a/adev/shared-docs/services/table-of-contents-loader.service.ts +++ b/adev/shared-docs/services/table-of-contents-loader.service.ts @@ -49,9 +49,7 @@ export class TableOfContentsLoader { const updatedTopValues = new Map(); for (const heading of headings) { - const parentTop = heading.parentElement?.offsetTop ?? 0; - const top = Math.floor(parentTop + heading.offsetTop - this.toleranceThreshold); - updatedTopValues.set(heading.id, top); + updatedTopValues.set(heading.id, this.calculateTop(heading)); } this.tableOfContentItems.update((oldItems) => { diff --git a/adev/shared-docs/styles/_reference.scss b/adev/shared-docs/styles/_reference.scss new file mode 100644 index 00000000000..ca5360bd574 --- /dev/null +++ b/adev/shared-docs/styles/_reference.scss @@ -0,0 +1,410 @@ +@use './anchor' as anchor; + +/* Common styles for the API & CLI references */ +@mixin reference-common() { + .docs-code { + pre { + margin-block: 0; + } + } + + .docs-reference-header { + // deprecated markers beside header + & ~ .docs-deprecated { + margin-block-start: 0.5rem; + } + + & > p { + color: var(--secondary-contrast); + margin-block-start: 0; + margin-block-end: 1.5rem; + } + + .docs-reference-title { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding-block-end: 0; + gap: 0.5rem; + + > div { + margin-block: 0.67em; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + + h1 { + margin-block: 0; + } + } + + a { + fill: var(--quinary-contrast); + transition: fill 0.3s ease; + + &:hover { + fill: var(--primary-contrast); + } + } + } + + .docs-reference-category { + color: var(--gray-400); + font-size: 0.875rem; + font-weight: 500; + line-height: 1.4rem; + letter-spacing: -0.00875rem; + } + + .docs-code { + margin-block-end: 1.5rem; + } + } + + .docs-reference-section-heading { + padding-block-start: 3rem; + + a { + @include anchor.docs-anchor(); + color: inherit; + } + } + + .docs-reference-members { + box-sizing: border-box; + width: 100%; + display: flex; + flex-direction: column; + gap: 20px; + + &:not(:first-child) { + margin-top: 1rem; + } + + .docs-reference-member-card { + border: 1px solid var(--senary-contrast); + border-radius: 0.25rem; + position: relative; + transition: border 0.3s ease; + pointer-events: none; + + &::before { + content: ''; + inset: -1px; + position: absolute; + background: transparent; + border-radius: 0.35rem; + z-index: 0; + } + + &:focus { + box-shadow: 10px 4px 40px 0 rgba(0, 0, 0, 0.01); + + &::before { + background: var(--red-to-pink-to-purple-horizontal-gradient); + } + } + + > p { + padding-inline: 1.25rem; + margin-block-end: 0; + } + + a { + pointer-events: initial; + } + + .docs-reference-card-header { + display: flex; + align-items: center; + justify-content: space-between; + border-radius: 0.25rem 0.25rem 0 0; + background-color: var(--octonary-contrast); + position: relative; + z-index: 10; + padding: 0.7rem 1rem; + cursor: pointer; + gap: 0.5rem; + flex-wrap: wrap; + transition: + background-color 0.3s ease, + border 0.3s ease; + + &:focus { + outline: none; + } + + &:has(+ .docs-reference-card-body:empty) { + border-radius: 0.25rem; + } + + code { + font-size: 0.875rem; + + &:has(pre) { + padding: 0; + } + + &:not(pre *) { + padding: 0 0.3rem; + } + } + + pre { + margin: 0; + + /* Do we have a better alternative ? */ + overflow: auto; + } + + h3 { + display: inline-block; + font-family: var(--code-font); + font-size: 1rem; + letter-spacing: -0.025rem; + margin: 0; + } + + span { + font-size: 0.875rem; + } + } + + .docs-reference-card-body { + padding: 0.25rem 1.25rem; + background: var(--septenary-contrast); + transition: background-color 0.3s ease; + color: var(--quaternary-contrast); + border-radius: 0 0 0.25rem 0.25rem; + position: relative; + z-index: 10; + + &:empty { + display: none; + } + + &:first-child { + border-radius: 0.25rem; + } + + hr { + margin-block: 2rem; + } + + .docs-code { + margin-block-end: 1rem; + } + + .docs-deprecation-message { + border-block-end: 1px solid var(--senary-contrast); + + .docs-deprecated { + color: var(--page-background); + background-color: var(--quaternary-contrast); + width: max-content; + border-radius: 0.25rem; + padding: 0.1rem 0.25rem; + margin-block-start: 1rem; + } + } + } + } + } +} + +/* API reference styles */ +@mixin api-reference { + // API section styles + .docs-reference-api-section { + .docs-code { + box-sizing: border-box; + width: 100%; + overflow: hidden; + padding: 0; + + button { + transition: background-color 0.3s ease; + + &.shiki-ln-line-highlighted { + background-color: var(--senary-contrast); + } + + &:hover { + background-color: var(--septenary-contrast); + } + + &:focus { + background-color: var(--senary-contrast); + + span { + background-color: inherit; + } + } + } + + // Hide copy source code button + button[docs-copy-source-code] { + display: none; + } + } + + code { + margin-block: 0; + } + + pre { + white-space: pre; + overflow-x: auto; + margin: 0; + } + } + + // "API member card"-specific styles + .docs-reference-member-card { + .docs-reference-card-item { + // When it's not the only card ... + &:has(~ .docs-reference-card-item) { + border: 1px solid var(--senary-contrast); + margin-block: 1rem; + border-radius: 0.25rem; + padding-inline: 1rem; + } + + // & the last card + &:last-child:not(:first-of-type) { + border: 1px solid var(--senary-contrast); + margin-block: 1rem; + border-radius: 0.25rem; + padding-inline: 1rem; + } + + span { + display: inline-block; + font-size: 0.875rem; + } + + code { + font-size: 0.875rem; + } + + .docs-function-definition:has(*) { + border-block-end: 1px solid var(--senary-contrast); + } + + .docs-param-group { + margin-block-start: 1rem; + + // If it's the only param group... + &:not(:has(~ .docs-param-group)) { + margin-block: 1rem; + } + + .docs-param-name { + color: var(--vivid-pink); + font-family: var(--code-font); + margin-inline-end: 0.25rem; + + &::after { + content: ':'; + } + } + + .docs-parameter-description { + p:first-child { + margin-block-start: 0; + } + } + } + + .docs-param-keyword { + color: var(--primary-contrast); + font-family: var(--code-font); + margin-inline-end: 0.5rem; + } + + .docs-return-type { + padding-block: 1rem; + + // & does not follow a function definition + &:not(.docs-function-definition + .docs-return-type) { + border-block-start: 1px solid var(--senary-contrast); + } + } + } + } +} + +/* CLI reference styles */ +@mixin cli-reference { + // CLI TOC + .docs-reference-cli-toc { + margin-bottom: 1rem; + + .shiki-ln-line-argument, + .shiki-ln-line-option { + padding: 0.1rem 0.2rem 0.2rem; + margin-inline: 0.1rem; + color: var(--quaternary-contrast); + background: transparent; + border-radius: 0.25rem; + position: relative; + transition: + color 0.3s ease, + background 0.3s ease, + border 0.3s ease; + + &:hover { + color: var(--primary-contrast); + background: var(--septenary-contrast); + } + + &.shiki-ln-line-highlighted { + color: var(--primary-contrast); + background: var(--senary-contrast); + } + } + + .shiki-ln-line-argument { + margin-inline-start: 0.2rem; + } + } + + .docs-reference-members { + .docs-reference-section-heading { + margin: 0; + } + + // "CLI member card"-specific styles + .docs-reference-member-card { + .docs-ref-content { + padding: 1rem 0; + + &:not(:first-child) { + border-block-start: 1px solid var(--senary-contrast); + } + + .docs-reference-type-and-default { + width: 4.375rem; + flex-shrink: 0; + + span { + display: block; + font-size: 0.875rem; + margin-block-end: 0.2rem; + white-space: nowrap; + + &:not(:first-child) { + margin-block-start: 1rem; + } + } + + code { + font-size: 0.775rem; + } + } + } + } + } +} diff --git a/adev/src/app/features/references/api-items-section/api-items-section.component.html b/adev/src/app/features/references/api-items-section/api-items-section.component.html index 45c2a24f5b8..52578f6aecb 100644 --- a/adev/src/app/features/references/api-items-section/api-items-section.component.html +++ b/adev/src/app/features/references/api-items-section/api-items-section.component.html @@ -29,7 +29,7 @@ {{ apiItem.title }} @if (apiItem.isDeprecated) { - <!> + <!> } } diff --git a/adev/src/app/features/references/api-items-section/api-items-section.component.scss b/adev/src/app/features/references/api-items-section/api-items-section.component.scss index 6790c0c65f4..a13c428ce45 100644 --- a/adev/src/app/features/references/api-items-section/api-items-section.component.scss +++ b/adev/src/app/features/references/api-items-section/api-items-section.component.scss @@ -79,7 +79,7 @@ gap: 1em; } -.docs-deprecated { +.adev-deprecated { font-family: var(--code-font); background-color: var(--senary-contrast); color: var(--tertiary-contrast); diff --git a/adev/src/app/features/references/api-items-section/api-items-section.component.spec.ts b/adev/src/app/features/references/api-items-section/api-items-section.component.spec.ts index 7a79182ecfc..ddb4a9fba95 100644 --- a/adev/src/app/features/references/api-items-section/api-items-section.component.spec.ts +++ b/adev/src/app/features/references/api-items-section/api-items-section.component.spec.ts @@ -10,7 +10,6 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import ApiItemsSection from './api-items-section.component'; import {ApiItemsGroup} from '../interfaces/api-items-group'; -import {ApiReferenceManager} from '../api-reference-list/api-reference-manager.service'; import {ApiItemType} from '../interfaces/api-item-type'; import {provideRouter} from '@angular/router'; import {By} from '@angular/platform-browser'; @@ -60,7 +59,7 @@ describe('ApiItemsSection', () => { fixture.detectChanges(); const deprecatedApiIcons = fixture.debugElement.queryAll( - By.css('.adev-api-items-section-grid li .docs-deprecated'), + By.css('.adev-api-items-section-grid li .adev-deprecated'), ); const deprecatedApiTitle = deprecatedApiIcons[0].parent?.query(By.css('.adev-item-title')); diff --git a/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.html b/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.html index d61fcf0aa8c..8c5e47f8de6 100644 --- a/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.html +++ b/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.html @@ -1,30 +1,9 @@ -
- - - - @for (tab of tabs(); track tab.url) { - -
- -
-
- } -
-
- -@if (isApiTabActive()) { +@if (docContent(); as docContent) { - + [docContent]="docContent.contents" + [hasToc]="true" + (contentLoaded)="onContentLoaded()" + /> }
Jump to details
diff --git a/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.scss b/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.scss index a2ac3ac7802..3d475c60b4f 100644 --- a/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.scss +++ b/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.scss @@ -1,11 +1,23 @@ @use '@angular/docs/styles/media-queries' as mq; +@use '@angular/docs/styles/reference' as ref; :host { - display: flex; - gap: 1rem; + display: block; width: 100%; + max-width: var(--page-width); + padding: var(--layout-padding) 0 1rem var(--layout-padding); box-sizing: border-box; - flex-direction: column; + + @include mq.for-desktop-down { + padding: var(--layout-padding); + max-width: none; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--septenary-contrast); + border-radius: 10px; + transition: background-color 0.3s ease; + } h1 { font-size: 1.5rem; @@ -27,391 +39,7 @@ } } -// stylelint-disable-next-line ::ng-deep { - .adev-header-and-tabs { - padding: var(--layout-padding) 0 1rem var(--layout-padding); - box-sizing: border-box; - width: 100%; - max-width: var(--page-width); - - @include mq.for-desktop-down { - padding: var(--layout-padding); - max-width: none; - } - - &::-webkit-scrollbar-thumb { - background-color: var(--septenary-contrast); - border-radius: 10px; - transition: background-color 0.3s ease; - } - } - - .docs-code { - pre { - margin-block: 0; - } - } - - .docs-reference-header { - > p { - color: var(--secondary-contrast); - margin-block-start: 0; - margin-block-end: 1.5rem; - } - - .docs-code { - margin-block-end: 1.5rem; - } - } - - .adev-reference-tab-body { - margin-block-start: 1.5rem; - docs-viewer > div { - :first-child { - margin-top: 0; - } - } - } - - .docs-reference-api-tab { - display: flex; - gap: 1.81rem; - align-items: flex-start; - margin-bottom: 1px; - - @include mq.for-desktop-down { - flex-direction: column; - } - - & > .docs-code { - box-sizing: border-box; - width: 100%; - overflow: hidden; - padding: 0; - - @include mq.for-desktop-down { - width: 100%; - position: static; - } - - button { - transition: background-color 0.3s ease; - - &.shiki-ln-line-highlighted { - background-color: var(--senary-contrast); - } - &:hover { - background-color: var(--septenary-contrast); - } - &:focus { - background-color: var(--senary-contrast); - } - } - - // Hide copy source code button - button[docs-copy-source-code] { - display: none; - } - } - - code { - margin-block: 0; - } - - pre { - white-space: pre; - overflow-x: auto; - margin: 0; - } - } - - .docs-reference-cli-toc { - margin-bottom: 1rem; - } - - .adev-reference-tab { - min-width: 50ch; - margin-block-start: 2.5rem; - } - - .docs-reference-members-container { - width: 40%; - box-sizing: border-box; - width: 100%; - max-width: var(--page-width); - padding: 0 0 1rem var(--layout-padding); - - @include mq.for-desktop-down { - padding: var(--layout-padding); - padding-top: 0; - max-width: none; - } - } - - // Sidebar - .docs-reference-members { - display: flex; - flex-direction: column; - gap: 20px; - - @include mq.for-desktop-down { - width: 100%; - } - } - - .docs-reference-title { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - padding-block-end: 0; - gap: 0.5rem; - - > div { - margin-block: 0.67em; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.5rem; - - h1 { - margin-block: 0; - } - } - - a { - fill: var(--quinary-contrast); - transition: fill 0.3s ease; - - &:hover { - fill: var(--primary-contrast); - } - } - } - - .adev-reference-labels { - display: flex; - gap: 0.5rem; - } - - .docs-reference-category { - color: var(--gray-400); - font-size: 0.875rem; - font-weight: 500; - line-height: 1.4rem; - letter-spacing: -0.00875rem; - } - - .docs-reference-card-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - flex-wrap: wrap; - - padding: 0.7rem 1rem; - - code:not(pre *) { - padding: 0 0.3rem; - } - } - - .docs-reference-member-card { - border: 1px solid var(--senary-contrast); - border-radius: 0.25rem; - position: relative; - transition: border 0.3s ease; - - &::before { - content: ''; - inset: -1px; - position: absolute; - background: transparent; - border-radius: 0.35rem; - z-index: 0; - } - - &:focus { - box-shadow: 10px 4px 40px 0 rgba(0, 0, 0, 0.01); - - &::before { - background: var(--red-to-pink-to-purple-horizontal-gradient); - } - } - - header { - display: flex; - flex-direction: column; - border-radius: 0.25rem 0.25rem 0 0; - background-color: var(--octonary-contrast); - position: relative; - z-index: 10; - cursor: pointer; - transition: - background-color 0.3s ease, - border 0.3s ease; - - & > code { - max-width: 100%; - } - - code:has(pre) { - padding: 0; - } - - pre { - margin: 0; - - /* Do we have a better alternative ? */ - overflow: auto; - } - } - - .docs-reference-card-header { - h3 { - display: inline-block; - font-family: var(--code-font); - font-size: 1rem; - letter-spacing: -0.025rem; - margin: 0; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - } - - code, - span { - font-size: 0.875rem; - } - } - - > p { - padding-inline: 1.25rem; - margin-block-end: 0; - } - } - - .docs-reference-card-body { - padding: 0.25rem 1.25rem; - background: var(--septenary-contrast); - transition: background-color 0.3s ease; - color: var(--quaternary-contrast); - border-radius: 0 0 0.25rem 0.25rem; - position: relative; - z-index: 10; - hr { - margin-block: 2rem; - } - .docs-code { - margin-block-end: 1rem; - } - - &:empty { - display: none; - } - } - - // when it's not the only card... - .docs-reference-card-item:has(~ .docs-reference-card-item) { - border: 1px solid var(--senary-contrast); - margin-block: 1rem; - border-radius: 0.25rem; - padding-inline: 1rem; - } - // & the last card - .docs-reference-card-item:last-child { - &:not(:first-of-type) { - border: 1px solid var(--senary-contrast); - margin-block: 1rem; - border-radius: 0.25rem; - padding-inline: 1rem; - } - } - - .docs-reference-card-item { - span { - display: inline-block; - font-size: 0.875rem; - } - code { - font-size: 0.875rem; - } - } - - .docs-function-definition { - &:has(*) { - border-block-end: 1px solid var(--senary-contrast); - } - } - - .docs-deprecation-message { - border-block-end: 1px solid var(--senary-contrast); - } - - .docs-param-group { - margin-block-start: 1rem; - } - - // If it's the only param group... - .docs-param-group:not(:has(~ .docs-param-group)) { - margin-block: 1rem; - } - - .docs-return-type { - padding-block: 1rem; - - // & does not follow a function definition - &:not(.docs-function-definition + .docs-return-type) { - border-block-start: 1px solid var(--senary-contrast); - } - } - - .docs-param-keyword { - color: var(--primary-contrast); - font-family: var(--code-font); - margin-inline-end: 0.5rem; - } - - .docs-param-name { - color: var(--vivid-pink); - font-family: var(--code-font); - margin-inline-end: 0.25rem; - &::after { - content: ':'; - } - } - - .docs-deprecated { - color: var(--page-background); - background-color: var(--quaternary-contrast); - width: max-content; - border-radius: 0.25rem; - padding: 0.1rem 0.25rem; - margin-block-start: 1rem; - } - - // deprecated markers beside header - .docs-reference-header ~ .docs-deprecated { - margin-block-start: 0.5rem; - } - - .docs-parameter-description { - p:first-child { - margin-block-start: 0; - } - } - - .docs-ref-content { - padding: 1rem 0; - - &:not(:first-child) { - border-block-start: 1px solid var(--senary-contrast); - } - - .docs-param-keyword { - display: block; - margin: 0 0 0.5rem 0; - } - } + @include ref.reference-common(); + @include ref.api-reference(); } diff --git a/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.spec.ts b/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.spec.ts index 7bc5a6a40bc..18c9830c022 100644 --- a/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.spec.ts +++ b/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.spec.ts @@ -6,37 +6,29 @@ * found in the LICENSE file at https://angular.dev/license */ -import {HarnessLoader} from '@angular/cdk/testing'; -import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import {TestBed} from '@angular/core/testing'; -import {MatTabGroupHarness} from '@angular/material/tabs/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import {provideNoopAnimations} from '@angular/platform-browser/animations'; import {ReferenceScrollHandler} from '../services/reference-scroll-handler.service'; -import {signal} from '@angular/core'; import {provideRouter, withComponentInputBinding} from '@angular/router'; import {RouterTestingHarness} from '@angular/router/testing'; import ApiReferenceDetailsPage from './api-reference-details-page.component'; -import {By} from '@angular/platform-browser'; describe('ApiReferenceDetailsPage', () => { let component: ApiReferenceDetailsPage; - let loader: HarnessLoader; - let harness: RouterTestingHarness; + let fixture: ComponentFixture; let fakeApiReferenceScrollHandler = { setupListeners: () => {}, - membersMarginTopInPx: signal(10), - updateMembersMarginTop: () => {}, }; - const SAMPLE_CONTENT_WITH_TABS = `
-
-
-
-
-
-
`; + const SAMPLE_CONTENT_WITH_SECTIONS = `
+
API
+
+
Description
+
Examples
+
Usage Notes
+
`; beforeEach(async () => { TestBed.configureTestingModule({ @@ -51,7 +43,7 @@ describe('ApiReferenceDetailsPage', () => { data: { 'docContent': { id: 'id', - contents: SAMPLE_CONTENT_WITH_TABS, + contents: SAMPLE_CONTENT_WITH_SECTIONS, }, }, }, @@ -61,10 +53,9 @@ describe('ApiReferenceDetailsPage', () => { ], }); TestBed.overrideProvider(ReferenceScrollHandler, {useValue: fakeApiReferenceScrollHandler}); - harness = await RouterTestingHarness.create(); - const {fixture} = harness; + const harness = await RouterTestingHarness.create(); + fixture = harness.fixture; component = await harness.navigateByUrl('/', ApiReferenceDetailsPage); - loader = TestbedHarnessEnvironment.loader(fixture); fixture.detectChanges(); }); @@ -72,39 +63,10 @@ describe('ApiReferenceDetailsPage', () => { expect(component).toBeTruthy(); }); - it('should render tabs for all elements with tab attribute', async () => { - const matTabGroup = await loader.getHarness(MatTabGroupHarness); + it('should load the doc content', () => { + expect(component.docContent()?.contents).toBeTruthy(); - const tabs = await matTabGroup.getTabs(); - - expect(tabs.length).toBe(4); - }); - - it('should display members cards when API tab is active', async () => { - const matTabGroup = await loader.getHarness(MatTabGroupHarness); - const tabs = await matTabGroup.getTabs(); - - let membersCard = harness.fixture.debugElement.query( - By.css('.docs-reference-members-container'), - ); - expect(membersCard).toBeTruthy(); - - await matTabGroup.selectTab({label: await tabs[1].getLabel()}); - - membersCard = harness.fixture.debugElement.query(By.css('.docs-reference-members-container')); - expect(membersCard).toBeFalsy(); - - await matTabGroup.selectTab({label: await tabs[0].getLabel()}); - - membersCard = harness.fixture.debugElement.query(By.css('.docs-reference-members-container')); - expect(membersCard).toBeTruthy(); - }); - - it('should setup scroll listeners when API members are loaded', () => { - const setupListenersSpy = spyOn(fakeApiReferenceScrollHandler, 'setupListeners'); - - component.membersCardsLoaded(); - - expect(setupListenersSpy).toHaveBeenCalled(); + const docsViewer = fixture.nativeElement.querySelector('docs-viewer'); + expect(docsViewer).toBeTruthy(); }); }); diff --git a/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.ts b/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.ts index 6cd36d3d1a1..e37635f6290 100644 --- a/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.ts +++ b/adev/src/app/features/references/api-reference-details-page/api-reference-details-page.component.ts @@ -6,103 +6,50 @@ * found in the LICENSE file at https://angular.dev/license */ -import {ChangeDetectionStrategy, Component, inject, input, computed} from '@angular/core'; -import {DOCUMENT} from '@angular/common'; -import {MatTabsModule} from '@angular/material/tabs'; +import {ChangeDetectionStrategy, Component, inject, input} from '@angular/core'; import {DocContent, DocViewer} from '@angular/docs'; -import {ActivatedRoute, Router} from '@angular/router'; -import {ApiItemType} from './../interfaces/api-item-type'; +import {ActivatedRoute} from '@angular/router'; +import {DOCUMENT} from '@angular/common'; import {ReferenceScrollHandler} from '../services/reference-scroll-handler.service'; -import { - API_REFERENCE_DETAILS_PAGE_HEADER_CLASS_NAME, - API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME, - API_REFERENCE_TAB_ATTRIBUTE, - API_REFERENCE_TAB_API_LABEL, - API_TAB_CLASS_NAME, - API_REFERENCE_TAB_URL_ATTRIBUTE, -} from '../constants/api-reference-prerender.constants'; +import {API_SECTION_CLASS_NAME} from '../constants/api-reference-prerender.constants'; @Component({ selector: 'adev-reference-page', - imports: [DocViewer, MatTabsModule], + standalone: true, + imports: [DocViewer], templateUrl: './api-reference-details-page.component.html', styleUrls: ['./api-reference-details-page.component.scss'], providers: [ReferenceScrollHandler], changeDetection: ChangeDetectionStrategy.OnPush, }) export default class ApiReferenceDetailsPage { - private readonly activatedRoute = inject(ActivatedRoute); + private readonly referenceScrollHandler = inject(ReferenceScrollHandler); + private readonly route = inject(ActivatedRoute); private readonly document = inject(DOCUMENT); - private readonly router = inject(Router); - private readonly scrollHandler = inject(ReferenceScrollHandler); docContent = input(); - tab = input(); - // aliases - ApiItemType = ApiItemType; - - // computed state - parsedDocContent = computed(() => { - // TODO: pull this logic outside of a computed where it can be tested etc. - const docContent = this.docContent(); - - if (docContent === undefined) { - return { - header: undefined, - members: undefined, - tabs: [], - }; - } - - const element = this.document.createElement('div'); - element.innerHTML = docContent.contents; - - // Get the innerHTML of the header element from received document. - const header = element.querySelector(API_REFERENCE_DETAILS_PAGE_HEADER_CLASS_NAME); - // Get the innerHTML of the card elements from received document. - const members = element.querySelector(API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME); - - // Get the tab elements from received document. - // We're expecting that tab element will contain `tab` attribute. - const tabs = Array.from(element.querySelectorAll(`[${API_REFERENCE_TAB_ATTRIBUTE}]`)).map( - (tab) => ({ - url: tab.getAttribute(API_REFERENCE_TAB_URL_ATTRIBUTE)!, - title: tab.getAttribute(API_REFERENCE_TAB_ATTRIBUTE)!, - content: tab.innerHTML, - }), - ); - - element.remove(); - - return { - header: header?.innerHTML, - members: members?.innerHTML, - tabs, - }; - }); - - tabs = () => this.parsedDocContent().tabs; - - selectedTabIndex = computed(() => { - const existingTabIdx = this.tabs().findIndex((tab) => tab.url === this.tab()); - return Math.max(existingTabIdx, 0); - }); - - isApiTabActive = computed(() => { - const activeTabTitle = this.tabs()[this.selectedTabIndex()]?.title; - return activeTabTitle === API_REFERENCE_TAB_API_LABEL || activeTabTitle === 'CLI'; - }); - - membersCardsLoaded(): void { - this.scrollHandler.setupListeners(API_TAB_CLASS_NAME); + onContentLoaded() { + this.referenceScrollHandler.setupListeners(API_SECTION_CLASS_NAME); + this.scrollToSectionLegacy(); } - tabChange(tabIndex: number) { - this.router.navigate([], { - relativeTo: this.activatedRoute, - queryParams: {tab: this.tabs()[tabIndex].url}, - queryParamsHandling: 'merge', - }); + /** Handle legacy URLs with a `tab` query param from the old tab layout */ + private scrollToSectionLegacy() { + const params = this.route.snapshot.queryParams; + const tab = params['tab'] as string | undefined; + + if (tab) { + const section = this.document.getElementById(tab); + + if (section) { + // `scrollIntoView` is ignored even, if the element exists. + // It seems that it's related to: https://issues.chromium.org/issues/40715316 + // Hence, the usage of `setTimeout`. + setTimeout(() => { + section.scrollIntoView({behavior: 'smooth'}); + }, 100); + } + } } } diff --git a/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.html b/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.html index 4345ce9f1e8..b9ba87546e1 100644 --- a/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.html +++ b/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.html @@ -1,11 +1,5 @@ -
- -
- - +@if (docContent(); as docContent) { + +}
Jump to details
diff --git a/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.scss b/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.scss index e1d1fcc38e8..9d356ce0c80 100644 --- a/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.scss +++ b/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.scss @@ -1,123 +1,21 @@ @use '@angular/docs/styles/media-queries' as mq; -// Note: cli-reference-details-page is receiving page styles -// from api-reference-details-page.component.scss +@use '@angular/docs/styles/reference' as ref; + +:host { + display: block; + width: 100%; + max-width: var(--page-width); + padding: var(--layout-padding) 0 1rem var(--layout-padding); + box-sizing: border-box; + + @include mq.for-desktop-down { + padding: var(--layout-padding); + max-width: none; + } +} // stylelint-disable-next-line ::ng-deep { - .adev-ref-content { - display: flex; - padding-block: 1rem; - gap: 1rem; - &:not(:last-of-type) { - border-block-end: 1px solid var(--senary-contrast); - } - } - - .adev-header-and-tabs { - &.adev-cli-content { - width: 100%; - max-width: var(--page-width); - - @include mq.for-desktop-down { - max-width: none; - } - } - } - - .adev-cli-members-container { - padding: 0 0 var(--layout-padding) var(--layout-padding); - padding-bottom: 1rem; - box-sizing: border-box; - max-width: var(--page-width); - - @include mq.for-desktop-down { - width: 100%; - padding: var(--layout-padding); - padding-top: 0; - max-width: none; - } - } - - .adev-ref-option-and-description { - flex-grow: 1; - max-width: calc(100% - 80px); - p { - margin-block-end: 0; - } - } - - .docs-reference-type-and-default { - width: 4.375rem; - flex-shrink: 0; - span { - display: block; - font-size: 0.875rem; - margin-block-end: 0.2rem; - white-space: nowrap; - - &:not(:first-child) { - margin-block-start: 1rem; - } - } - - code { - font-size: 0.775rem; - } - } - - .adev-reference-cli-toc { - border: 1px solid var(--senary-contrast); - border-radius: 0.3rem; - position: relative; - transition: border 0.3s ease; - - &::before { - content: ''; - inset: -1px; - position: absolute; - background: transparent; - border-radius: 0.35rem; - z-index: 0; - } - - &:has(.shiki-ln-line-highlighted) { - &::before { - background: var(--red-to-pink-to-purple-horizontal-gradient); - } - } - - pre { - border-radius: 0.25rem; - position: relative; - z-index: 100; - background: var(--octonary-contrast); - } - } - - .shiki-ln-line-argument, - .shiki-ln-line-option { - padding: 0.1rem 0.2rem 0.2rem; - margin-inline: 0.1rem; - color: var(--quaternary-contrast); - background: transparent; - border-radius: 0.25rem; - position: relative; - transition: - color 0.3s ease, - background 0.3s ease, - border 0.3s ease; - - &:hover { - color: var(--primary-contrast); - background: var(--septenary-contrast); - } - - &.shiki-ln-line-highlighted { - color: var(--primary-contrast); - background: var(--senary-contrast); - } - } - .shiki-ln-line-argument { - margin-inline-start: 0.2rem; - } + @include ref.reference-common(); + @include ref.cli-reference(); } diff --git a/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.spec.ts b/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.spec.ts index a0e7a100511..fc7e14c9438 100644 --- a/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.spec.ts +++ b/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.spec.ts @@ -6,23 +6,20 @@ * found in the LICENSE file at https://angular.dev/license */ -import {TestBed} from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import CliReferenceDetailsPage from './cli-reference-details-page.component'; -import {RouterTestingHarness, RouterTestingModule} from '@angular/router/testing'; -import {signal} from '@angular/core'; +import {RouterTestingHarness} from '@angular/router/testing'; import {ReferenceScrollHandler} from '../services/reference-scroll-handler.service'; -import {provideRouter} from '@angular/router'; -import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {provideRouter, withComponentInputBinding} from '@angular/router'; +import {provideNoopAnimations} from '@angular/platform-browser/animations'; describe('CliReferenceDetailsPage', () => { let component: CliReferenceDetailsPage; - let harness: RouterTestingHarness; + let fixture: ComponentFixture; let fakeApiReferenceScrollHandler = { setupListeners: () => {}, - membersMarginTopInPx: signal(0), - updateMembersMarginTop: () => {}, }; const SAMPLE_CONTENT = ` @@ -34,33 +31,42 @@ describe('CliReferenceDetailsPage', () => { beforeEach(async () => { TestBed.configureTestingModule({ - imports: [CliReferenceDetailsPage, RouterTestingModule], + imports: [CliReferenceDetailsPage], providers: [ - provideRouter([ - { - path: '**', - component: CliReferenceDetailsPage, - data: { - 'docContent': { - id: 'id', - contents: SAMPLE_CONTENT, + provideNoopAnimations(), + provideRouter( + [ + { + path: '**', + component: CliReferenceDetailsPage, + data: { + 'docContent': { + id: 'id', + contents: SAMPLE_CONTENT, + }, }, }, - }, - ]), + ], + withComponentInputBinding(), + ), ], }); TestBed.overrideProvider(ReferenceScrollHandler, {useValue: fakeApiReferenceScrollHandler}); - harness = await RouterTestingHarness.create(); - const {fixture} = harness; + const harness = await RouterTestingHarness.create(); + fixture = harness.fixture; component = await harness.navigateByUrl('/', CliReferenceDetailsPage); - TestbedHarnessEnvironment.loader(fixture); fixture.detectChanges(); }); - it('should set content on init', () => { - expect(component.mainContentInnerHtml()).toBe('First column content'); - expect(component.cardsInnerHtml()).toBe('Members content'); + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should load the doc content', () => { + expect(component.docContent()?.contents).toBeTruthy(); + + const docsViewer = fixture.nativeElement.querySelector('docs-viewer'); + expect(docsViewer).toBeTruthy(); }); }); diff --git a/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.ts b/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.ts index 75414c36164..ebbadfe7b36 100644 --- a/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.ts +++ b/adev/src/app/features/references/cli-reference-details-page/cli-reference-details-page.component.ts @@ -6,81 +6,16 @@ * found in the LICENSE file at https://angular.dev/license */ -import {DOCUMENT} from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - DestroyRef, - OnInit, - inject, - signal, -} from '@angular/core'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {ChangeDetectionStrategy, Component, input} from '@angular/core'; import {DocContent, DocViewer} from '@angular/docs'; -import {ActivatedRoute} from '@angular/router'; -import {map} from 'rxjs/operators'; -import {ReferenceScrollHandler} from '../services/reference-scroll-handler.service'; -import {API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME} from '../constants/api-reference-prerender.constants'; - -export const CLI_MAIN_CONTENT_SELECTOR = '.docs-reference-cli-content'; -export const CLI_TOC = '.adev-reference-cli-toc'; @Component({ selector: 'adev-cli-reference-page', imports: [DocViewer], templateUrl: './cli-reference-details-page.component.html', - styleUrls: [ - './cli-reference-details-page.component.scss', - '../api-reference-details-page/api-reference-details-page.component.scss', - ], - providers: [ReferenceScrollHandler], + styleUrls: ['./cli-reference-details-page.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export default class CliReferenceDetailsPage implements OnInit { - private readonly activatedRoute = inject(ActivatedRoute); - private readonly destroyRef = inject(DestroyRef); - private readonly document = inject(DOCUMENT); - private readonly scrollHandler = inject(ReferenceScrollHandler); - - cardsInnerHtml = signal(''); - mainContentInnerHtml = signal(''); - - ngOnInit(): void { - this.setPageContent(); - } - - contentLoaded(): void { - this.scrollHandler.setupListeners(CLI_TOC); - } - - // Fetch the content for CLI Reference page based on the active route. - private setPageContent(): void { - this.activatedRoute.data - .pipe( - map((data) => data['docContent']), - takeUntilDestroyed(this.destroyRef), - ) - .subscribe((doc: DocContent | undefined) => { - this.setContentForPageSections(doc); - }); - } - - private setContentForPageSections(doc: DocContent | undefined) { - const element = this.document.createElement('div'); - element.innerHTML = doc?.contents!; - - // Get the innerHTML of the main content from received document. - const mainContent = element.querySelector(CLI_MAIN_CONTENT_SELECTOR); - if (mainContent) { - this.mainContentInnerHtml.set(mainContent.innerHTML); - } - - // Get the innerHTML of the cards from received document. - const cards = element.querySelector(API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME); - if (cards) { - this.cardsInnerHtml.set(cards.innerHTML); - } - - element.remove(); - } +export default class CliReferenceDetailsPage { + docContent = input(); } diff --git a/adev/src/app/features/references/constants/api-reference-prerender.constants.ts b/adev/src/app/features/references/constants/api-reference-prerender.constants.ts index f0a9dd731e2..84cc97814b9 100644 --- a/adev/src/app/features/references/constants/api-reference-prerender.constants.ts +++ b/adev/src/app/features/references/constants/api-reference-prerender.constants.ts @@ -6,10 +6,5 @@ * found in the LICENSE file at https://angular.dev/license */ -export const API_REFERENCE_DETAILS_PAGE_HEADER_CLASS_NAME = '.docs-reference-header'; -export const API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME = '.docs-reference-members-container'; -export const API_REFERENCE_TAB_ATTRIBUTE = 'data-tab'; -export const API_REFERENCE_TAB_URL_ATTRIBUTE = 'data-tab-url'; -export const API_REFERENCE_TAB_API_LABEL = 'API'; -export const API_TAB_CLASS_NAME = '.docs-reference-api-tab'; +export const API_SECTION_CLASS_NAME = 'docs-reference-api-section'; export const MEMBER_ID_ATTRIBUTE = 'member-id'; diff --git a/adev/src/app/features/references/services/reference-scroll-handler.service.ts b/adev/src/app/features/references/services/reference-scroll-handler.service.ts index b479de08d43..f8df1204726 100644 --- a/adev/src/app/features/references/services/reference-scroll-handler.service.ts +++ b/adev/src/app/features/references/services/reference-scroll-handler.service.ts @@ -7,55 +7,29 @@ */ import {DOCUMENT, isPlatformBrowser} from '@angular/common'; -import {DestroyRef, Injectable, Injector, PLATFORM_ID, inject} from '@angular/core'; +import {DestroyRef, Injectable, PLATFORM_ID, inject} from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {fromEvent} from 'rxjs'; import {MEMBER_ID_ATTRIBUTE} from '../constants/api-reference-prerender.constants'; -import {WINDOW} from '@angular/docs'; import {Router} from '@angular/router'; -import {AppScroller} from '../../../app-scroller'; - -// Adds some space/margin between the top of the target element and the top of viewport. -const SCROLL_MARGIN_TOP = 100; @Injectable() export class ReferenceScrollHandler { private readonly destroyRef = inject(DestroyRef); private readonly document = inject(DOCUMENT); - private readonly injector = inject(Injector); - private readonly window = inject(WINDOW); private readonly router = inject(Router); - private readonly appScroller = inject(AppScroller); private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); - setupListeners(tocSelector: string): void { + setupListeners(tocClass: string): void { if (!this.isBrowser) { return; } - this.setupCodeToCListeners(tocSelector); - this.setupFragmentChangeListener(); + this.setupCodeToCListeners(tocClass); } - private setupFragmentChangeListener() { - this.router.routerState.root.fragment - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((fragment) => { - // If there is no fragment or the scroll event has a position (traversing through history), - // allow the scroller to handle scrolling instead of going to the fragment - if (!fragment || this.appScroller.lastScrollEvent?.position) { - this.appScroller.scroll(this.injector); - return; - } - - const card = this.document.getElementById(fragment) as HTMLDivElement | null; - card?.focus(); - this.scrollToCard(card); - }); - } - - private setupCodeToCListeners(tocSelector: string): void { - const tocContainer = this.document.querySelector(tocSelector); + private setupCodeToCListeners(tocClass: string): void { + const tocContainer = this.document.querySelector(`.${tocClass}`); if (!tocContainer) { return; @@ -82,21 +56,6 @@ export class ReferenceScrollHandler { }); } - private scrollToCard(card: HTMLDivElement | null): void { - if (!card) { - return; - } - - if (card !== document.activeElement) { - (document.activeElement).blur(); - } - - this.window.scrollTo({ - top: card!.offsetTop - SCROLL_MARGIN_TOP, - behavior: 'smooth', - }); - } - private getMemberId(lineButton: HTMLButtonElement | null): string | undefined { if (!lineButton) { return undefined;