fix(explore): support mouse-wheel scrolling on desktop

Hiding the scrollbar (previous commit) removed the only desktop
affordance for moving a horizontally-scrolling, scrollbar-hidden row
— mouse wheel scrolls vertically by default and there's no drag
handle anymore. Adds useHorizontalScroll, which redirects vertical
wheel delta to scrollLeft while leaving trackpad horizontal gestures
and touch swipe untouched. Applied to the explore species tabs and
StoryTray, which had the identical latent issue.
This commit is contained in:
2026-08-16 13:45:07 +02:00
parent 0f3b672532
commit 824927cb77
3 changed files with 38 additions and 1 deletions
+7 -1
View File
@@ -7,6 +7,7 @@ import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { Loader2, TrendingUp } from "lucide-react";
import { useTRPC } from "@/trpc/client";
import { useActivePet } from "@/context/ActivePetContext";
import { useHorizontalScroll } from "@/hooks/use-horizontal-scroll";
import { ExploreCard } from "@/components/explore/ExploreCard";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
@@ -32,6 +33,7 @@ export default function ExplorePage() {
const [activeTab, setActiveTab] = useState<string>("trending");
const [activeBreedId, setActiveBreedId] = useState<string | null>(null);
const [selectedPost, setSelectedPost] = useState<PostCardPost | null>(null);
const speciesTabsScroll = useHorizontalScroll<HTMLDivElement>();
const isTrending = activeTab === "trending";
const activeSpeciesId =
@@ -94,7 +96,11 @@ export default function ExplorePage() {
<h1 className="text-xl font-semibold mb-4">{t("title")}</h1>
<Tabs value={activeTab} onValueChange={handleTabChange} className="mb-4">
<div className="overflow-x-auto overflow-y-hidden scrollbar-hide">
<div
ref={speciesTabsScroll.ref}
onWheel={speciesTabsScroll.onWheel}
className="overflow-x-auto overflow-y-hidden scrollbar-hide"
>
<TabsList className="w-max">
<TabsTrigger value="trending" className="gap-1">
<TrendingUp className="h-3.5 w-3.5" />
+4
View File
@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { useQuery } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { useActivePet } from "@/context/ActivePetContext";
import { useHorizontalScroll } from "@/hooks/use-horizontal-scroll";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Dialog,
@@ -34,6 +35,7 @@ export function StoryTray() {
const trpc = useTRPC();
const { activePetId } = useActivePet();
const trayScroll = useHorizontalScroll<HTMLDivElement>();
const [addStoryOpen, setAddStoryOpen] = useState(false);
const [viewingPet, setViewingPet] = useState<{
id: string;
@@ -53,6 +55,8 @@ export function StoryTray() {
<>
{/* Horizontal scroll tray */}
<div
ref={trayScroll.ref}
onWheel={trayScroll.onWheel}
className="flex gap-4 overflow-x-auto px-6 py-3 scrollbar-hide"
role="list"
aria-label={t("storiesAriaLabel")}
+27
View File
@@ -0,0 +1,27 @@
import { useCallback, useRef, type WheelEvent } from "react";
/**
* Translates vertical mouse-wheel scrolling into horizontal scrolling for a
* scrollbar-hide-d container. Touch/trackpad swipes already scroll these
* containers natively — this only covers the desktop mouse-wheel case,
* which has no other way to move a horizontally-scrolling row with no
* visible scrollbar (see Gitea #27).
*
* Trackpad horizontal gestures (deltaX already present) pass through
* untouched, and the handler no-ops when the container isn't actually
* overflowing, so it never hijacks normal page scroll.
*/
export function useHorizontalScroll<T extends HTMLElement>() {
const ref = useRef<T>(null);
const onWheel = useCallback((e: WheelEvent<T>) => {
const el = ref.current;
if (!el || el.scrollWidth <= el.clientWidth) return;
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
el.scrollLeft += e.deltaY;
e.preventDefault();
}, []);
return { ref, onWheel };
}