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:
hawkgs 2024-11-12 09:35:24 +02:00 committed by Pawel Kozlowski
parent 9658dd1fe3
commit 922ffdb40e
7 changed files with 42 additions and 225 deletions

View file

@ -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>
}

View file

@ -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%;

View file

@ -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() {

View file

@ -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);

View file

@ -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.

View file

@ -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';

View file

@ -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;