Friday, 1 September 2023

Is there a better way to recognize and update the in-line style of an element during a swipe gesture?

I've been working on a left swipe gesture handler for bubbles in a chat. touchMove and touchStart would probably be the next step, but right now I'm trying to get this to work just for PC/web.

I kind of got it working, but I feel like there is a much better way, than just updating the inline transform style for each bubble. It doesn't feel quite as natural as I'd expect it to.

I've also tried a "hacky" way with scroll-snap, but I couldn't quite get it to work.

Ideally, I'd love a pure CSS solution, but I'll take what I can get. Is the below approach most likely the only one I can use?

distracted-haze-y5wtd4

// App.tsx
import * as React from "react";
import "./styles.css";
import { createSwipeHandlers } from "./createSwipeHandlers";

const BubbleRow = React.memo(
  ({
    messageText,
    setSelectedMessage,
    isLastBubble
  }: {
    messageText: string;
    setSelectedMessage: (messageText: string) => void;
    isLastBubble: boolean;
  }) => {
    const bubbleRef = React.useRef<HTMLDivElement>(null);

    const handleLeftSwipe = React.useCallback((dist: number) => {
      if (bubbleRef.current) {
        if (dist > 72) {
          //@ts-expect-error
          bubbleRef.current.style.transform = null;
          setSelectedMessage(messageText);
          return;
        }

        bubbleRef.current.style.transform = `translateX(-${dist}px)`;
      }
    }, []);

    const { mouseStart, mouseMove, mouseOut, mouseStop } = createSwipeHandlers({
      leftSwipeHandler: handleLeftSwipe,
      swipeStopHandler: (e: any) => {
        if (bubbleRef.current) {
          //@ts-expect-error
          bubbleRef.current.style.transform = null;
        }
      }
    });

    const className = `row${isLastBubble ? " last-bubble-row" : ""}`;
    return (
      <div className={className}>
        <div
          ref={bubbleRef}
          className={"bubble"}
          onMouseDown={mouseStart}
          onMouseMove={mouseMove}
          onMouseOut={mouseOut}
          onMouseLeave={mouseStop}
          onMouseUp={mouseStop}
        >
          {messageText}
        </div>
      </div>
    );
  }
);

const Footer = React.memo(
  ({
    selectedMessage,
    setSelectedMessage
  }: {
    selectedMessage: string | null;
    setSelectedMessage: (selectedMessage: null) => void;
  }) => {
    const clearSelectedMessage = React.useCallback(() => {
      setSelectedMessage(null);
    }, []);

    if (!selectedMessage) return null;

    return (
      <div key={selectedMessage} className="footer">
        <span>{selectedMessage}</span>
        <span onClick={clearSelectedMessage} className="clear-button">
          {"×"}
        </span>
      </div>
    );
  }
);

const numberOfChatBubbles = 5;
export const App = () => {
  const [selectedMessage, setSelectedMessage] = React.useState<string | null>(
    null
  );

  return (
    <div className="wrapper">
      <div className="container">
        {Array.from({ length: numberOfChatBubbles }, (_, index) => (
          <BubbleRow
            messageText={`Text for message ${index + 1}`}
            setSelectedMessage={setSelectedMessage}
            isLastBubble={index + 1 === numberOfChatBubbles}
          />
        ))}
        <Footer
          selectedMessage={selectedMessage}
          setSelectedMessage={setSelectedMessage}
        />
      </div>
    </div>
  );
};

const createSwipeHandlers = ({
  leftSwipeHandler,
  swipeStopHandler,
  minSwipeDist = 5
}: {
  leftSwipeHandler?: (dist: number) => void;
  swipeStopHandler?: (e: React.MouseEvent<HTMLDivElement>) => void;
  minSwipeDist?: number;
}) => {
  let xDown: number | null = null;
  let yDown: number | null = null;

  return {
    mouseStart: ((e: React.MouseEvent<HTMLDivElement>) => {
      xDown = e.clientX;
      yDown = e.clientY;
    }) as React.MouseEventHandler,
    mouseMove: ((e: React.MouseEvent<HTMLDivElement>) => {
      if (!xDown || !yDown) return;

      const xUp = e.clientX;
      const yUp = e.clientY;

      const xDiff = xDown - xUp;
      const xDiffAbs = Math.abs(xDiff);
      const yDiff = yDown - yUp;
      const yDiffAbs = Math.abs(yDiff);

      if (xDiffAbs > yDiffAbs && xDiff > 0 && xDiffAbs > minSwipeDist) {
        leftSwipeHandler?.(xDiffAbs);
      }
    }) as React.MouseEventHandler,
    mouseOut: ((e: React.MouseEvent<HTMLDivElement>) => {
      xDown = null;
      yDown = null;
    }) as React.MouseEventHandler,
    mouseStop: ((e: React.MouseEvent<HTMLDivElement>) => {
      xDown = null;
      yDown = null;
      swipeStopHandler?.(e);
    }) as React.MouseEventHandler
  };
};
// styles.css
* {
  outline: none;
  box-sizing: border-box;
  margin: 0;
}

.wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100vh;
}

.container {
  border: 1px solid black;
  height: 500px;
  width: 400px;
  display: flex;
  flex-direction: column;
}

.row {
  display: flex;
  justify-content: flex-end;
  width: 100%;
  padding: 8px;
}

.bubble {
  display: flex;
  align-items: center;
  background-color: gray;
  padding: 12px;
  border-radius: 12px 12px 3px 12px;
  transition: 0.04s linear;
  height: fit-content;
  user-select: none;
}

.last-bubble-row {
  flex-grow: 1;
}

@keyframes blinkFade {
  0%,
  100% {
    filter: brightness(1);
  }

  20% {
    filter: brightness(0.4);
  }
}

.footer {
  height: 64px;
  width: 100%;
  padding: 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  animation: blinkFade 1.5s;
  background-color: gray;
}

.clear-button {
  cursor: pointer;
  font-size: 32px;
}

Currently looks like this:

gif



from Is there a better way to recognize and update the in-line style of an element during a swipe gesture?

No comments:

Post a Comment