diff --git a/src/ui/Carousel/index.tsx b/src/ui/Carousel/index.tsx
index 70ed0af32..8cd540f46 100644
--- a/src/ui/Carousel/index.tsx
+++ b/src/ui/Carousel/index.tsx
@@ -1,10 +1,9 @@
import './index.scss';
-import React, { ReactElement, useRef, useState } from 'react';
-import { useMediaQueryContext } from '../../lib/MediaQueryContext';
+import React, { ReactElement, useEffect, useRef, useState } from 'react';
const PADDING_WIDTH = 24;
const CONTENT_LEFT_WIDTH = 40;
-const SWIPE_THRESHOLD = 30;
+const SWIPE_THRESHOLD = 15;
const LAST_ITEM_RIGHT_SNAP_THRESHOLD = 100;
interface ItemPosition {
@@ -28,11 +27,10 @@ function shouldRenderAsFixed(item: ReactElement) {
}
function CarouselItem({
- key,
item,
defaultWidth,
}: CarouselItemProps): ReactElement {
- return
+ return
{item}
;
}
@@ -41,14 +39,29 @@ interface CarouselProps {
id: string;
items: ReactElement[];
gap?: number;
+ classNameWithTouchAction?: string;
}
-export function Carousel({
+interface Position {
+ x: number;
+ y: number;
+}
+
+interface DraggingInfo {
+ scrolling: boolean;
+ dragging: boolean;
+ startPos: Position | null;
+ offset: number;
+ translateX: number;
+ currentIndex: number;
+}
+
+export const Carousel = React.memo(({
id,
items,
gap = 8,
-}: CarouselProps): ReactElement {
- const { isMobile } = useMediaQueryContext();
+ classNameWithTouchAction = 'sendbird-conversation__messages-padding',
+}: CarouselProps): ReactElement => {
const carouselRef = useRef
(null);
const screenWidth = window.innerWidth;
const defaultItemWidth = carouselRef.current?.clientWidth ?? 0;
@@ -63,119 +76,212 @@ export function Carousel({
const isLastItemNarrow = lastItemWidth <= LAST_ITEM_RIGHT_SNAP_THRESHOLD;
const isLastTwoItemsFitScreen = getIsLastTwoItemsFitScreen();
const itemPositions: ItemPosition[] = getEachItemPositions();
- const [currentIndex, setCurrentIndex] = useState(0);
- const [dragging, setDragging] = useState<'vertical' | 'horizontal' | null>(null);
- const [startX, setStartX] = useState(0);
- const [offset, setOffset] = useState(0);
- const [translateX, setTranslateX] = useState(0);
+ const [draggingInfo, setDraggingInfo] = useState({
+ scrolling: false,
+ dragging: false,
+ startPos: null,
+ offset: 0,
+ translateX: 0,
+ currentIndex: 0,
+ });
+
const handleMouseDown = (event: React.MouseEvent) => {
- event.preventDefault();
- setDragging('horizontal');
- setStartX(event.clientX);
+ setDraggingInfo({
+ ...draggingInfo,
+ scrolling: false,
+ dragging: true,
+ startPos: {
+ x: event.clientX,
+ y: event.clientY,
+ },
+ offset: 0,
+ });
};
const handleMouseMove = (event: React.MouseEvent) => {
- if (!dragging) return;
+ if (!draggingInfo.dragging) return;
const currentX = event.clientX;
- const newOffset = currentX - startX;
- setOffset(newOffset);
+ const newOffset = currentX - draggingInfo.startPos.x;
+ setDraggingInfo({
+ ...draggingInfo,
+ offset: newOffset,
+ });
};
const handleMouseUp = () => {
- if (!dragging) return;
- setDragging(null);
- onDragEnd();
+ if (!draggingInfo.dragging) return;
+ handleDragEnd();
+ };
+
+ const blockScroll = () => {
+ const parentElements = document.getElementsByClassName(classNameWithTouchAction);
+ const parentElement: HTMLElement = parentElements[0] as HTMLElement;
+ if (parentElement) {
+ parentElement.style.touchAction = 'pan-x';
+ }
+ };
+
+ const unblockScroll = () => {
+ const parentElements = document.getElementsByClassName(classNameWithTouchAction);
+ const parentElement: HTMLElement = parentElements[0] as HTMLElement;
+ if (parentElement) {
+ parentElement.style.touchAction = 'pan-y';
+ }
};
const handleTouchStart = (event: React.TouchEvent) => {
- setStartX(event.touches[0].clientX);
+ setDraggingInfo({
+ ...draggingInfo,
+ scrolling: false,
+ dragging: false,
+ startPos: {
+ x: event.touches[0].clientX,
+ y: event.touches[0].clientY,
+ },
+ offset: 0,
+ });
};
+ useEffect(() => {
+ if (draggingInfo.scrolling) {
+ unblockScroll();
+ }
+
+ }, [draggingInfo.scrolling]);
+
const handleTouchMove = (event: React.TouchEvent) => {
- if (!startX) return;
+ if (!draggingInfo.startPos || draggingInfo.scrolling) return;
+
+ const startPos = draggingInfo.startPos;
const touchMoveX = event.touches[0].clientX;
- const deltaX = Math.abs(touchMoveX - startX);
- const deltaY = Math.abs(event.touches[0].clientY - event.touches[event.touches.length - 1].clientY);
- const threshold = 5;
-
- if (dragging === 'horizontal' || (dragging !== 'vertical' && deltaX > deltaY + threshold)) {
- const parentElement = document.getElementsByClassName('sendbird-conversation__messages-padding');
- (parentElement[0] as HTMLElement).style.overflowY = 'hidden';
- if (dragging !== 'horizontal') setDragging('horizontal');
- const newOffset = event.touches[0].clientX - startX;
- if (newOffset !== offset) setOffset(newOffset);
- } else if (dragging !== 'vertical') setDragging('vertical');
- };
+ const touchMoveY = event.touches[0].clientY;
+ const deltaX = Math.abs(touchMoveX - startPos.x);
+ const deltaY = Math.abs(touchMoveY - startPos.y);
+ const newOffset = touchMoveX - startPos.x;
- const handleTouchEnd = () => {
- if (dragging !== null) {
- setDragging(null);
+ if (newOffset === draggingInfo.offset) return;
+ if (draggingInfo.dragging) {
+ setDraggingInfo({
+ ...draggingInfo,
+ offset: newOffset,
+ });
+ return;
+ }
+ if (deltaY > deltaX) {
+ setDraggingInfo({
+ ...draggingInfo,
+ scrolling: true,
+ });
+ } else {
+ blockScroll();
+ setDraggingInfo({
+ ...draggingInfo,
+ dragging: true,
+ offset: newOffset,
+ });
}
- onDragEnd();
+ };
+
+ const getNewDraggingInfo = (props: { newTranslateX?: number, nextIndex?: number } = {}): DraggingInfo => {
+ const { newTranslateX, nextIndex } = props;
+ const { translateX, currentIndex } = draggingInfo;
+ return {
+ scrolling: false,
+ dragging: false,
+ startPos: null,
+ offset: 0,
+ translateX: newTranslateX ?? translateX,
+ currentIndex: nextIndex ?? currentIndex,
+ };
};
const handleDragEnd = () => {
+ const offset = draggingInfo.offset;
const absOffset = Math.abs(offset);
- if (absOffset >= SWIPE_THRESHOLD) {
- // If dragged to left, next index should be to the right
- if (offset < 0 && currentIndex < items.length - 1) {
- const nextIndex = currentIndex + 1;
- setTranslateX(itemPositions[nextIndex].start);
- setCurrentIndex(nextIndex);
+ if (absOffset < SWIPE_THRESHOLD) {
+ setDraggingInfo(getNewDraggingInfo());
+ return;
+ }
+ // If dragged to left, next index should be to the right
+ const currentIndex = draggingInfo.currentIndex;
+ if (offset < 0 && currentIndex < items.length - 1) {
+ const nextIndex = currentIndex + 1;
+ setDraggingInfo(getNewDraggingInfo({
+ newTranslateX: itemPositions[nextIndex].start,
+ nextIndex,
+ }));
// If dragged to right, next index should be to the left
- } else if (offset > 0 && currentIndex > 0) {
- const nextIndex = currentIndex - 1;
- setTranslateX(itemPositions[nextIndex].start);
- setCurrentIndex(nextIndex);
- }
+ } else if (offset > 0 && currentIndex > 0) {
+ const nextIndex = currentIndex - 1;
+ setDraggingInfo(getNewDraggingInfo({
+ newTranslateX: itemPositions[nextIndex].start,
+ nextIndex,
+ }));
+ } else {
+ setDraggingInfo(getNewDraggingInfo());
}
- setOffset(0);
};
-
- const handleDragEndForMobile = () => {
+ const handleTouchEnd = () => {
+ const { offset, currentIndex } = draggingInfo;
const absOffset = Math.abs(offset);
- if (absOffset >= SWIPE_THRESHOLD) {
- // If dragged to left, next index should be to the right
- if (offset < 0 && currentIndex < items.length - 1) {
- const nextIndex = currentIndex + 1;
- /**
- * This is special logic for "더 보기" button for Socar use-case.
- * The button will have a small width (less than 50px).
- * We want to include this button in the view and snap to right padding wall IFF !isLastTwoItemsFitScreen.
- */
- if (isLastItemNarrow) {
- if (isLastTwoItemsFitScreen) {
- if (nextIndex !== items.length - 1) {
- setTranslateX(itemPositions[nextIndex].start);
- setCurrentIndex(nextIndex);
- }
- } else if (nextIndex !== items.length - 1) {
- setTranslateX(itemPositions[nextIndex].start);
- setCurrentIndex(nextIndex);
+ if (absOffset < SWIPE_THRESHOLD) {
+ setDraggingInfo(getNewDraggingInfo());
+ return;
+ }
+ // If dragged to left, next index should be to the right
+ if (offset < 0 && currentIndex < items.length - 1) {
+ const nextIndex = currentIndex + 1;
+ /**
+ * This is special logic for "더 보기" button for Socar use-case.
+ * The button will have a small width (less than 50px).
+ * We want to include this button in the view and snap to right padding wall IFF !isLastTwoItemsFitScreen.
+ */
+ if (isLastItemNarrow) {
+ if (isLastTwoItemsFitScreen) {
+ if (nextIndex !== items.length - 1) {
+ setDraggingInfo(getNewDraggingInfo({
+ newTranslateX: itemPositions[nextIndex].start,
+ nextIndex,
+ }));
} else {
- const translateWidth = itemPositions[nextIndex].start - lastItemWidth;
- const rightEmptyWidth = screenWidth - (allItemsWidth + translateWidth + PADDING_WIDTH + CONTENT_LEFT_WIDTH);
- setTranslateX(translateWidth + rightEmptyWidth);
- setCurrentIndex(nextIndex);
+ setDraggingInfo(getNewDraggingInfo());
}
+ } else if (nextIndex !== items.length - 1) {
+ setDraggingInfo(getNewDraggingInfo({
+ newTranslateX: itemPositions[nextIndex].start,
+ nextIndex,
+ }));
} else {
- setTranslateX(itemPositions[nextIndex].start);
- setCurrentIndex(nextIndex);
+ const translateWidth = itemPositions[nextIndex].start - lastItemWidth;
+ const rightEmptyWidth = screenWidth - (allItemsWidth + translateWidth + PADDING_WIDTH + CONTENT_LEFT_WIDTH);
+ setDraggingInfo(getNewDraggingInfo({
+ newTranslateX: translateWidth + rightEmptyWidth,
+ nextIndex,
+ }));
}
- // If dragged to right, next index should be to the left
- } else if (offset > 0 && currentIndex > 0) {
- const nextIndex = currentIndex - 1;
- setTranslateX(itemPositions[nextIndex].start);
- setCurrentIndex(nextIndex);
+ } else {
+ setDraggingInfo(getNewDraggingInfo({
+ newTranslateX: itemPositions[nextIndex].start,
+ nextIndex,
+ }));
}
+ // If dragged to right, next index should be to the left
+ } else if (offset > 0 && currentIndex > 0) {
+ const nextIndex = currentIndex - 1;
+ setDraggingInfo(getNewDraggingInfo({
+ newTranslateX: itemPositions[nextIndex].start,
+ nextIndex,
+ }));
+ } else {
+ setDraggingInfo(getNewDraggingInfo());
+ }
+ if (draggingInfo.dragging) {
+ unblockScroll();
}
- setOffset(0);
- const parentElement = document.getElementsByClassName('sendbird-conversation__messages-padding');
- (parentElement[0] as HTMLElement).style.overflowY = 'scroll';
};
function getCurrentTranslateX() {
- return translateX + offset;
+ return draggingInfo.translateX + draggingInfo.offset;
}
function getIsLastTwoItemsFitScreen() {
@@ -184,9 +290,6 @@ export function Carousel({
return restTotalWidth <= screenWidth;
}
- const onDragEnd = isMobile ? handleDragEndForMobile : handleDragEnd;
- const currentTranslateX = getCurrentTranslateX();
-
function getEachItemPositions(): ItemPosition[] {
let accumulator = 0;
return itemWidths.map((itemWidth, i): ItemPosition => {
@@ -206,9 +309,6 @@ export function Carousel({
-
- {items.map((item, index) => (
-
- ))}
-
+ {items.map((item, index) => (
+
+ ))}
);
-}
+});
export default Carousel;
diff --git a/src/ui/MessageTemplate/index.scss b/src/ui/MessageTemplate/index.scss
index 3fe0415aa..21bbd3c07 100644
--- a/src/ui/MessageTemplate/index.scss
+++ b/src/ui/MessageTemplate/index.scss
@@ -3,5 +3,6 @@
}
.sendbird-message-template__root {
- border-radius: 0;
+ border-radius: 0;
+ font-family: var(--sendbird-font-family-default);
}
\ No newline at end of file