Jaime02
Jaime02

Reputation: 356

Overflow-y auto makes overflow-x clip invisible

I am trying to create a scroll container for cards where the scroll is performed in the card content only, not in the header or footer. I guess that the container should hide the horizontal overflow and the card should have overflow-y: auto. However, doing so causes that the following cards are not visible.

function MainComponent() {
  const [activeCardIndex, setActiveCardIndex] = React.useState(0);

  const cardsContainerRef = React.useRef(null);
  const cardsRefs = React.useRef([]);

  const updateLayoutOffset = React.useCallback(() => {
    if (!cardsContainerRef.current || !cardsRefs.current[activeCardIndex]) {
      return 0;
    }

    const storyWidth = cardsRefs.current[activeCardIndex].offsetWidth;
    const containerWidth = cardsContainerRef.current.offsetWidth;
    let offset = containerWidth / 2 - storyWidth / 2 - activeCardIndex * storyWidth - activeCardIndex * 8;
    cardsContainerRef.current.style.transform = `translateX(${offset}px)`;
  }, [activeCardIndex]);

  React.useLayoutEffect(() => {
    updateLayoutOffset();
  }, [updateLayoutOffset]);

  React.useEffect(() => {
    updateLayoutOffset();
  }, [activeCardIndex, updateLayoutOffset]);

  const goToNextCard = React.useCallback(() => {
    if (activeCardIndex === cardsRefs.current.length - 1) {
      return;
    }

    setActiveCardIndex(activeCardIndex + 1);
  }, [activeCardIndex]);

  const goToPreviousCard = React.useCallback(() => {
    if (activeCardIndex === 0) {
      return;
    }

    setActiveCardIndex(activeCardIndex - 1);
  }, [activeCardIndex]);

  let cards = ["Card 1", "Card 2", "Card 3"];

  return (
    <div className="flex h-dvh max-h-dvh w-full max-w-full flex-col justify-center overflow-hidden bg-black px-2 py-1 sm:bg-[#1a1a1a] sm:py-3">
      <div className="flex h-full max-h-full flex-row transition-transform duration-500">
        <div className="flex h-full max-w-full flex-row items-center sm:gap-4">
          <button className="rounded-md bg-blue-500 p-2 text-white" onClick={goToPreviousCard}>
            Previous
          </button>
          <div className="flex aspect-[9/16] h-full max-h-full max-w-full flex-col rounded-md bg-black">
            <div className="flex flex-col gap-2 p-2">
              <h1 className="h-fit flex-1 text-left leading-none text-white">Header </h1>
            </div>
            <div ref={cardsContainerRef} className="flex grow flex-row items-center gap-2 overflow-y-auto overflow-x-clip transition-transform duration-500">
              {cards.map((card, index) => (
                <div
                  ref={(el) => {
                    if (el) {
                      cardsRefs.current[index] = el;
                    }
                  }}
                  key={index}
                  className="relative flex h-full min-w-full max-w-full flex-col gap-2 overflow-y-auto text-pretty rounded-md bg-white p-2 dark:bg-[#343434]"
                >
                  {Array(40)
                    .fill(0)
                    .map((_, i) => (
                      <p key={i} className="text-center">
                        {card}
                      </p>
                    ))}
                </div>
              ))}
            </div>
            <div className="flex w-full flex-row items-center p-2">
              <input className="min-w-0 flex-1 rounded-full border-[1px] bg-transparent px-4 py-2 text-left" />
            </div>
          </div>
          <button className="rounded-md bg-blue-500 p-2 text-white" onClick={goToNextCard}>
            Next
          </button>
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<MainComponent />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

I can not find any combination of overflow settings that works fine.

Upvotes: 2

Views: 138

Answers (2)

Jaime02
Jaime02

Reputation: 356

I found a simple solution: setting min-height: 0 in the cards container and overflow: hidden

function MainComponent() {
  const [activeCardIndex, setActiveCardIndex] = React.useState(0);

  const cardsContainerRef = React.useRef(null);
  const cardsRefs = React.useRef([]);

  const updateLayoutOffset = React.useCallback(() => {
    if (!cardsContainerRef.current || !cardsRefs.current[activeCardIndex]) {
      return 0;
    }

    const storyWidth = cardsRefs.current[activeCardIndex].offsetWidth;
    const containerWidth = cardsContainerRef.current.offsetWidth;
    let offset = containerWidth / 2 - storyWidth / 2 - activeCardIndex * storyWidth - activeCardIndex * 8;
    cardsContainerRef.current.style.transform = `translateX(${offset}px)`;
  }, [activeCardIndex]);

  React.useLayoutEffect(() => {
    updateLayoutOffset();
  }, [updateLayoutOffset]);

  React.useEffect(() => {
    updateLayoutOffset();
  }, [activeCardIndex, updateLayoutOffset]);

  const goToNextCard = React.useCallback(() => {
    if (activeCardIndex === cardsRefs.current.length - 1) {
      return;
    }

    setActiveCardIndex(activeCardIndex + 1);
  }, [activeCardIndex]);

  const goToPreviousCard = React.useCallback(() => {
    if (activeCardIndex === 0) {
      return;
    }

    setActiveCardIndex(activeCardIndex - 1);
  }, [activeCardIndex]);

  let cards = ["Card 1", "Card 2", "Card 3"];

  return (
    <div className="flex h-dvh max-h-dvh w-full max-w-full flex-col justify-center overflow-hidden bg-black px-2 py-1 sm:bg-[#1a1a1a] sm:py-3">
      <div className="flex h-full max-h-full flex-row transition-transform duration-500">
        <div className="flex h-full max-w-full flex-row items-center sm:gap-4">
          <button className="rounded-md bg-blue-500 p-2 text-white" onClick={goToPreviousCard}>
            Previous
          </button>
          <div className="overflow-x-hidden flex aspect-[9/16] h-full max-h-full max-w-full flex-col rounded-md bg-black">
            <div className="flex flex-col gap-2 p-2">
              <h1 className="h-fit flex-1 text-left leading-none text-white">Header </h1>
            </div>
            <div ref={cardsContainerRef} className="min-h-0 flex grow flex-row items-center gap-2 transition-transform duration-500">
              {cards.map((card, index) => (
                <div
                  ref={(el) => {
                    if (el) {
                      cardsRefs.current[index] = el;
                    }
                  }}
                  key={index}
                  className="relative flex h-full min-w-full max-w-full flex-col gap-2 overflow-y-auto text-pretty rounded-md bg-white p-2 dark:bg-[#343434]"
                >
                  {Array((index + 1) * 4)
                    .fill(0)
                    .map((_, i) => (
                      <p key={i} className="text-center">
                        {card}
                      </p>
                    ))}
                </div>
              ))}
            </div>
            <div className="flex w-full flex-row items-center p-2">
              <input className="min-w-0 flex-1 rounded-full border-[1px] bg-transparent px-4 py-2 text-left" />
            </div>
          </div>
          <button className="rounded-md bg-blue-500 p-2 text-white" onClick={goToNextCard}>
            Next
          </button>
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<MainComponent />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

Upvotes: 3

Reza Bakhshi Nia
Reza Bakhshi Nia

Reputation: 56

I had same challenge in my project and I tried to change my code based on your implementation.

here is the MainComponent

'use client'

import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from 'react'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { ChevronLeft, ChevronRight } from "lucide-react"

export default function MainComponent() {
  const [activeCardIndex, setActiveCardIndex] = useState(0)
  const cardsContainerRef = useRef<HTMLDivElement>(null)
  const cardsRefs = useRef<(HTMLDivElement | null)[]>([])

  const updateLayoutOffset = useCallback(() => {
    if (!cardsContainerRef.current || !cardsRefs.current[activeCardIndex]) {
      return
    }

    const cardWidth = cardsRefs.current[activeCardIndex]!.offsetWidth
    const containerWidth = cardsContainerRef.current.offsetWidth
    let offset = containerWidth / 2 - cardWidth / 2 - activeCardIndex * cardWidth
    cardsContainerRef.current.style.transform = `translateX(${offset}px)`
  }, [activeCardIndex])

  useLayoutEffect(() => {
    updateLayoutOffset()
  }, [updateLayoutOffset])

  useEffect(() => {
    updateLayoutOffset()
    window.addEventListener('resize', updateLayoutOffset)
    return () => window.removeEventListener('resize', updateLayoutOffset)
  }, [activeCardIndex, updateLayoutOffset])

  const goToNextCard = useCallback(() => {
    setActiveCardIndex((prev) => Math.min(prev + 1, cardsRefs.current.length - 1))
  }, [])

  const goToPreviousCard = useCallback(() => {
    setActiveCardIndex((prev) => Math.max(prev - 1, 0))
  }, [])

  const cards = ["Card 1", "Card 2", "Card 3"]

  return (
    <div className="flex h-dvh max-h-dvh w-full max-w-full flex-col justify-center overflow-hidden bg-background px-2 py-1 sm:py-3">
      <div className="flex h-full max-h-full flex-row items-center gap-4">
        <Button
          variant="outline"
          size="icon"
          onClick={goToPreviousCard}
          disabled={activeCardIndex === 0}
        >
          <ChevronLeft className="h-4 w-4" />
        </Button>
        <div className="relative flex aspect-[9/16] h-full max-h-full max-w-full flex-col overflow-hidden rounded-md bg-card">
          <div
            ref={cardsContainerRef}
            className="flex h-full transition-transform duration-500 ease-in-out"
          >
            {cards.map((card, index) => (
              <Card
                key={index}
                ref={(el) => (cardsRefs.current[index] = el)}
                className="h-full w-full shrink-0 overflow-hidden"
              >
                <CardHeader className="h-16">
                  <h2 className="text-xl font-semibold">{card}</h2>
                </CardHeader>
                <CardContent className="h-[calc(100%-8rem)] overflow-y-auto">
                  <div className="space-y-2">
                    {Array(40).fill(0).map((_, i) => (
                      <p key={i} className="text-center">
                        {card} Content {i + 1}
                      </p>
                    ))}
                  </div>
                </CardContent>
                <CardFooter className="h-16">
                  <Input className="w-full" placeholder="Type a message..." />
                </CardFooter>
              </Card>
            ))}
          </div>
        </div>
        <Button
          variant="outline"
          size="icon"
          onClick={goToNextCard}
          disabled={activeCardIndex === cards.length - 1}
        >
          <ChevronRight className="h-4 w-4" />
        </Button>
      </div>
    </div>
  )
}

Button.tsx Component:

import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline:
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }

Input.tsx component

import * as React from "react"

import { cn } from "@/lib/utils"

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Input.displayName = "Input"

export { Input }

Card.tsx component

import * as React from "react"

import { cn } from "@/lib/utils"

const Card = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "rounded-lg border bg-card text-card-foreground shadow-sm",
      className
    )}
    {...props}
  />
))
Card.displayName = "Card"

const CardHeader = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex flex-col space-y-1.5 p-6", className)}
    {...props}
  />
))
CardHeader.displayName = "CardHeader"

const CardTitle = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h3
    ref={ref}
    className={cn(
      "text-2xl font-semibold leading-none tracking-tight",
      className
    )}
    {...props}
  />
))
CardTitle.displayName = "CardTitle"

const CardDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <p
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
CardDescription.displayName = "CardDescription"

const CardContent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"

const CardFooter = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex items-center p-6 pt-0", className)}
    {...props}
  />
))
CardFooter.displayName = "CardFooter"

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

Upvotes: 2

Related Questions