feat: add gradient indicator for scrollable event descriptions (#25460)
* feat: add gradient indicator for scrollable event descriptions - Create EventDescription component with dynamic gradient overlay - Show gradient only when description content overflows (180px max-height) - Use Tailwind v4 gradient utilities for optimal styling - Replace inline description rendering in EventMeta with new component - Minimal implementation: 28 lines, no useEffect, single state variable * add top gradient and optimize ScrollableWithGradients component * add top gradient and optimize ScrollableWithGradients component * refactor: extract reusable ScrollableWithGradients component with top/bottom gradients * adjust gradient fades
This commit is contained in:
1 parent
9050f9b61f
commit
e312d1c044
2 files changed
+86
-9
No files matched your search
@@ -20,6 +20,7 @@ import { EventTypeAutoTranslatedField } from "@calcom/prisma/enums";
|
||||
import i18nConfigration from "../../../../../i18n.json";
|
||||
import { fadeInUp } from "../config";
|
||||
import { FromToTime } from "../utils/dates";
|
||||
import { ScrollableWithGradients } from "./ScrollableWithGradients";
|
||||
import { useBookerTime } from "./hooks/useBookerTime";
|
||||
|
||||
const WebTimezoneSelect = dynamic(
|
||||
@@ -174,15 +175,16 @@ export const EventMeta = ({
|
||||
{translatedTitle ?? event?.title}
|
||||
</EventTitle>
|
||||
{(event.description || translatedDescription) && (
|
||||
<EventMetaBlock
|
||||
data-testid="event-meta-description"
|
||||
contentClassName="mb-8 wrap-break-word max-w-full max-h-[180px] scroll-bar pr-4 overflow-y-auto">
|
||||
<div
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToSafeHTMLClient(translatedDescription ?? event.description),
|
||||
}}
|
||||
/>
|
||||
<EventMetaBlock data-testid="event-meta-description" contentClassName="mb-8">
|
||||
<ScrollableWithGradients
|
||||
className="wrap-break-word scroll-bar max-h-[180px] max-w-full overflow-y-auto pr-4"
|
||||
ariaLabel={t("description")}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: markdownToSafeHTMLClient(translatedDescription ?? event.description),
|
||||
}}
|
||||
/>
|
||||
</ScrollableWithGradients>
|
||||
</EventMetaBlock>
|
||||
)}
|
||||
<div className="stack-y-4 font-medium rtl:-mr-2">
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
|
||||
interface ScrollGradients {
|
||||
top: boolean;
|
||||
bottom: boolean;
|
||||
}
|
||||
|
||||
interface ScrollableWithGradientsProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
const SCROLL_THRESHOLD = 5; // Pixels before showing gradient
|
||||
|
||||
export function ScrollableWithGradients({ children, className, ariaLabel }: ScrollableWithGradientsProps) {
|
||||
const [gradients, setGradients] = useState<ScrollGradients>({ top: false, bottom: false });
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const updateGradients = useCallback(() => {
|
||||
const node = scrollRef.current;
|
||||
if (!node) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = node;
|
||||
const top = scrollTop > SCROLL_THRESHOLD;
|
||||
const bottom = scrollTop + clientHeight < scrollHeight - SCROLL_THRESHOLD;
|
||||
|
||||
setGradients((prev) => (prev.top === top && prev.bottom === bottom ? prev : { top, bottom }));
|
||||
}, []);
|
||||
|
||||
const setRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
scrollRef.current = node;
|
||||
updateGradients();
|
||||
},
|
||||
[updateGradients]
|
||||
);
|
||||
|
||||
// Handle resize events (window resize, font load, etc.)
|
||||
useEffect(() => {
|
||||
const node = scrollRef.current;
|
||||
if (!node) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateGradients);
|
||||
resizeObserver.observe(node);
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [updateGradients]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={setRef}
|
||||
onScroll={updateGradients}
|
||||
className={className}
|
||||
tabIndex={0}
|
||||
role="region"
|
||||
aria-label={ariaLabel}>
|
||||
{children}
|
||||
</div>
|
||||
{gradients.top && (
|
||||
<div
|
||||
className="from-default pointer-events-none absolute left-0 right-0 top-0 h-8 bg-gradient-to-b to-transparent dark:from-[var(--cal-bg-muted)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{gradients.bottom && (
|
||||
<div
|
||||
className="from-default pointer-events-none absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t to-transparent dark:from-[var(--cal-bg-muted)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user