mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(docs-infra): move members descriptions section to the center of the page (#58610)
Move the members description section below the main content of the page since it is part of the core information about a doc item. The CLI reference details page is affected as well. PR Close #58610
This commit is contained in:
parent
9658dd1fe3
commit
922ffdb40e
7 changed files with 42 additions and 225 deletions
|
|
@ -1,7 +1,13 @@
|
|||
<div class="adev-header-and-tabs docs-scroll-track-transparent">
|
||||
<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)">
|
||||
<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">
|
||||
|
|
@ -12,12 +18,12 @@
|
|||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
@if (isApiTabActive() && membersMarginTopInPx() > 0) {
|
||||
@if (isApiTabActive()) {
|
||||
<docs-viewer
|
||||
class="docs-reference-members-container"
|
||||
[docContent]="parsedDocContent().members"
|
||||
[style.marginTop.px]="membersMarginTopInPx()"
|
||||
(contentLoaded)="membersCardsLoaded()">
|
||||
(contentLoaded)="membersCardsLoaded()"
|
||||
>
|
||||
</docs-viewer>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@
|
|||
gap: 1rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
@include mq.for-desktop-down {
|
||||
flex-direction: column;
|
||||
}
|
||||
flex-direction: column;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
|
|
@ -34,32 +32,14 @@
|
|||
.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-up {
|
||||
// this section should only be independently scrollable if
|
||||
// the API tab is active & the screen is wide enough
|
||||
&:has(.docs-reference-api-tab),
|
||||
&.adev-cli-content {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding-inline-end: 1rem;
|
||||
max-height: 100vh;
|
||||
height: max-content;
|
||||
overflow-y: scroll;
|
||||
width: 50%;
|
||||
}
|
||||
&:has(.docs-reference-api-tab) {
|
||||
width: 60%;
|
||||
}
|
||||
&:not(:has(.docs-reference-api-tab)) {
|
||||
width: 100%;
|
||||
max-width: var(--page-width);
|
||||
}
|
||||
}
|
||||
@include mq.for-desktop-down {
|
||||
padding: var(--layout-padding);
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--septenary-contrast);
|
||||
border-radius: 10px;
|
||||
|
|
@ -164,17 +144,15 @@
|
|||
|
||||
.docs-reference-members-container {
|
||||
width: 40%;
|
||||
padding: 0 var(--layout-padding) 1rem 0;
|
||||
box-sizing: border-box;
|
||||
max-width: 60ch;
|
||||
width: 100%;
|
||||
max-width: var(--page-width);
|
||||
padding: 0 0 1rem var(--layout-padding);
|
||||
|
||||
@include mq.for-desktop-down {
|
||||
width: 100%;
|
||||
padding: var(--layout-padding);
|
||||
padding-top: 0;
|
||||
max-width: none;
|
||||
// override javascript-applied margin-top:
|
||||
// determined by how much space the sticky header takes up
|
||||
// to align the selected card member with the code block
|
||||
margin-block-start: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -301,7 +279,7 @@
|
|||
h3 {
|
||||
display: inline-block;
|
||||
font-family: var(--code-font);
|
||||
font-size: 1.25rem;
|
||||
font-size: 1rem;
|
||||
letter-spacing: -0.025rem;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
|
|
|
|||
|
|
@ -6,14 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
afterNextRender,
|
||||
inject,
|
||||
input,
|
||||
computed,
|
||||
} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, inject, input, computed} from '@angular/core';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {MatTabsModule} from '@angular/material/tabs';
|
||||
import {DocContent, DocViewer} from '@angular/docs';
|
||||
|
|
@ -26,7 +19,6 @@ import {
|
|||
API_REFERENCE_TAB_ATTRIBUTE,
|
||||
API_REFERENCE_TAB_API_LABEL,
|
||||
API_TAB_CLASS_NAME,
|
||||
API_REFERENCE_TAB_BODY_CLASS_NAME,
|
||||
API_REFERENCE_TAB_URL_ATTRIBUTE,
|
||||
} from '../constants/api-reference-prerender.constants';
|
||||
import {AppScroller} from '../../../app-scroller';
|
||||
|
|
@ -51,7 +43,6 @@ export default class ApiReferenceDetailsPage {
|
|||
|
||||
// aliases
|
||||
ApiItemType = ApiItemType;
|
||||
membersMarginTopInPx = this.scrollHandler.membersMarginTopInPx;
|
||||
|
||||
// computed state
|
||||
parsedDocContent = computed(() => {
|
||||
|
|
@ -107,11 +98,6 @@ export default class ApiReferenceDetailsPage {
|
|||
|
||||
constructor() {
|
||||
this.appScroller.disableScrolling = true;
|
||||
afterNextRender({
|
||||
write: () => {
|
||||
this.scrollHandler.updateMembersMarginTop(API_REFERENCE_TAB_BODY_CLASS_NAME);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
|
|
|||
|
|
@ -15,26 +15,25 @@
|
|||
|
||||
.adev-header-and-tabs {
|
||||
&.adev-cli-content {
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
max-width: var(--page-width);
|
||||
|
||||
@include mq.for-desktop-down {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adev-cli-members-container {
|
||||
width: 50%;
|
||||
padding: 0 var(--layout-padding) 1rem 0;
|
||||
|
||||
margin-top: 8.3125rem;
|
||||
padding: 0 0 var(--layout-padding) var(--layout-padding);
|
||||
padding-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
max-width: 60ch;
|
||||
max-width: var(--page-width);
|
||||
|
||||
@include mq.for-desktop-down {
|
||||
width: 100%;
|
||||
padding: var(--layout-padding);
|
||||
// override js applied margin top
|
||||
margin-block-start: 0 !important;
|
||||
padding-top: 0;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -103,7 +102,10 @@
|
|||
background: transparent;
|
||||
border-radius: 0.25rem;
|
||||
position: relative;
|
||||
transition: color 0.3s ease, background 0.3s ease, border 0.3s ease;
|
||||
transition:
|
||||
color 0.3s ease,
|
||||
background 0.3s ease,
|
||||
border 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-contrast);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ export default class CliReferenceDetailsPage implements OnInit {
|
|||
|
||||
cardsInnerHtml = signal<string>('');
|
||||
mainContentInnerHtml = signal<string>('');
|
||||
membersMarginTopInPx = this.scrollHandler.membersMarginTopInPx;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setPageContent();
|
||||
|
|
@ -52,7 +51,6 @@ export default class CliReferenceDetailsPage implements OnInit {
|
|||
|
||||
contentLoaded(): void {
|
||||
this.scrollHandler.setupListeners(CLI_TOC);
|
||||
this.scrollHandler.updateMembersMarginTop(CLI_TOC);
|
||||
}
|
||||
|
||||
// Fetch the content for CLI Reference page based on the active route.
|
||||
|
|
|
|||
|
|
@ -6,16 +6,10 @@
|
|||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
export const API_REFERENCE_DETAILS_PAGE_CONTENT_CLASS_NAME = '.adev-reference-content';
|
||||
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_BODY_CLASS_NAME = '.adev-reference-tab-body';
|
||||
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_REFERENCE_TAB_QUERY_PARAM = 'tab';
|
||||
export const API_TAB_CLASS_NAME = '.docs-reference-api-tab';
|
||||
export const API_REFERENCE_MEMBER_CARD_CLASS_NAME = '.docs-reference-member-card';
|
||||
export const API_TAB_ACTIVE_CODE_LINE = 'shiki-ln-line-highlighted';
|
||||
export const HIGHLIGHT_JS_CODE_LINE_CLASS_NAME = 'shiki-ln-line';
|
||||
export const MEMBER_ID_ATTRIBUTE = 'member-id';
|
||||
|
|
|
|||
|
|
@ -7,60 +7,32 @@
|
|||
*/
|
||||
|
||||
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
|
||||
import {
|
||||
DestroyRef,
|
||||
EnvironmentInjector,
|
||||
Injectable,
|
||||
OnDestroy,
|
||||
PLATFORM_ID,
|
||||
afterNextRender,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import {DestroyRef, Injectable, PLATFORM_ID, inject} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {fromEvent} from 'rxjs';
|
||||
import {auditTime} from 'rxjs/operators';
|
||||
import {
|
||||
API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME,
|
||||
API_REFERENCE_MEMBER_CARD_CLASS_NAME,
|
||||
API_TAB_ACTIVE_CODE_LINE,
|
||||
MEMBER_ID_ATTRIBUTE,
|
||||
} from '../constants/api-reference-prerender.constants';
|
||||
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';
|
||||
|
||||
export const SCROLL_EVENT_DELAY = 20;
|
||||
export const SCROLL_THRESHOLD = 20;
|
||||
// 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 implements OnDestroy {
|
||||
export class ReferenceScrollHandler {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly injector = inject(EnvironmentInjector);
|
||||
private readonly window = inject(WINDOW);
|
||||
private readonly router = inject(Router);
|
||||
private readonly appScroller = inject(AppScroller);
|
||||
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
|
||||
private readonly cardOffsetTop = new Map<string, number>();
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
membersMarginTopInPx = signal<number>(0);
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.resizeObserver?.disconnect();
|
||||
}
|
||||
|
||||
setupListeners(tocSelector: string): void {
|
||||
if (!this.isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupCodeToCListeners(tocSelector);
|
||||
this.setupMemberCardListeners();
|
||||
this.setScrollEventHandlers();
|
||||
this.listenToResizeCardContainer();
|
||||
this.setupFragmentChangeListener();
|
||||
}
|
||||
|
||||
|
|
@ -76,22 +48,11 @@ export class ReferenceScrollHandler implements OnDestroy {
|
|||
}
|
||||
|
||||
const card = this.document.getElementById(fragment) as HTMLDivElement | null;
|
||||
card?.focus();
|
||||
this.scrollToCard(card);
|
||||
});
|
||||
}
|
||||
|
||||
updateMembersMarginTop(selectorOfTheElementToAlign: string): void {
|
||||
if (!this.isBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementToAlign = this.document.querySelector<HTMLElement>(selectorOfTheElementToAlign);
|
||||
|
||||
if (elementToAlign) {
|
||||
this.updateMarginTopWhenTabBodyIsResized(elementToAlign);
|
||||
}
|
||||
}
|
||||
|
||||
private setupCodeToCListeners(tocSelector: string): void {
|
||||
const tocContainer = this.document.querySelector<HTMLDivElement>(tocSelector);
|
||||
|
||||
|
|
@ -120,89 +81,6 @@ export class ReferenceScrollHandler implements OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
private setupMemberCardListeners(): void {
|
||||
this.getAllMemberCards().forEach((card) => {
|
||||
this.cardOffsetTop.set(card.id, card.offsetTop);
|
||||
const header = card.querySelector('header');
|
||||
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
fromEvent(header, 'click')
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigate([], {fragment: card.id, replaceUrl: true});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private setScrollEventHandlers(): void {
|
||||
const scroll$ = fromEvent(this.document, 'scroll').pipe(
|
||||
auditTime(SCROLL_EVENT_DELAY),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
);
|
||||
|
||||
scroll$.subscribe(() => this.setActiveCodeLine());
|
||||
}
|
||||
|
||||
private listenToResizeCardContainer(): void {
|
||||
const membersCardContainer = this.document.querySelector(
|
||||
API_REFERENCE_DETAILS_PAGE_MEMBERS_CLASS_NAME,
|
||||
);
|
||||
if (membersCardContainer) {
|
||||
afterNextRender(
|
||||
() => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.updateCardsOffsetTop();
|
||||
this.setActiveCodeLine();
|
||||
});
|
||||
resizeObserver.observe(membersCardContainer);
|
||||
this.destroyRef.onDestroy(() => resizeObserver.disconnect());
|
||||
},
|
||||
{injector: this.injector},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private setActiveCodeLine(): void {
|
||||
const activeCard = Array.from(this.cardOffsetTop)
|
||||
.filter(([_, offsetTop]) => {
|
||||
return offsetTop < this.window.scrollY + this.membersMarginTopInPx() + SCROLL_THRESHOLD;
|
||||
})
|
||||
.pop();
|
||||
|
||||
if (!activeCard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeLines = this.document.querySelectorAll<HTMLButtonElement>(
|
||||
`button.${API_TAB_ACTIVE_CODE_LINE}`,
|
||||
);
|
||||
|
||||
const activeLine = activeLines.length > 0 ? activeLines.item(0) : null;
|
||||
const previousActiveMemberId = this.getMemberId(activeLine);
|
||||
const currentActiveMemberId = activeCard[0];
|
||||
|
||||
if (previousActiveMemberId && previousActiveMemberId !== currentActiveMemberId) {
|
||||
for (const line of Array.from(activeLines)) {
|
||||
line.classList.remove(API_TAB_ACTIVE_CODE_LINE);
|
||||
}
|
||||
} else {
|
||||
const lines = this.document.querySelectorAll<HTMLButtonElement>(
|
||||
`button[${MEMBER_ID_ATTRIBUTE}="${currentActiveMemberId}"]`,
|
||||
);
|
||||
for (const line of Array.from(lines)) {
|
||||
line.classList.add(API_TAB_ACTIVE_CODE_LINE);
|
||||
}
|
||||
this.document.getElementById(`${currentActiveMemberId}`)?.focus({preventScroll: true});
|
||||
}
|
||||
}
|
||||
|
||||
private scrollToCard(card: HTMLDivElement | null): void {
|
||||
if (!card) {
|
||||
return;
|
||||
|
|
@ -213,23 +91,11 @@ export class ReferenceScrollHandler implements OnDestroy {
|
|||
}
|
||||
|
||||
this.window.scrollTo({
|
||||
top: card!.offsetTop - this.membersMarginTopInPx(),
|
||||
top: card!.offsetTop - SCROLL_MARGIN_TOP,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
private updateCardsOffsetTop(): void {
|
||||
this.getAllMemberCards().forEach((card) => {
|
||||
this.cardOffsetTop.set(card.id, card.offsetTop);
|
||||
});
|
||||
}
|
||||
|
||||
private getAllMemberCards(): NodeListOf<HTMLDivElement> {
|
||||
return this.document.querySelectorAll<HTMLDivElement>(
|
||||
`${API_REFERENCE_MEMBER_CARD_CLASS_NAME}`,
|
||||
);
|
||||
}
|
||||
|
||||
private getMemberId(lineButton: HTMLButtonElement | null): string | undefined {
|
||||
if (!lineButton) {
|
||||
return undefined;
|
||||
|
|
@ -237,19 +103,6 @@ export class ReferenceScrollHandler implements OnDestroy {
|
|||
return lineButton.attributes.getNamedItem(MEMBER_ID_ATTRIBUTE)?.value;
|
||||
}
|
||||
|
||||
private updateMarginTopWhenTabBodyIsResized(tabBody: HTMLElement): void {
|
||||
this.resizeObserver?.disconnect();
|
||||
|
||||
this.resizeObserver = new ResizeObserver((_) => {
|
||||
const offsetTop = tabBody.getBoundingClientRect().top;
|
||||
if (offsetTop) {
|
||||
this.membersMarginTopInPx.set(offsetTop);
|
||||
}
|
||||
});
|
||||
|
||||
this.resizeObserver.observe(tabBody);
|
||||
}
|
||||
|
||||
private findButtonElement(element: HTMLElement) {
|
||||
let parent = element.parentElement;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue