Feat/BOOK-424 campaign banner
Approved-by: Bianca Widstam
This commit is contained in:
93
apps/scandic-web/components/MarqueeText/index.tsx
Normal file
93
apps/scandic-web/components/MarqueeText/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client"
|
||||
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
|
||||
import styles from "./marqueeText.module.css"
|
||||
|
||||
interface MarqueeTextProps
|
||||
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>> {
|
||||
backgroundColor: string
|
||||
textWrapperClassName?: string
|
||||
}
|
||||
|
||||
export function MarqueeText({
|
||||
backgroundColor,
|
||||
children,
|
||||
className,
|
||||
textWrapperClassName,
|
||||
...props
|
||||
}: MarqueeTextProps) {
|
||||
const textContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [dimensions, setDimensions] = useState({
|
||||
containerWidth: 0,
|
||||
contentWidth: 0,
|
||||
isOverflowing: false,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const element = textContainerRef.current
|
||||
const parentElement = element?.parentElement
|
||||
if (!parentElement) {
|
||||
return
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const containerWidth = element.clientWidth
|
||||
const contentWidth = element.scrollWidth
|
||||
const isOverflowing = contentWidth > containerWidth
|
||||
|
||||
setDimensions({
|
||||
containerWidth,
|
||||
contentWidth,
|
||||
isOverflowing,
|
||||
})
|
||||
|
||||
if (isOverflowing && containerWidth > 0) {
|
||||
const scrollDistance = contentWidth - containerWidth
|
||||
parentElement.style.setProperty(
|
||||
"--scroll-distance",
|
||||
`${scrollDistance}px`
|
||||
)
|
||||
|
||||
// Calculate dynamic animation duration based on scroll distance
|
||||
// This is done to avoid long scrolling durations for small distances and vice versa
|
||||
// Base formula: minimum 2s, add 50ms per pixel of scroll distance
|
||||
const baseDuration = 2
|
||||
const durationPerPixel = 0.05
|
||||
const calculatedDuration = Math.max(
|
||||
baseDuration,
|
||||
baseDuration + scrollDistance * durationPerPixel
|
||||
)
|
||||
|
||||
parentElement.style.setProperty(
|
||||
"--animation-duration",
|
||||
`${calculatedDuration}s`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
resizeObserver.observe(element)
|
||||
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(styles.marqueeText, className)}
|
||||
style={
|
||||
{ "--marquee-background-color": backgroundColor } as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
ref={textContainerRef}
|
||||
className={cx(styles.textWrapper, textWrapperClassName, {
|
||||
[styles.overflowing]: dimensions.isOverflowing,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
apps/scandic-web/components/MarqueeText/marqueeText.module.css
Normal file
103
apps/scandic-web/components/MarqueeText/marqueeText.module.css
Normal file
@@ -0,0 +1,103 @@
|
||||
.marqueeText {
|
||||
background-color: var(--marquee-background-color);
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&:has(.overflowing) {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--marquee-background-color) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
animation: leftShadow var(--animation-duration, 8s) linear infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
var(--marquee-background-color) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
opacity: 1;
|
||||
animation: rightShadow var(--animation-duration, 8s) linear infinite;
|
||||
}
|
||||
|
||||
&:has(.overflowing:hover)::before,
|
||||
&:has(.overflowing:hover)::after {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textWrapper {
|
||||
display: flex;
|
||||
scrollbar-width: none;
|
||||
align-items: center;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
* {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.overflowing {
|
||||
animation: autoScrollText var(--animation-duration, 8s) ease-in-out infinite;
|
||||
|
||||
&:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes autoScrollText {
|
||||
0%,
|
||||
15% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
80%,
|
||||
100% {
|
||||
transform: translateX(calc(-1 * var(--scroll-distance, 50px)));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leftShadow {
|
||||
0%,
|
||||
16% {
|
||||
opacity: 0;
|
||||
}
|
||||
17%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rightShadow {
|
||||
0%,
|
||||
79% {
|
||||
opacity: 1;
|
||||
}
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user