Search code examples
reactjsnext.jstouch-event

How do I smoothly do drags or mobile swipe? Time Picker Rect Next Js 13 App Dir


So I decided to try and do my own time picker component. Why? Well there isn't a single UI library out there that the user can swipe up or down to choose just hour or minutes or PM/AM. I don't need the user to select a date, I just want the time they want.

So far, I can scroll and make the user choose the numbers on swipe both up/down. The problem is im trying that the user can move multiple numbers on the swiper instead of having to swipe and only move 1 number up or down. I also wanted to see the numbers moving as the user swipes.

If I can't do this I will simply to an select tag with options, but I wanted to avoid this as I don't like how it looks for mobile devices as they use the native select.

This is the first time trying to something like this so I tried to use chatGPT for this as well, so don't think I did this from scratch lol. I haven't had to do something custom like this before.

"use client";
import React, { useState } from "react";
import TimeSection from "./TimeSection";
export default function TimePickerModal() {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedHour, setSelectedHour] = useState(12);
  const [selectedMinute, setSelectedMinute] = useState(30);

  const hours = Array.from({ length: 24 }, (_, i) => i); 
  const minutes = Array.from({ length: 60 }, (_, i) => i); 

  const handleSwipe = (
    setter: React.Dispatch<React.SetStateAction<number>>,
    values: number[],
    currentValue: number
  ) => {
    let startY: number | null = null;
    let initialValue: number | null = null; 

    return (e: React.TouchEvent<HTMLDivElement>) => {
      if (e.type === "touchstart" && e.touches.length > 0) {
        startY = e.touches[0].clientY;
        initialValue = currentValue; 
      } else if (
        e.type === "touchmove" &&
        startY !== null &&
        initialValue !== null
      ) {
        const currentY = e.touches[0].clientY;
        const diff = currentY - startY;

        const swipeSensitivity = 50;
        const unitsToMove = Math.round(diff / swipeSensitivity);

        const newValue = initialValue - unitsToMove; 

        if (newValue >= 0 && newValue < values.length) {
          setter(newValue);
        } else if (newValue < 0) {
          setter(0);
        } else if (newValue >= values.length) {
          setter(values.length - 1);
        }
      }
    };
  };
  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Time Picker</button>

      {isOpen && (
        <div
          style={{
            position: "fixed",
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            backgroundColor: "rgba(0,0,0,0.5)",
            display: "flex",
            justifyContent: "center",
            alignItems: "center",
          }}
        >
          <div
            style={{
              backgroundColor: "white",
              padding: "20px",
              borderRadius: "5px",
              display: "flex",
              flexDirection: "row",
              color: "black",
            }}
          >
            <TimeSection
              values={hours}
              currentValue={selectedHour}
              handleSwipe={handleSwipe(setSelectedHour, hours, selectedHour)}
            />
            <TimeSection
              values={minutes}
              currentValue={selectedMinute}
              handleSwipe={handleSwipe(
                setSelectedMinute,
                minutes,
                selectedMinute
              )}
            />
            <button
              onClick={() => {
                console.log(
                  `Selected Time: ${String(selectedHour).padStart(
                    2,
                    "0"
                  )}:${String(selectedMinute).padStart(2, "0")}`
                );
                setIsOpen(false);
              }}
            >
              Enter
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

TimeSection.tsx

import React from "react";

interface TimeSectionProps {
  values: number[];
  currentValue: number;
  handleSwipe: (e: React.TouchEvent<HTMLDivElement>) => void;
}
export default function TimeSection({
  values,
  currentValue,
  handleSwipe,
}: TimeSectionProps) {
  return (
    <div
      style={{
        height: "250px",
        width: "50px",
        overflow: "hidden",
        position: "relative",
      }}
    >
      <div
        onTouchStart={handleSwipe}
        onTouchMove={handleSwipe}
        style={{
          position: "absolute",
          top: `calc(50% - ${currentValue * 24}px)`, 
          width: "100%",
          textAlign: "center",
        }}
      >
        {values.map((value, index) => (
          <div
            key={index}
            style={{
              color: "black",
              height: "24px",
              lineHeight: "24px",
              visibility:
                index >= currentValue - 2 && index <= currentValue + 2
                  ? "visible"
                  : "hidden",
              opacity: index === currentValue ? 1 : 0.5,
              fontSize: index === currentValue ? "24px" : "18px",
            }}
          >
            {String(value).padStart(2, "0")}
          </div>
        ))}
      </div>
    </div>
  );
}

Anyone know how can I fix or if there is a hidden npm package idk? Hopefully not, but if I don't find a solution then I will stick with select and option tags.


Solution

  • AntD component supports many components including timepicker. https://ant.design/components/time-picker

    if you check this, you can find the time-picker, which you can only select time.