mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
docs(docs-infra): drop tabs layout from the API reference details page (#59068)
Drop the tabs in favor of a single page separated by sections. PR Close #59068
This commit is contained in:
parent
e0c33814fd
commit
b6733eeea4
37 changed files with 834 additions and 1068 deletions
|
|
@ -6,26 +6,26 @@
|
|||
<ul class="docs-faceted-list">
|
||||
<!-- TODO: Hide li elements with class docs-toc-item-h3 for laptop, table and phone screen resolutions -->
|
||||
@for (item of tableOfContentItems(); track item.id) {
|
||||
<li
|
||||
class="docs-faceted-list-item"
|
||||
[class.docs-toc-item-h2]="item.level === TableOfContentsLevel.H2"
|
||||
[class.docs-toc-item-h3]="item.level === TableOfContentsLevel.H3"
|
||||
>
|
||||
<a
|
||||
routerLink="."
|
||||
[fragment]="item.id"
|
||||
[class.docs-faceted-list-item-active]="item.id === activeItemId()"
|
||||
<li
|
||||
class="docs-faceted-list-item"
|
||||
[class.docs-toc-item-h2]="item.level === TableOfContentsLevel.H2"
|
||||
[class.docs-toc-item-h3]="item.level === TableOfContentsLevel.H3"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</li>
|
||||
<!-- Not using routerLink + fragment because of: https://github.com/angular/angular/issues/30139 -->
|
||||
<a
|
||||
[href]="location.path() + '#' + item.id"
|
||||
[class.docs-faceted-list-item-active]="item.id === activeItemId()"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
@if (shouldDisplayScrollToTop()) {
|
||||
<button type="button" (click)="scrollToTop()">
|
||||
<docs-icon role="presentation">arrow_upward_alt</docs-icon>
|
||||
Back to the top
|
||||
</button>
|
||||
<button type="button" (click)="scrollToTop()">
|
||||
<docs-icon role="presentation">arrow_upward_alt</docs-icon>
|
||||
Back to the top
|
||||
</button>
|
||||
}
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>();
|
||||
readonly location = inject(Location);
|
||||
|
||||
private readonly scrollSpy = inject(TableOfContentsScrollSpy);
|
||||
private readonly tableOfContentsLoader = inject(TableOfContentsLoader);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 <ClassMethodInfo entry={renderableMember} options={{showUsageNotes: true}} />;
|
||||
});
|
||||
};
|
||||
|
||||
const body = (
|
||||
<div className={REFERENCE_MEMBER_CARD_BODY}>
|
||||
{isClassMethodEntry(props.member) ? (
|
||||
(props.member.signatures.length
|
||||
? props.member.signatures
|
||||
: [props.member.implementation]
|
||||
).map((sig) => {
|
||||
const renderableMember = getFunctionMetadataRenderable(sig);
|
||||
return <ClassMethodInfo entry={renderableMember} options={{showUsageNotes: true}} />;
|
||||
})
|
||||
) : props.member.htmlDescription || props.member.deprecationMessage ? (
|
||||
{isClassMethodEntry(member) ? (
|
||||
renderMethod(member)
|
||||
) : member.htmlDescription || member.deprecationMessage ? (
|
||||
<div className={REFERENCE_MEMBER_CARD_ITEM}>
|
||||
<DeprecatedLabel entry={props.member} />
|
||||
<RawHtml value={props.member.htmlDescription} />
|
||||
<DeprecatedLabel entry={member} />
|
||||
<RawHtml value={member.htmlDescription} />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
|
|
@ -48,23 +52,19 @@ export function ClassMember(props: {member: MemberEntryRenderable}) {
|
|||
</div>
|
||||
);
|
||||
|
||||
const memberName = props.member.name;
|
||||
const returnType = getMemberType(props.member);
|
||||
const memberName = member.name;
|
||||
const returnType = getMemberType(member);
|
||||
return (
|
||||
<div id={memberName} className={REFERENCE_MEMBER_CARD} tabIndex={-1}>
|
||||
<header>
|
||||
<div className={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
<h3>{memberName}</h3>
|
||||
<div>
|
||||
{isClassMethodEntry(props.member) && props.member.signatures.length > 1 ? (
|
||||
<span>{props.member.signatures.length} overloads</span>
|
||||
) : returnType ? (
|
||||
<CodeSymbol code={returnType} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div id={memberName} tabIndex={-1} className={REFERENCE_MEMBER_CARD}>
|
||||
<header className={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
<h3>{memberName}</h3>
|
||||
{isClassMethodEntry(member) && member.signatures.length > 1 ? (
|
||||
<span>{member.signatures.length} overloads</span>
|
||||
) : returnType ? (
|
||||
<CodeSymbol code={returnType} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</header>
|
||||
{body}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div class="api">
|
||||
<div className={API_REFERENCE_CONTAINER}>
|
||||
<HeaderApi entry={entry} />
|
||||
<TabApi entry={entry} />
|
||||
<TabDescription entry={entry} />
|
||||
<TabUsageNotes entry={entry} />
|
||||
<SectionApi entry={entry} />
|
||||
{entry.members.length > 0 ? (
|
||||
<div class={REFERENCE_MEMBERS_CONTAINER}>
|
||||
<div class={REFERENCE_MEMBERS}>
|
||||
<ClassMemberList members={entry.members} />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<SectionDescription entry={entry} />
|
||||
<SectionUsageNotes entry={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div id={props.card.type} class={REFERENCE_MEMBER_CARD} tabIndex={-1}>
|
||||
<header>
|
||||
<div class={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
<h3>{props.card.type}</h3>
|
||||
</div>
|
||||
</header>
|
||||
<div class="docs-reference-card-body">
|
||||
<div class={REFERENCE_MEMBER_CARD}>
|
||||
<div className={REFERENCE_MEMBER_CARD_BODY}>
|
||||
{props.card.items.map((item) => (
|
||||
<div class="docs-ref-content">
|
||||
{item.deprecated ? <DeprecatedLabel entry={item} /> : <></>}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="cli">
|
||||
<div className="docs-cli">
|
||||
<div className="docs-reference-cli-content">
|
||||
<HeaderCli command={entry} />
|
||||
{[entry.name, ...entry.aliases].map((command) =>
|
||||
{[entry.name, ...entry.aliases].map((command) => (
|
||||
<div class="docs-code docs-reference-cli-toc">
|
||||
<pre class="docs-mini-scroll-track">
|
||||
<code>
|
||||
<div className={'shiki line cli'}>
|
||||
ng {commandName(entry, command)}
|
||||
{entry.argumentsLabel ? <button member-id={'Arguments'} className="shiki-ln-line-argument">{entry.argumentsLabel}</button> : <></>}
|
||||
{entry.hasOptions ? <button member-id={'Options'} className="shiki-ln-line-option">[options]</button> : <></>}
|
||||
{entry.argumentsLabel ? (
|
||||
<button member-id={'Arguments'} className="shiki-ln-line-argument">
|
||||
{entry.argumentsLabel}
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{entry.hasOptions ? (
|
||||
<button member-id={'Options'} className="shiki-ln-line-option">
|
||||
[options]
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
<RawHtml value={entry.htmlDescription} />
|
||||
{entry.subcommands && entry.subcommands?.length > 0 ? (
|
||||
<>
|
||||
<h3>Sub-commands</h3>
|
||||
<p>This command has the following sub-commands</p>
|
||||
<ul>
|
||||
{entry.subcommands.map((subcommand) => (
|
||||
<li>
|
||||
<a href={`cli/${entry.name}/${subcommand.name}`}>{subcommand.name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<RawHtml value={entry.htmlDescription}/>
|
||||
{entry.subcommands && entry.subcommands?.length > 0 ? <>
|
||||
<h3>Sub-commands</h3>
|
||||
<p>This command has the following sub-commands</p>
|
||||
<ul>
|
||||
{entry.subcommands.map((subcommand) =>
|
||||
<li>
|
||||
<a href={`cli/${entry.name}/${subcommand.name}`}>{subcommand.name}</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</> : <></>}
|
||||
</div>
|
||||
<div className={REFERENCE_MEMBERS_CONTAINER}>
|
||||
<div className={REFERENCE_MEMBERS}>
|
||||
{entry.cards.map((card) => <CliCard card={card} />)}
|
||||
</div>
|
||||
<div className={REFERENCE_MEMBERS}>
|
||||
{entry.cards.map((card) => (
|
||||
<>
|
||||
<SectionHeading name={card.type} />
|
||||
<CliCard card={card} />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function commandName(entry: CliCommandRenderable, command: string) {
|
||||
if (entry.parentCommand?.name) {
|
||||
return `${entry.parentCommand?.name} ${command}`;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div class="api">
|
||||
<div className={API_REFERENCE_CONTAINER}>
|
||||
<HeaderApi entry={entry} />
|
||||
<TabApi entry={entry} />
|
||||
<TabDescription entry={entry} />
|
||||
<TabUsageNotes entry={entry} />
|
||||
<SectionApi entry={entry} />
|
||||
<SectionDescription entry={entry} />
|
||||
<SectionUsageNotes entry={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div class="api">
|
||||
<div className={API_REFERENCE_CONTAINER}>
|
||||
<HeaderApi entry={entry} />
|
||||
<TabDescription entry={entry} />
|
||||
<SectionDescription entry={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div class="api">
|
||||
<div className={API_REFERENCE_CONTAINER}>
|
||||
<HeaderApi entry={entry} />
|
||||
<TabApi entry={entry} />
|
||||
<TabDescription entry={entry} />
|
||||
{
|
||||
entry.members.length > 0
|
||||
? (
|
||||
<div class={REFERENCE_MEMBERS_CONTAINER}>
|
||||
<div class={REFERENCE_MEMBERS}>
|
||||
{entry.members.map((member: MemberEntryRenderable) => (<ClassMember member={member}/>))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (<></>)
|
||||
}
|
||||
<SectionApi entry={entry} />
|
||||
{entry.members.length > 0 ? (
|
||||
<div class={REFERENCE_MEMBERS}>
|
||||
{entry.members.map((member: MemberEntryRenderable) => (
|
||||
<ClassMember member={member} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<SectionDescription entry={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div class={REFERENCE_MEMBER_CARD} id={opts.id} tabIndex={-1}>
|
||||
<header>
|
||||
<div id={opts.id} tabIndex={-1} class={REFERENCE_MEMBER_CARD}>
|
||||
<header class={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
{printSignaturesAsHeader ? (
|
||||
<code>
|
||||
<HighlightTypeScript
|
||||
|
|
@ -46,12 +49,12 @@ export const signatureCard = (
|
|||
/>
|
||||
</code>
|
||||
) : (
|
||||
<div className={REFERENCE_MEMBER_CARD_HEADER}>
|
||||
<>
|
||||
<h3>{name}</h3>
|
||||
<div>
|
||||
<CodeSymbol code={signature.returnType} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
<div class={REFERENCE_MEMBER_CARD_BODY}>
|
||||
|
|
@ -67,25 +70,24 @@ export function FunctionReference(entry: FunctionEntryRenderable) {
|
|||
const printSignaturesAsHeader = entry.signatures.length > 1;
|
||||
|
||||
return (
|
||||
<div class="api">
|
||||
<div className={API_REFERENCE_CONTAINER}>
|
||||
<HeaderApi entry={entry} />
|
||||
<TabApi entry={entry} />
|
||||
<TabDescription entry={entry} />
|
||||
<TabUsageNotes entry={entry} />
|
||||
<div className={REFERENCE_MEMBERS_CONTAINER}>
|
||||
<div className={REFERENCE_MEMBERS}>
|
||||
{entry.signatures.map((s, i) =>
|
||||
signatureCard(
|
||||
s.name,
|
||||
getFunctionMetadataRenderable(s, entry.moduleName),
|
||||
{
|
||||
id: `${s.name}_${i}`,
|
||||
},
|
||||
printSignaturesAsHeader,
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<SectionApi entry={entry} />
|
||||
<div className={REFERENCE_MEMBERS}>
|
||||
{entry.signatures.map((s, i) =>
|
||||
signatureCard(
|
||||
s.name,
|
||||
getFunctionMetadataRenderable(s, entry.moduleName),
|
||||
{
|
||||
id: `${s.name}_${i}`,
|
||||
},
|
||||
printSignaturesAsHeader,
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SectionDescription entry={entry} />
|
||||
<SectionUsageNotes entry={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div class="api">
|
||||
<div className={API_REFERENCE_CONTAINER}>
|
||||
<HeaderApi entry={entry} showFullDescription={true} />
|
||||
<TabApi entry={entry} />
|
||||
<TabUsageNotes entry={entry} />
|
||||
<SectionApi entry={entry} />
|
||||
|
||||
<div class={REFERENCE_MEMBERS_CONTAINER}>
|
||||
<div class={REFERENCE_MEMBERS}>
|
||||
{entry.callFunction.signatures.map((s, i) =>
|
||||
signatureCard(
|
||||
s.name,
|
||||
getFunctionMetadataRenderable(s, entry.moduleName),
|
||||
{
|
||||
id: `${s.name}_${i}`,
|
||||
},
|
||||
printSignaturesAsHeader,
|
||||
),
|
||||
)}
|
||||
<div class={REFERENCE_MEMBERS}>
|
||||
{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[],
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
],
|
||||
[] as JSX.Element[],
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SectionUsageNotes entry={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={SECTION_CONTAINER + ' docs-reference-api-section'}>
|
||||
<SectionHeading name={API_SECTION_NAME} />
|
||||
<CodeTableOfContents entry={props.entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div data-tab={DESCRIPTION_TAB_NAME} data-tab-url={normalizeTabUrl(DESCRIPTION_TAB_NAME)}>
|
||||
<div className={SECTION_CONTAINER}>
|
||||
<SectionHeading name={DESCRIPTION_SECTION_NAME} />
|
||||
<RawHtml value={props.entry.htmlDescription} />
|
||||
|
||||
{exportedBy.length ? (
|
||||
|
|
@ -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 (
|
||||
<h2 id={id} class={SECTION_HEADING}>
|
||||
<a href={'#' + id} aria-label={label} tabIndex={-1}>
|
||||
{props.name}
|
||||
</a>
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div data-tab={USAGE_NOTES_TAB_NAME} data-tab-url={normalizeTabUrl(USAGE_NOTES_TAB_NAME)}>
|
||||
<div className={SECTION_CONTAINER}>
|
||||
<SectionHeading name={USAGE_NOTES_SECTION_NAME} />
|
||||
<RawHtml value={props.entry.htmlUsageNotes} />
|
||||
</div>
|
||||
);
|
||||
|
|
@ -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 (
|
||||
<div data-tab={API_TAB_NAME} data-tab-url={normalizeTabUrl(API_TAB_NAME)}>
|
||||
<div class={'docs-reference-api-tab'}>
|
||||
<CodeTableOfContents entry={props.entry} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div class="api">
|
||||
<div className={API_REFERENCE_CONTAINER}>
|
||||
<HeaderApi entry={entry} />
|
||||
<TabApi entry={entry} />
|
||||
<TabDescription entry={entry} />
|
||||
<TabUsageNotes entry={entry} />
|
||||
<SectionApi entry={entry} />
|
||||
<SectionDescription entry={entry} />
|
||||
<SectionUsageNotes entry={entry} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -19,15 +19,3 @@ export const normalizePath = (path: string): string => {
|
|||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
export const normalizeTabUrl = (tabName: string): string => {
|
||||
return tabName
|
||||
.toLowerCase()
|
||||
.replace(/<code>(.*?)<\/code>/g, '$1') // remove <code>
|
||||
.replace(/<strong>(.*?)<\/strong>/g, '$1') // remove <strong>
|
||||
.replace(/<em>(.*?)<\/em>/g, '$1') // remove <em>
|
||||
.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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -49,9 +49,7 @@ export class TableOfContentsLoader {
|
|||
const updatedTopValues = new Map<string, number>();
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
410
adev/shared-docs/styles/_reference.scss
Normal file
410
adev/shared-docs/styles/_reference.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
<span class="adev-item-title" [attr.title]="apiItem.title">{{ apiItem.title }}</span>
|
||||
</a>
|
||||
@if (apiItem.isDeprecated) {
|
||||
<span class="docs-deprecated"> <!> </span>
|
||||
<span class="adev-deprecated"> <!> </span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
gap: 1em;
|
||||
}
|
||||
|
||||
.docs-deprecated {
|
||||
.adev-deprecated {
|
||||
font-family: var(--code-font);
|
||||
background-color: var(--senary-contrast);
|
||||
color: var(--tertiary-contrast);
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,9 @@
|
|||
<div class="adev-header-and-tabs">
|
||||
<docs-viewer [docContent]="parsedDocContent().header" class="docs-reference-header"></docs-viewer>
|
||||
|
||||
<mat-tab-group
|
||||
class="docs-reference-tabs"
|
||||
animationDuration="0ms"
|
||||
mat-stretch-tabs="false"
|
||||
[selectedIndex]="selectedTabIndex()"
|
||||
(selectedIndexChange)="tabChange($event)"
|
||||
>
|
||||
@for (tab of tabs(); track tab.url) {
|
||||
<mat-tab [label]="tab.title">
|
||||
<div class="adev-reference-tab-body">
|
||||
<docs-viewer [docContent]="tab.content"></docs-viewer>
|
||||
</div>
|
||||
</mat-tab>
|
||||
}
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
@if (isApiTabActive()) {
|
||||
@if (docContent(); as docContent) {
|
||||
<docs-viewer
|
||||
class="docs-reference-members-container"
|
||||
[docContent]="parsedDocContent().members"
|
||||
(contentLoaded)="membersCardsLoaded()"
|
||||
>
|
||||
</docs-viewer>
|
||||
[docContent]="docContent.contents"
|
||||
[hasToc]="true"
|
||||
(contentLoaded)="onContentLoaded()"
|
||||
/>
|
||||
}
|
||||
|
||||
<div id="jump-msg" class="cdk-visually-hidden">Jump to details</div>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<unknown>;
|
||||
|
||||
let fakeApiReferenceScrollHandler = {
|
||||
setupListeners: () => {},
|
||||
membersMarginTopInPx: signal(10),
|
||||
updateMembersMarginTop: () => {},
|
||||
};
|
||||
|
||||
const SAMPLE_CONTENT_WITH_TABS = `<div class="docs-reference-tabs">
|
||||
<div data-tab="API" data-tab-url="api" class="adev-reference-tab"></div>
|
||||
<div data-tab="Description" data-tab-url="description" class="adev-reference-tab"></div>
|
||||
<div data-tab="Examples" data-tab-url="examples" class="adev-reference-tab"></div>
|
||||
<div data-tab="Usage Notes" data-tab-url="usage-notes" class="adev-reference-tab"></div>
|
||||
<div class="docs-reference-members-container"></div>
|
||||
</div>`;
|
||||
const SAMPLE_CONTENT_WITH_SECTIONS = `<div class="docs-api">
|
||||
<div class="docs-reference-section">API</div>
|
||||
<div class="docs-reference-members"></div>
|
||||
<div class="docs-reference-section">Description</div>
|
||||
<div class="docs-reference-section">Examples</div>
|
||||
<div class="docs-reference-section">Usage Notes</div>
|
||||
</div>`;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<DocContent | undefined>();
|
||||
tab = input<string | undefined>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
<div class="adev-header-and-tabs adev-cli-content docs-scroll-track-transparent">
|
||||
<docs-viewer [docContent]="mainContentInnerHtml()" />
|
||||
</div>
|
||||
|
||||
<docs-viewer
|
||||
class="adev-cli-members-container"
|
||||
[docContent]="cardsInnerHtml()"
|
||||
(contentLoaded)="contentLoaded()"
|
||||
/>
|
||||
@if (docContent(); as docContent) {
|
||||
<docs-viewer [docContent]="docContent.contents" [hasToc]="true" />
|
||||
}
|
||||
|
||||
<div id="jump-msg" class="cdk-visually-hidden">Jump to details</div>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<unknown>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string>('');
|
||||
mainContentInnerHtml = signal<string>('');
|
||||
|
||||
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<DocContent | undefined>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(tocSelector);
|
||||
private setupCodeToCListeners(tocClass: string): void {
|
||||
const tocContainer = this.document.querySelector<HTMLDivElement>(`.${tocClass}`);
|
||||
|
||||
if (!tocContainer) {
|
||||
return;
|
||||
|
|
@ -82,21 +56,6 @@ export class ReferenceScrollHandler {
|
|||
});
|
||||
}
|
||||
|
||||
private scrollToCard(card: HTMLDivElement | null): void {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (card !== <HTMLElement>document.activeElement) {
|
||||
(<HTMLElement>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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue