Creating a Synchronized Vertical and Horizontal Scrolling Component for Web Apps

creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps

Introduction

Microsoft Teams’ mobile agenda page offers a sleek and intuitive interface with synchronized vertical and horizontal scrolling. This design allows users to scroll through dates horizontally and see the corresponding events in a vertical list. Inspired by this elegant solution, I decided to create a similar component using modern web technologies. While there are many libraries and blogs about synchronized scrolling, they typically handle scrolling in the same direction. This article will show you how to achieve synchronized scrolling in both vertical and horizontal directions.

You can also checkout the live demo

Demo gif

Prerequisites

Before diving in, you should have a basic understanding of React, JavaScript, and Tailwind CSS. Make sure you have Node.js and npm installed on your machine.

Setting Up the Project

First, create a new React project using Create React App or your preferred method.

npm create vite@latest my-sync-scroll-app -- --template react
cd my-sync-scroll-app 
npm install

Next, install Tailwind CSS (optional).

npm install -D tailwindcss npx tailwindcss init

Configure Tailwind CSS by adding the following content to your tailwind.config.js file:

module.exports = { 
    purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 
    darkMode: false, 
    theme: { 
        extend: {}, 
    }, 
    variants: { 
        extend: {}, 
    }, 
    plugins: [], 
};

Add the Tailwind directives to your CSS file (src/index.css):

@tailwind base;
@tailwind components;
@tailwind utilities;

Utility Function for Date Generation

Let’s create a utility function to generate a list of dates starting from a given date.

export const generateDates = (startDate, days) => {
  const dates = [];
  for (let i = 0; i < days; i++) {
    const date = new Date(startDate);
    date.setDate(startDate.getDate() + i);
    dates.push(date.toISOString().split("T")[0]); // Format date as YYYY-MM-DD
  }
  return dates;
};

Creating the Horizontal Scroll Component

Let's start by creating the HorizontalScroll component. This component will allow users to scroll through dates horizontally and select a date.

import React, { useEffect, useRef } from "react";

const HorizontalScroll = ({
  dates,
  selectedDate,
  setSelectedDate,
  setSelectFromHorizontal,
}) => {
  const containerRef = useRef();

  useEffect(() => {
    // Automatically scroll to the selected date and center it in the view
    const selectedElement = containerRef.current.querySelector(`.date-item.selected`);
    if (selectedElement) {
      const containerWidth = containerRef.current.offsetWidth;
      const elementWidth = selectedElement.offsetWidth;
      const elementOffsetLeft = selectedElement.offsetLeft;
      const scrollTo = elementOffsetLeft - containerWidth / 2 + elementWidth / 2;
      containerRef.current.scrollTo({
        left: scrollTo,
        behavior: "smooth",
      });
    }
  }, [selectedDate]);

  const handleDateSelection = (index) => {
    setSelectedDate(dates[index]);
    setSelectFromHorizontal(true);
  };

  const onWheel = (e) => {
    const element = containerRef.current;
    if (element) {
      if (e.deltaY === 0) return;
      element.scrollTo({
        left: element.scrollLeft + e.deltaY,
      });
    }
  };

  return (
    
{dates.map((date, index) => { const day = new Date(date).toLocaleString([], { month: "short" }); const d = new Date(date).toLocaleString([], { day: "2-digit" }); return (
handleDateSelection(index)} style={{ backgroundColor: selectedDate === date ? "#90cdf4" : "#f7fafc", borderRadius: selectedDate === date ? "4px" : "0px", }} >

{day}

{d}

); })}
); }; export default HorizontalScroll;

Creating the Vertical Scroll Component

Next, create the VerticalScroll component to display the events for the selected date. This component will synchronize with the HorizontalScroll component to update the displayed events when a date is selected.

import React, { useEffect, useRef, useState } from "react";

