Schedule chips: horizontal scroll with edge arrows (PC navigable)
Replace hidden-scrollbar row (inaccessible on desktop without touch) with a ChipScroller: single-row scroll + left/right arrow buttons that auto-hide at each end and a bg fade. Resets to start on tab switch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>develop
parent
955ffae4cf
commit
3bcc47c0e8
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { timeOnly, dateKey } from "@/lib/format";
|
import { timeOnly, dateKey } from "@/lib/format";
|
||||||
import { type Lang, dict, teamShort, roundLabel as tRound, dayName, monthEn } from "@/lib/i18n";
|
import { type Lang, dict, teamShort, roundLabel as tRound, dayName, monthEn } from "@/lib/i18n";
|
||||||
|
|
@ -90,8 +90,8 @@ export default function ScheduleBoard({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 칩: 날짜 또는 조 */}
|
{/* 칩: 날짜 또는 조 — 단일 줄 가로 스크롤 + 양 끝 화살표(PC에서 마우스로 넘김, 끝 도달 시 숨김) */}
|
||||||
<div className="mb-4 flex gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
<ChipScroller resetKey={tab}>
|
||||||
{tab === "date"
|
{tab === "date"
|
||||||
? dates.map((d) => (
|
? dates.map((d) => (
|
||||||
<Chip key={d} active={d === selDate} onClick={() => setSelDate(d)}>
|
<Chip key={d} active={d === selDate} onClick={() => setSelDate(d)}>
|
||||||
|
|
@ -103,7 +103,7 @@ export default function ScheduleBoard({
|
||||||
{lang === "en" ? `Group ${g}` : `${g}조`}
|
{lang === "en" ? `Group ${g}` : `${g}조`}
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ChipScroller>
|
||||||
|
|
||||||
{/* 경기 카드 */}
|
{/* 경기 카드 */}
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
|
|
@ -120,6 +120,86 @@ export default function ScheduleBoard({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 단일 줄 가로 스크롤 + 양 끝 화살표. 끝에 닿으면 해당 방향 화살표를 숨긴다.
|
||||||
|
// resetKey 가 바뀌면(탭 전환 등) 맨 앞으로 되감고 재측정한다.
|
||||||
|
function ChipScroller({
|
||||||
|
children,
|
||||||
|
resetKey,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
resetKey?: string | number;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [atStart, setAtStart] = useState(true);
|
||||||
|
const [atEnd, setAtEnd] = useState(true);
|
||||||
|
|
||||||
|
const measure = () => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
const max = el.scrollWidth - el.clientWidth;
|
||||||
|
setAtStart(el.scrollLeft <= 1);
|
||||||
|
setAtEnd(el.scrollLeft >= max - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 탭/목록 변경 시 맨 앞으로 + 재측정 (레이아웃 확정 후)
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (el) el.scrollLeft = 0;
|
||||||
|
measure();
|
||||||
|
}, [resetKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
measure();
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener("scroll", measure, { passive: true });
|
||||||
|
window.addEventListener("resize", measure);
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener("scroll", measure);
|
||||||
|
window.removeEventListener("resize", measure);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const nudge = (dir: 1 | -1) =>
|
||||||
|
ref.current?.scrollBy({ left: dir * 220, behavior: "smooth" });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="flex gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!atStart && (
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pr-8 bg-gradient-to-r from-[var(--bg)] via-[var(--bg)] to-transparent">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="이전"
|
||||||
|
onClick={() => nudge(-1)}
|
||||||
|
className="pointer-events-auto grid h-7 w-7 place-items-center rounded-full border border-[var(--line-d)] bg-[var(--bg2)] text-[15px] leading-none text-white/80 shadow transition hover:text-white active:scale-95"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!atEnd && (
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center justify-end pl-8 bg-gradient-to-l from-[var(--bg)] via-[var(--bg)] to-transparent">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="다음"
|
||||||
|
onClick={() => nudge(1)}
|
||||||
|
className="pointer-events-auto grid h-7 w-7 place-items-center rounded-full border border-[var(--line-d)] bg-[var(--bg2)] text-[15px] leading-none text-white/80 shadow transition hover:text-white active:scale-95"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Chip({
|
function Chip({
|
||||||
active,
|
active,
|
||||||
onClick,
|
onClick,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue