feat(MediaManager): enhance MarqueeText component with unique keyframes for improved scrolling performance
This commit is contained in:
parent
04fa5ab054
commit
7e4b078d1f
|
|
@ -1,4 +1,4 @@
|
||||||
import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
|
@ -128,45 +128,69 @@ function ViewerModeIcon({ mode, className }: { mode: ViewMode; className?: strin
|
||||||
|
|
||||||
// ─── MarqueeText ──────────────────────────────────────────────────────────────
|
// ─── MarqueeText ──────────────────────────────────────────────────────────────
|
||||||
// Scrolls text left→right when it overflows its container, then back — ping-pong.
|
// Scrolls text left→right when it overflows its container, then back — ping-pong.
|
||||||
// Uses a CSS variable for the per-instance scroll distance so a single keyframe
|
// Injects a unique per-instance @keyframes rule with the exact pixel offset so
|
||||||
// declaration covers every instance.
|
// no CSS variables are needed (more reliable inside Radix portals).
|
||||||
|
|
||||||
if (typeof document !== 'undefined' && !document.getElementById('mm-marquee-kf')) {
|
let _mmSeq = 0;
|
||||||
const s = document.createElement('style');
|
|
||||||
s.id = 'mm-marquee-kf';
|
|
||||||
s.textContent = `@keyframes mm-scroll {
|
|
||||||
0%,12% { transform: translateX(0) }
|
|
||||||
88%,100%{ transform: translateX(var(--mm-dx,0px)) }
|
|
||||||
}`;
|
|
||||||
document.head.appendChild(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MarqueeText({ children, className }: { children?: string; className?: string }) {
|
function MarqueeText({ children, className }: { children?: string; className?: string }) {
|
||||||
const wrapRef = useRef<HTMLSpanElement>(null);
|
const wrapRef = useRef<HTMLSpanElement>(null);
|
||||||
const textRef = useRef<HTMLSpanElement>(null);
|
const idRef = useRef(`mm${++_mmSeq}`);
|
||||||
const [dx, setDx] = useState(0);
|
const [animCSS, setAnimCSS] = useState('');
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
const id = idRef.current;
|
||||||
const wrap = wrapRef.current;
|
const wrap = wrapRef.current;
|
||||||
const text = textRef.current;
|
if (!wrap) return;
|
||||||
if (!wrap || !text) return;
|
|
||||||
const measure = () => setDx(Math.max(0, text.scrollWidth - wrap.offsetWidth));
|
const measure = () => {
|
||||||
measure();
|
if (cancelled) return;
|
||||||
|
const ov = wrap.scrollWidth - wrap.clientWidth;
|
||||||
|
if (ov <= 0) { setAnimCSS(''); return; }
|
||||||
|
|
||||||
|
// Inject / update the keyframe with the exact measured pixel offset.
|
||||||
|
let el = document.getElementById(`kf-${id}`) as HTMLStyleElement | null;
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('style');
|
||||||
|
el.id = `kf-${id}`;
|
||||||
|
document.head.appendChild(el);
|
||||||
|
}
|
||||||
|
el.textContent = `@keyframes ${id}{0%,12%{transform:translateX(0)}88%,100%{transform:translateX(-${ov}px)}}`;
|
||||||
|
|
||||||
|
const secs = (Math.max(2000, ov * 20) / 1000).toFixed(2);
|
||||||
|
setAnimCSS(`${id} ${secs}s ease-in-out 0.8s infinite alternate`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Two nested RAFs ensure the portal element has a stable layout before we
|
||||||
|
// measure (one RAF fires before browser layout; two guarantees post-layout).
|
||||||
|
const r1 = requestAnimationFrame(() => {
|
||||||
|
const r2 = requestAnimationFrame(measure);
|
||||||
|
return () => cancelAnimationFrame(r2);
|
||||||
|
});
|
||||||
|
|
||||||
const ro = new ResizeObserver(measure);
|
const ro = new ResizeObserver(measure);
|
||||||
ro.observe(wrap);
|
ro.observe(wrap);
|
||||||
return () => ro.disconnect();
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
cancelAnimationFrame(r1);
|
||||||
|
ro.disconnect();
|
||||||
|
document.getElementById(`kf-${id}`)?.remove();
|
||||||
|
setAnimCSS('');
|
||||||
|
};
|
||||||
}, [children]);
|
}, [children]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span ref={wrapRef} className={`overflow-hidden whitespace-nowrap block ${className ?? ''}`}>
|
// overflow:hidden clips the scrolling text; minWidth:0 lets the element
|
||||||
|
// shrink inside grid/flex parents without forcing them to expand to the
|
||||||
|
// full text width (white-space:nowrap lives only on the inner span).
|
||||||
<span
|
<span
|
||||||
ref={textRef}
|
ref={wrapRef}
|
||||||
style={dx > 0 ? {
|
style={{ overflow: 'hidden', display: 'block', minWidth: 0 }}
|
||||||
display: 'inline-block',
|
className={className}
|
||||||
'--mm-dx': `-${dx}px`,
|
|
||||||
animation: `mm-scroll ${1.5 + dx / 80}s ease-in-out 1s infinite alternate`,
|
|
||||||
} as React.CSSProperties : undefined}
|
|
||||||
>
|
>
|
||||||
|
<span style={{ display: 'inline-block', whiteSpace: 'nowrap', animation: animCSS || undefined }}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -210,10 +234,14 @@ function ActionsModal({ entry, onClose, onOpen, onMount, onDownload, onDuplicate
|
||||||
return (
|
return (
|
||||||
<Dialog open={entry !== null} onOpenChange={open => !open && onClose()}>
|
<Dialog open={entry !== null} onOpenChange={open => !open && onClose()}>
|
||||||
<DialogContent className="max-w-sm">
|
<DialogContent className="max-w-sm">
|
||||||
<DialogHeader>
|
<DialogHeader className="overflow-hidden min-w-0">
|
||||||
<DialogTitle><MarqueeText>{entry?.name || '/'}</MarqueeText></DialogTitle>
|
{/* Keep DialogTitle for accessibility (aria-labelledby); styled text lives in the p below */}
|
||||||
|
<DialogTitle className="sr-only">{entry?.name || '/'}</DialogTitle>
|
||||||
|
<p className="text-lg font-semibold leading-none pr-6 overflow-hidden min-w-0">
|
||||||
|
<MarqueeText>{entry?.name || '/'}</MarqueeText>
|
||||||
|
</p>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{isFolder ? 'Folder' : humanFileSize(entry?.size ?? 0)}
|
<MarqueeText>{isFolder ? 'Folder' : humanFileSize(entry?.size ?? 0)}</MarqueeText>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{entry && (
|
{entry && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user