const VerticalScroll = ({
  dates,
  onDateChange,
  selectedDate,
  selectFromHorizontal,
  setSelectFromHorizontal,
}) => {
  const containerRef = useRef();
  const [visibleDates, setVisibleDates] = useState([]);
  const [isProgrammaticScroll, setIsProgrammaticScroll] = useState(false);

  useEffect(() => {
    const container = containerRef.current;
    const handleScroll = () => {
      if (isProgrammaticScroll) {
        setIsProgrammaticScroll(false);
        return;
      }
      if (!selectFromHorizontal) {
        // Calculate the date at the top of the vertical scroll
        const topDateIndex = Math.floor(container.scrollTop / 100);
        const topDate = dates[topDateIndex];
        onDateChange(topDate);
      }
      // Calculate the visible dates based on the current scroll position
      const start = Math.floor(container.scrollTop / 100);
      const end = start + Math.ceil(container.clientHeight / 100);
      const visible = dates.slice(start, end);
      setVisibleDates(visible);
    };

    container.addEventListener("scroll", handleScroll);

    return () => container.removeEventListener("scroll", handleScroll);
  }, [dates, isProgrammaticScroll, onDateChange]);

  useEffect(() => {
    setTimeout(() => setSelectFromHorizontal(false), 1000);
  }, [selectedDate]);

  useEffect(() => {
    const selectedIndex = dates.indexOf(selectedDate);
    if (selectedIndex !== -1) {
      // Scroll to the selected date in the vertical scroll
      const scrollTo = selectedIndex * 100;
      setIsProgrammaticScroll(true);
      containerRef.current.scrollTo({
        top: scrollTo,
        behavior: "smooth",
      });
    }
  }, [selectedDate, dates]);

  return (
    
{dates.map((date) => (
{new Date(date).toLocaleString([], { month: "short", day: "2-digit", weekday: "short" })}
{visibleDates.includes(date) ? ( ) : (

No events

)}
))}
); }; const DateContent = ({ date }) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { const selectDate = new Date(date); selectDate.setHours(6, 0, 0, 0); const epochStartTimestamp = Math.floor(selectDate.getTime() / 1000); selectDate.setDate(selectDate.getDate() + 3); selectDate.setHours(23, 59, 59, 999); const epochEndTimestamp = Math.floor(selectDate.getTime() / 1000); const queryParams = `?start_timestamp=${epochStartTimestamp}&end_timestamp=${epochEndTimestamp}`; try { const response = await fetch(`https://example.com/api/upcomingShifts${queryParams}`); if (response.status === 200) { const result = await response.json(); setLoading(false); setData((prevData) => [...prevData, ...result.upcomingShifts]); } } catch (error) { console.error("Error fetching data:", error); } }; fetchData(); }, [date]); if (!data) return

Loading...

; return (
{loading ? (
) : ( data.map((d) => (

{d.id}

)) )}
); }; export default VerticalScroll;

Bringing It All Together

Now, let's integrate these components in the main App component.

import React, { useState } from "react";
import HorizontalScroll from "./components/HorizontalScroll";
import VerticalScroll from "./components/VerticalScroll";

const App = () => {
    const dates = generateDates(new Date(), 90);
    const [selectedDate, setSelectedDate] = useState(dates[0]);
    const [selectFromHorizontal, setSelectFromHorizontal] = useState(false);

  // Function to handle date changes from the vertical scroll component
  const handleDateChange = (date) => {
    if (!selectFromHorizontal) {
      setSelectedDate(date);
    }
  };

  return (
    
); }; export default App;

Conclusion

By following this guide, you can create a synchronized vertical and horizontal scrolling component for your web application. This design pattern, inspired by Microsoft Teams' mobile agenda page, enhances the user experience by providing an intuitive and efficient way to navigate through dates and events. Experiment with the components, adjust the styles, and integrate them into your projects to meet your specific needs. Happy coding!

Live Demo

For a live demonstration of the synchronized vertical and horizontal scrolling component, you can explore the demo on CodeSandbox. This interactive sandbox allows you to see the code in action and experiment with the functionality described in this blog.

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
web-components-vs.-vanilla-javascript:-a-niche-comparison-in-frontend-technologies

Web Components vs. Vanilla JavaScript: A Niche Comparison in Frontend Technologies

Next Post
automating-email-notifications-for-s3-object-uploads-using-sns

Automating Email Notifications for S3 Object Uploads using SNS

Related Posts