fix(home): slow sponsor ticker
This commit is contained in:
@@ -1,52 +1,151 @@
|
||||
import React from "react";
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface PartnerLogo {
|
||||
id: string;
|
||||
label: React.ReactNode;
|
||||
id: string
|
||||
label: React.ReactNode
|
||||
}
|
||||
|
||||
interface PartnerTickerProps {
|
||||
logos: PartnerLogo[];
|
||||
ariaLabel: string
|
||||
logos: PartnerLogo[]
|
||||
}
|
||||
|
||||
export function PartnerTicker({ logos }: PartnerTickerProps) {
|
||||
// Duplicate logos for seamless infinite loop
|
||||
const track = [...logos, ...logos];
|
||||
export const PARTNER_TICKER_ITEM_WIDTH_PX = 260
|
||||
export const PARTNER_TICKER_SPEED_PX_PER_SECOND = 48
|
||||
export const PARTNER_TICKER_MIN_DURATION_SECONDS = 24
|
||||
|
||||
export function partnerTickerDurationSeconds(logoCount: number) {
|
||||
if (logoCount <= 0) return 0
|
||||
|
||||
const loopDistance = logoCount * PARTNER_TICKER_ITEM_WIDTH_PX
|
||||
return Math.max(
|
||||
PARTNER_TICKER_MIN_DURATION_SECONDS,
|
||||
loopDistance / PARTNER_TICKER_SPEED_PX_PER_SECOND,
|
||||
)
|
||||
}
|
||||
|
||||
export function PartnerTicker({ ariaLabel, logos }: PartnerTickerProps) {
|
||||
const [hasFocusWithin, setHasFocusWithin] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
if (logos.length === 0) return null
|
||||
|
||||
const durationSeconds = partnerTickerDurationSeconds(logos.length)
|
||||
const isPaused = hasFocusWithin || isHovered
|
||||
|
||||
const renderLogoCopy = (copy: number) => (
|
||||
<div
|
||||
aria-hidden={copy === 1 ? 'true' : undefined}
|
||||
className="partner-ticker-copy"
|
||||
data-testid="partner-ticker-copy"
|
||||
key={copy}
|
||||
style={{ display: 'flex' }}
|
||||
>
|
||||
{logos.map((logo) => (
|
||||
<div
|
||||
key={`${copy}-${logo.id}`}
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
justifyContent: 'center',
|
||||
padding: '0 24px',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
width: PARTNER_TICKER_ITEM_WIDTH_PX,
|
||||
}}
|
||||
>
|
||||
{logo.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ overflow: 'hidden', flex: 1, minWidth: 0, position: 'relative' }}>
|
||||
{/* fade edges */}
|
||||
<div style={{ position: 'absolute', inset: 0, left: 0, width: 32, background: 'linear-gradient(to right, rgba(5,26,135,0.6), transparent)', zIndex: 1, pointerEvents: 'none' }} />
|
||||
<div style={{ position: 'absolute', inset: 0, right: 0, left: 'auto', width: 32, background: 'linear-gradient(to left, rgba(5,26,135,0.6), transparent)', zIndex: 1, pointerEvents: 'none' }} />
|
||||
<div
|
||||
aria-label={ariaLabel}
|
||||
className="partner-ticker-viewport"
|
||||
data-duration-seconds={durationSeconds}
|
||||
data-testid="partner-ticker"
|
||||
onBlur={(event) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
|
||||
setHasFocusWithin(false)
|
||||
}
|
||||
}}
|
||||
onFocus={() => setHasFocusWithin(true)}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
role="region"
|
||||
style={{ flex: 1, minWidth: 0, overflow: 'hidden', position: 'relative' }}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(to right, rgba(5,26,135,0.6), transparent)',
|
||||
inset: 0,
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
width: 32,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(to left, rgba(5,26,135,0.6), transparent)',
|
||||
inset: 0,
|
||||
left: 'auto',
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
width: 32,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', animation: 'marquee 22s linear infinite', width: 'max-content' }}>
|
||||
{track.map((logo, i) => (
|
||||
<div
|
||||
key={`${logo.id}-${i}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0 24px',
|
||||
color: '#fff',
|
||||
fontFamily: '"IBM Plex Sans", sans-serif',
|
||||
whiteSpace: 'nowrap',
|
||||
width: 260,
|
||||
flexShrink: 0,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{logo.label}
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className="partner-ticker-track"
|
||||
data-testid="partner-ticker-track"
|
||||
style={{
|
||||
animation: `partner-marquee ${durationSeconds}s linear infinite`,
|
||||
animationPlayState: isPaused ? 'paused' : 'running',
|
||||
display: 'flex',
|
||||
width: 'max-content',
|
||||
}}
|
||||
>
|
||||
{renderLogoCopy(0)}
|
||||
{renderLogoCopy(1)}
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes marquee {
|
||||
@keyframes partner-marquee {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
.partner-ticker-viewport:hover .partner-ticker-track,
|
||||
.partner-ticker-viewport:focus-within .partner-ticker-track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.partner-ticker-viewport {
|
||||
overflow-x: auto !important;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
.partner-ticker-track {
|
||||
animation: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.partner-ticker-copy[aria-hidden="true"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user