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