fix: extend scroll-fade treatment to provider tabs and data tables

Apply the same hidden-scrollbar + edge-fade fix to two more overflow
spots found by a repo-wide scan:

- Application/Compose "General" tab source-provider picker
  (GitHub/GitLab/Bitbucket/Gitea/Git/Raw/Docker/Drop) had the same bare
  overflow-auto tab row as the service detail pages.
- The shared ui/table.tsx container wraps nearly every data table in
  the app with overflow-x-auto and no scroll affordance at all.

Refactor ScrollFadeContainer to expose the underlying scroll-fade logic
as a reusable useScrollFade hook + ScrollFadeEdges overlay, since
Table needs the fade attached to its own existing container div rather
than an extra wrapping div. ScrollFadeContainer's own behavior and API
are unchanged.
This commit is contained in:
Shivam Gupta 2026-08-08 23:35:30 +05:30
parent 0a95436d16
commit e0ffda82e4
4 changed files with 64 additions and 25 deletions

View File

@ -14,6 +14,7 @@ import {
GitIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { ScrollFadeContainer } from "@/components/shared/scroll-fade-container";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
@ -153,7 +154,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
setSab(e as TabState);
}}
>
<div className="flex flex-row items-center justify-between w-full overflow-auto">
<ScrollFadeContainer className="flex flex-row items-center justify-between w-full">
<TabsList
variant="line"
className="flex gap-4 justify-start bg-transparent"
@ -208,7 +209,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
Drop
</TabsTrigger>
</TabsList>
</div>
</ScrollFadeContainer>
<TabsContent value="github" className="w-full p-2">
{githubProviders && githubProviders?.length > 0 ? (

View File

@ -10,6 +10,7 @@ import {
GitIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { ScrollFadeContainer } from "@/components/shared/scroll-fade-container";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
@ -142,7 +143,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
setSab(e as TabState);
}}
>
<div className="flex flex-row items-center justify-between w-full overflow-auto">
<ScrollFadeContainer className="flex flex-row items-center justify-between w-full">
<TabsList
variant="line"
className="flex gap-4 justify-start bg-transparent"
@ -189,7 +190,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
Raw
</TabsTrigger>
</TabsList>
</div>
</ScrollFadeContainer>
<TabsContent value="github" className="w-full p-2">
{githubProviders && githubProviders?.length > 0 ? (

View File

@ -1,13 +1,10 @@
import { cn } from "@/lib/utils";
import { useEffect, useRef, useState } from "react";
import { type RefObject, useEffect, useRef, useState } from "react";
interface Props {
children: React.ReactNode;
className?: string;
}
export const ScrollFadeContainer = ({ children, className }: Props) => {
const ref = useRef<HTMLDivElement>(null);
export const useScrollFade = <T extends HTMLElement>(
deps: unknown[] = [],
): [RefObject<T | null>, boolean, boolean] => {
const ref = useRef<T>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
@ -32,7 +29,43 @@ export const ScrollFadeContainer = ({ children, className }: Props) => {
el.removeEventListener("scroll", updateFades);
resizeObserver.disconnect();
};
}, [children]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return [ref, canScrollLeft, canScrollRight];
};
export const ScrollFadeEdges = ({
canScrollLeft,
canScrollRight,
}: {
canScrollLeft: boolean;
canScrollRight: boolean;
}) => (
<>
<div
className={cn(
"pointer-events-none absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-background to-transparent transition-opacity",
canScrollLeft ? "opacity-100" : "opacity-0",
)}
/>
<div
className={cn(
"pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-background to-transparent transition-opacity",
canScrollRight ? "opacity-100" : "opacity-0",
)}
/>
</>
);
interface Props {
children: React.ReactNode;
className?: string;
}
export const ScrollFadeContainer = ({ children, className }: Props) => {
const [ref, canScrollLeft, canScrollRight] =
useScrollFade<HTMLDivElement>([children]);
return (
<div className="relative min-w-0">
@ -42,17 +75,9 @@ export const ScrollFadeContainer = ({ children, className }: Props) => {
>
{children}
</div>
<div
className={cn(
"pointer-events-none absolute inset-y-0 left-0 w-8 bg-gradient-to-r from-background to-transparent transition-opacity",
canScrollLeft ? "opacity-100" : "opacity-0",
)}
/>
<div
className={cn(
"pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-background to-transparent transition-opacity",
canScrollRight ? "opacity-100" : "opacity-0",
)}
<ScrollFadeEdges
canScrollLeft={canScrollLeft}
canScrollRight={canScrollRight}
/>
</div>
);

View File

@ -2,19 +2,31 @@
import type * as React from "react";
import {
ScrollFadeEdges,
useScrollFade,
} from "@/components/shared/scroll-fade-container";
import { cn } from "@/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
const [ref, canScrollLeft, canScrollRight] =
useScrollFade<HTMLDivElement>([props.children]);
return (
<div
ref={ref}
data-slot="table-container"
className="relative w-full overflow-x-auto"
className="relative w-full overflow-x-auto no-scrollbar"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
<ScrollFadeEdges
canScrollLeft={canScrollLeft}
canScrollRight={canScrollRight}
/>
</div>
);
}