feat(content-hero): add expandable description with toggle functionality and overflow detection

Entire-Checkpoint: c6e522b4276c
This commit is contained in:
4gray committed 2026-04-17 23:18:29 +02:00
1 parent c0e045f45c
commit 8be51fc246
3 files changed
+144 -4

No files matched your search

@@ -142,9 +142,37 @@
<!-- Description -->
@if (description()) {
<p class="details__description">
<p
#descriptionEl
class="details__description"
[class.details__description--expanded]="
isDescriptionExpanded()
"
>
{{ description() }}
</p>
@if (hasDescriptionOverflow() || isDescriptionExpanded()) {
<button
type="button"
class="details__description-toggle"
(click)="toggleDescription()"
[attr.aria-expanded]="isDescriptionExpanded()"
>
<span>{{
(isDescriptionExpanded()
? 'SHOW_LESS'
: 'SHOW_MORE'
) | translate
}}</span>
<mat-icon aria-hidden="true">
{{
isDescriptionExpanded()
? 'expand_less'
: 'expand_more'
}}
</mat-icon>
</button>
}
}
<!-- Metadata Grid Slot -->
@@ -322,7 +322,7 @@
font-size: 0.9375rem;
line-height: 1.6;
color: var(--text-secondary);
margin-bottom: 20px;
margin-bottom: 6px;
max-width: 600px;
// Truncate long descriptions
@@ -330,6 +330,58 @@
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
&--expanded {
display: block;
-webkit-line-clamp: unset;
overflow: visible;
}
}
&__description-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
margin: 0 0 20px 0;
padding: 4px 0;
background: transparent;
border: 0;
cursor: pointer;
color: var(--text-secondary);
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
transition:
color 180ms ease,
transform 180ms ease;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
line-height: 16px;
transition: transform 180ms ease;
}
&:hover {
color: var(--text-primary);
mat-icon {
transform: translateY(2px);
}
}
&[aria-expanded='true']:hover mat-icon {
transform: translateY(-2px);
}
&:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
border-radius: 2px;
}
}
&__meta {
@@ -483,6 +535,10 @@
&__description {
-webkit-line-clamp: 3;
&--expanded {
-webkit-line-clamp: unset;
}
}
&__meta {
@@ -1,16 +1,35 @@
import { Component, computed, input, output, signal } from '@angular/core';
import {
Component,
DestroyRef,
computed,
effect,
inject,
input,
output,
signal,
viewChild,
ElementRef,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
@Component({
selector: 'app-content-hero',
standalone: true,
imports: [MatIconModule, MatButtonModule, NgxSkeletonLoaderComponent],
imports: [
MatIconModule,
MatButtonModule,
NgxSkeletonLoaderComponent,
TranslateModule,
],
templateUrl: './content-hero.component.html',
styleUrls: ['./content-hero.component.scss'],
})
export class ContentHeroComponent {
private readonly destroyRef = inject(DestroyRef);
readonly title = input<string>();
readonly description = input<string>();
readonly posterUrl = input<string>();
@@ -21,6 +40,26 @@ export class ContentHeroComponent {
readonly backClicked = output<void>();
readonly posterError = signal(false);
readonly descriptionEl = viewChild<ElementRef<HTMLElement>>('descriptionEl');
readonly isDescriptionExpanded = signal(false);
readonly hasDescriptionOverflow = signal(false);
private resizeObserver?: ResizeObserver;
constructor() {
effect(() => {
// Re-measure whenever description content or the element changes.
this.description();
const el = this.descriptionEl()?.nativeElement;
if (!el) return;
this.measureOverflow(el);
this.observeOverflow(el);
});
this.destroyRef.onDestroy(() => this.resizeObserver?.disconnect());
}
onPosterError(): void {
this.posterError.set(true);
}
@@ -57,4 +96,21 @@ export class ContentHeroComponent {
onBack(): void {
this.backClicked.emit();
}
toggleDescription(): void {
this.isDescriptionExpanded.update((v) => !v);
}
private measureOverflow(el: HTMLElement): void {
// Measure only in the clamped state; if already expanded, clamped overflow
// is implied when the element previously overflowed.
if (this.isDescriptionExpanded()) return;
this.hasDescriptionOverflow.set(el.scrollHeight > el.clientHeight + 1);
}
private observeOverflow(el: HTMLElement): void {
this.resizeObserver?.disconnect();
this.resizeObserver = new ResizeObserver(() => this.measureOverflow(el));
this.resizeObserver.observe(el);
}
}