diff --git a/client/package-lock.json b/client/package-lock.json index be3aad1a..acb84d57 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,7 +15,8 @@ "react-dom": "^18.2.0", "react-icons": "^4.11.0", "react-router-dom": "^6.14.1", - "styled-components": "^6.0.2" + "styled-components": "^6.0.2", + "yozm-cafe-react-scroll-snap": "^0.0.3" }, "devDependencies": { "@babel/core": "^7.22.5", @@ -22765,6 +22766,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yozm-cafe-react-scroll-snap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/yozm-cafe-react-scroll-snap/-/yozm-cafe-react-scroll-snap-0.0.3.tgz", + "integrity": "sha512-c7M5qRObqWe1Pk52LXfLBUQqcuNnUu2KhpX7FsQZT7Z1BScBOpEtDIyWlze915j3om9M2vxLhWfjGFCN+JJ04Q==", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + "styled-components": "^6.0.0" + } } } } diff --git a/client/package.json b/client/package.json index 1bf752dc..6da673ca 100644 --- a/client/package.json +++ b/client/package.json @@ -25,7 +25,8 @@ "react-dom": "^18.2.0", "react-icons": "^4.11.0", "react-router-dom": "^6.14.1", - "styled-components": "^6.0.2" + "styled-components": "^6.0.2", + "yozm-cafe-react-scroll-snap": "^0.0.3" }, "devDependencies": { "@babel/core": "^7.22.5", diff --git a/client/src/components/CafeDetailBottomSheet.tsx b/client/src/components/CafeDetailBottomSheet.tsx index 71092469..acba492b 100644 --- a/client/src/components/CafeDetailBottomSheet.tsx +++ b/client/src/components/CafeDetailBottomSheet.tsx @@ -1,13 +1,13 @@ import { Suspense, useEffect } from 'react'; import { BsX } from 'react-icons/bs'; import { styled } from 'styled-components'; +import { useScrollSnapGuard } from 'yozm-cafe-react-scroll-snap'; import useCafeMenus from '../hooks/useCafeMenus'; import type { Theme } from '../styles/theme'; import type { Cafe } from '../types'; import CafeMenuMiniList from './CafeMenuMiniList'; import OpeningHoursDetail from './OpeningHoursDetail'; import QueryErrorBoundary from './QueryErrorBoundary'; -import useScrollSnapGuard from './ScrollSnap/hooks/useScrollSnapGuard'; type CafeDetailBottomSheetProps = { cafe: Cafe; diff --git a/client/src/components/CafeMenuBottomSheet.tsx b/client/src/components/CafeMenuBottomSheet.tsx index ddf3f1f9..0233d653 100644 --- a/client/src/components/CafeMenuBottomSheet.tsx +++ b/client/src/components/CafeMenuBottomSheet.tsx @@ -2,6 +2,7 @@ import { Suspense, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { BsX } from 'react-icons/bs'; import { styled } from 'styled-components'; +import { useScrollSnapGuard } from 'yozm-cafe-react-scroll-snap'; import useCafeMenus from '../hooks/useCafeMenus'; import type { Theme } from '../styles/theme'; import type { Cafe } from '../types'; @@ -9,7 +10,6 @@ import Resource from '../utils/Resource'; import CafeMenuList from './CafeMenuList'; import ImageModal from './ImageModal'; import QueryErrorBoundary from './QueryErrorBoundary'; -import useScrollSnapGuard from './ScrollSnap/hooks/useScrollSnapGuard'; type CafeMenuBottomSheetProps = { cafe: Cafe; diff --git a/client/src/components/ImageModal.tsx b/client/src/components/ImageModal.tsx index 81dd7677..ef59774a 100644 --- a/client/src/components/ImageModal.tsx +++ b/client/src/components/ImageModal.tsx @@ -1,8 +1,8 @@ import type { MouseEventHandler } from 'react'; import { useState } from 'react'; import { styled } from 'styled-components'; +import { useScrollSnapGuard } from 'yozm-cafe-react-scroll-snap'; import Resource from '../utils/Resource'; -import useScrollSnapGuard from './ScrollSnap/hooks/useScrollSnapGuard'; type ImageModalProps = { imageUrls: string[]; diff --git a/client/src/components/ScrollSnap/components/ScrollSnap.tsx b/client/src/components/ScrollSnap/components/ScrollSnap.tsx deleted file mode 100644 index fae7a48f..00000000 --- a/client/src/components/ScrollSnap/components/ScrollSnap.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { ScrollSnapProps } from '../types'; -import ScrollSnapCSS from './ScrollSnapCSS/ScrollSnapCSS'; -import ScrollSnapImpl from './ScrollSnapImpl/ScrollSnapImpl'; - -const isMobile = /(iPad)|(iPhone)|(iPod)|(android)|(webOS)/i.test(window.navigator.userAgent); - -const isIOS = /(iPad)|(iPhone)|(iPod)/i.test(window.navigator.userAgent); - -type ScrollSnapWithMode = ScrollSnapProps & { - // auto일 시 userAgent의 값에 따라 mode를 결정 - // css일 시 ScrollSnapCSS 사용 - // impl일 시 ScrollSnapImpl 사용 - mode?: 'auto' | 'css' | 'impl'; -}; - -const ScrollSnap = (props: ScrollSnapWithMode) => { - const { mode = 'auto', ...restProps } = props; - - if (mode === 'auto') { - if (isIOS) return ; - if (isMobile) return ; - - return ; - } - if (mode === 'css') { - return ; - } - return ; -}; - -export default ScrollSnap; diff --git a/client/src/components/ScrollSnap/components/ScrollSnapCSS/ScrollSnapCSS.tsx b/client/src/components/ScrollSnap/components/ScrollSnapCSS/ScrollSnapCSS.tsx deleted file mode 100644 index 7c0e5af5..00000000 --- a/client/src/components/ScrollSnap/components/ScrollSnapCSS/ScrollSnapCSS.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import type { UIEventHandler } from 'react'; -import { useLayoutEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; -import type { ScrollSnapProps } from '../../types'; -import ScrollSnapCSSItemList from './ScrollSnapCSSItemList'; - -// CSS scroll-snap의 scrollPosition과 parent에서 내려준 scrollPosition의 불일치를 -// 어느정도 허용할 것인지 설정 -// 값이 0일 경우 오차를 허용하지 않음 -const SCROLL_UNMATCH_TOLERANCE = 0.1; - -// CSS scroll-snap에서 나타나는 glitch를 고치기 위한 상수 -// 스크롤이 너무 많이 바뀌었을 경우 캔슬한다 -const SCROLL_SNAP_GLITCH_RANGE = 1; - -const ScrollSnapCSS = (props: ScrollSnapProps) => { - const { - activeIndex, - onActiveIndexChange, - scrollPosition: parentScrollPosition, - onScrollPositionChange, - items, - itemRenderer, - enableRolling, - timingFn, - ...divProps - } = props; - const [clientScrollPosition, setClientScrollPosition] = useState(parentScrollPosition); - const [shouldSync, setShouldSync] = useState(true); - - // 마지막 아이템에서 첫 번째 아이템으로 넘어가려고 할 때 허용할 지, 허용하지 않을 지 - // 여부를 결정하는 함수 - const clampPosition = (position: number) => Math.max(0, Math.min(position, items.length - 1)); - - const setActiveIndex = (activePosition: number) => { - onActiveIndexChange(clampPosition(activePosition)); - }; - - const containerRef = useRef(null); - - const getDomScrollPosition = () => { - const $element = containerRef.current; - if (!($element instanceof HTMLDivElement)) { - return clientScrollPosition; - } - return $element.scrollTop / $element.clientHeight; - }; - - const handleScroll: UIEventHandler = (event) => { - const domScrollPosition = getDomScrollPosition(); - const refinedPosition = - Math.abs(domScrollPosition - Math.round(domScrollPosition)) < 0.01 - ? Math.round(domScrollPosition) - : domScrollPosition; - - // A glitch has been detected. However, fixing this may actually impair the user experience, - // so it has been commented out. - // - // const seemsGlitch = Math.abs(clientScrollPosition - refinedPosition) > SCROLL_SNAP_GLITCH_RANGE; - // if (seemsGlitch) { - // setShouldSync(true); - // return; - // } - setActiveIndex(Math.round(refinedPosition)); - onScrollPositionChange(clampPosition(refinedPosition)); - - setClientScrollPosition(refinedPosition); - setShouldSync(false); - }; - - useLayoutEffect(() => { - const $element = containerRef.current; - if (!($element instanceof HTMLDivElement)) return; - - if (shouldSync) { - $element.scrollTo({ top: $element.clientHeight * clientScrollPosition }); - setShouldSync(false); - } - }, [clientScrollPosition, shouldSync]); - - // 부모에서 내려준 scroll position이 DOM의 scroll position과 - // 불일치가 심한지 여부 - const shouldSyncWithParent = () => { - const domScrollPosition = getDomScrollPosition(); - return Math.abs(domScrollPosition - parentScrollPosition) > SCROLL_UNMATCH_TOLERANCE; - }; - - // DOM scroll position과 parent에서 내려준 scroll position이 mismatch할 때 - // scroll을 강제로 이동시킬 필요가 있음 - if (shouldSyncWithParent() && !shouldSync) { - setClientScrollPosition(parentScrollPosition); - setShouldSync(true); - } - - return ( - - - - ); -}; - -export default ScrollSnapCSS; - -const Container = styled.div` - scroll-snap-type: y mandatory; - overflow-y: scroll; - width: 100%; - height: 100%; - - & > * { - scroll-snap-align: start; - scroll-snap-stop: always; - } -`; diff --git a/client/src/components/ScrollSnap/components/ScrollSnapCSS/ScrollSnapCSSItemList.tsx b/client/src/components/ScrollSnap/components/ScrollSnapCSS/ScrollSnapCSSItemList.tsx deleted file mode 100644 index 5b7f222c..00000000 --- a/client/src/components/ScrollSnap/components/ScrollSnapCSS/ScrollSnapCSSItemList.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import ScrollSnapItem from '../ScrollSnapItem'; - -// 음수 mod 시 양수 값을 얻기 위한 함수 -const mod = (n: number, m: number) => ((n % m) + m) % m; - -type ScrollSnapCSSItemListProps = { - scrollPosition: number; - items: Item[]; - itemRenderer: (item: Item, index: number) => React.ReactNode; - // focus된 아이템에서 일정 범위만큼 아이템을 렌더링 - renderDistance?: number; -}; - -/** - * DOM scroll box에 의존적인 리스트 - * - * CSS scroll-snap 사용 시 이 컴포넌트를 사용하여야 한다 - */ -const ScrollSnapCSSItemList = (props: ScrollSnapCSSItemListProps) => { - const { scrollPosition, items, itemRenderer, renderDistance = 2 } = props; - - const focusedIndex = mod(Math.floor(scrollPosition), items.length); - const visiblePosition = mod(scrollPosition, 1); - - return items.map((item, index) => { - const offset = index - focusedIndex; - - return ( - - {Math.abs(offset) <= renderDistance && itemRenderer(item, index)} - - ); - }); -}; - -export default ScrollSnapCSSItemList; diff --git a/client/src/components/ScrollSnap/components/ScrollSnapImpl/ScrollSnapImpl.tsx b/client/src/components/ScrollSnap/components/ScrollSnapImpl/ScrollSnapImpl.tsx deleted file mode 100644 index d98b72e3..00000000 --- a/client/src/components/ScrollSnap/components/ScrollSnapImpl/ScrollSnapImpl.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import type { MouseEventHandler, TouchEventHandler } from 'react'; -import { useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; -import useEffectEvent from '../../../../shims/useEffectEvent'; -import { easeOutExpo } from '../../../../utils/timingFunctions'; -import type { ScrollSnapProps } from '../../types'; -import ScrollSnapImplItemList from './ScrollSnapImplItemList'; - -// 터치 스와이프가 수직 혹은 수평 방향인지 판단할 때 샘플링하는 거리 -const SWIPE_TOUCH_DECISION_RADIUS = 4; - -// 스와이프 시 다음 아이템으로 이동하는 시간(ms) -const SWIPE_SNAP_TRANSITION_DURATION = 300; - -// 빠르게 휙 스와이프 시: -// 스와이프 판정을 위해 직전 {value}ms 터치 포인트와 비교 -const SWIPE_FAST_SCROLL_TIME_DURATION = 100; - -// 빠르게 휙 스와이프 시: -// 직전 터치 포인트와 비교하여 {value}% 만큼 이동하였다면 스와이프 처리 -// 작을 수록 짧게 스와이프해도 판정됨 -// 클 수록 넓게 스와이프해야 판정됨 -const SWIPE_FAST_SCROLL_DISTANCE_RATIO = 0.03; - -// 음수 mod 시 양수 값을 얻기 위한 함수 -const mod = (n: number, m: number) => ((n % m) + m) % m; - -type BoxModel = { - pageY: number; - height: number; -}; - -type TouchPoint = { - x: number; - y: number; -}; - -type MachineState = - // 아무런 동작도 하지 않는 상태 - | { - label: 'idle'; - } - // 터치 방향(좌우 혹은 상하)에 따라 스와이프를 해야할 지 결정하는 상태 - // 이 상태에선 실질적으로 스와이프가 동작하지 않는다 - | { - label: 'touchdecision'; - originTouchPoint: TouchPoint; - } - // 터치의 움직임에 따라 스크롤이 동작하는 상태 - | { - label: 'touchmove'; - prevPosition: number; - recentPositionHistory: { timestamp: number; position: number }[]; - } - // 터치가 끝났고 스크롤이 가장 가까운 스냅 포인트로 이동하는(애니메이션) 상태 - | { - label: 'snap'; - startedAt: number; - startedPosition: number; - }; - -const ScrollSnapImpl = (props: ScrollSnapProps) => { - const { - activeIndex, - onActiveIndexChange, - scrollPosition, - onScrollPositionChange, - items, - itemRenderer, - enableRolling, - timingFn = easeOutExpo, - ...divProps - } = props; - - const [, setNothing] = useState({}); - const rerender = () => setNothing({}); - - const [boxModel, setBoxModel] = useState(null); - - // 마지막 아이템에서 첫 번째 아이템으로 넘어가려고 할 때 허용할 지, 허용하지 않을 지 - // 여부를 결정하는 함수 - const clampPosition = enableRolling - ? (position: number) => mod(position, items.length) - : (position: number) => Math.max(0, Math.min(position, items.length - 1)); - - const setScrollPosition = (scrollPosition: number) => { - onScrollPositionChange(clampPosition(scrollPosition)); - }; - - const setActiveIndex = (activePosition: number) => { - onActiveIndexChange(clampPosition(activePosition)); - }; - - const containerRef = useRef(null); - - useEffect(() => { - const $container = containerRef.current; - if ($container === null) return; - - const observer = new ResizeObserver((entries) => { - const entry = entries.pop(); - if (!entry) return; - - const { y: pageY, height } = entry.contentRect; - setBoxModel({ pageY, height }); - }); - observer.observe($container); - - return () => observer.disconnect(); - }, []); - - const [machineState, setMachineState] = useState({ - label: 'idle', - }); - const transitionMachineState = (state: MachineState) => { - setMachineState(state); - }; - - // returns normalized by container box height. - // if touch is inside of screen, then 0.0 ~ 1.0 - // or outside, it can be negative or greater than 1.0 - const normalizePageY = (pageY: number): number => { - if (!boxModel) return 0; - - return (pageY - boxModel.pageY) / boxModel.height; - }; - - const onTouchStart = (touchPoint: TouchPoint) => { - transitionMachineState({ - label: 'touchdecision', - originTouchPoint: touchPoint, - }); - }; - - const onTouchDecision = (touchPoint: TouchPoint) => { - if (machineState.label !== 'touchdecision') return; - - // swipe decision - const { originTouchPoint } = machineState; - const [rx, ry] = [originTouchPoint.x - touchPoint.x, originTouchPoint.y - touchPoint.y].map(Math.abs); - if (Math.abs(rx) < SWIPE_TOUCH_DECISION_RADIUS || Math.abs(ry) < SWIPE_TOUCH_DECISION_RADIUS) { - return; - } - - // not my direction - if (ry < rx) { - transitionMachineState({ label: 'idle' }); - return; - } - - const position = normalizePageY(touchPoint.y); - // transition state to 'touchmove' - transitionMachineState({ - label: 'touchmove', - prevPosition: position, - recentPositionHistory: [{ timestamp: Date.now(), position }], - }); - }; - - const onTouchMove = (touchPoint: TouchPoint) => { - if (machineState.label === 'touchdecision') { - onTouchDecision(touchPoint); - return; - } - - if (machineState.label !== 'touchmove') return; - - const { prevPosition, recentPositionHistory } = machineState; - const position = normalizePageY(touchPoint.y); - - // process swipe movement - const delta = prevPosition - position; - setScrollPosition(scrollPosition + delta); - - // transition itself - transitionMachineState({ - label: 'touchmove', - prevPosition: position, - recentPositionHistory: [...recentPositionHistory, { timestamp: Date.now(), position }].filter( - ({ timestamp }) => Date.now() - timestamp < SWIPE_FAST_SCROLL_TIME_DURATION, - ), - }); - }; - - const onTouchEnd = () => { - onSnapStart(); - - if (machineState.label !== 'touchmove') return; - - const { prevPosition, recentPositionHistory } = machineState; - - const recentPosition = recentPositionHistory[0]?.position; - if (!recentPosition) return; - - const accelDelta = recentPosition - prevPosition; - - if (Math.abs(accelDelta) > SWIPE_FAST_SCROLL_DISTANCE_RATIO) { - const positionOffset = accelDelta > 0 ? 1 : -1; - setActiveIndex(activeIndex + positionOffset); - return; - } - setActiveIndex(Math.round(scrollPosition)); - }; - - const onSnapStart = () => { - if (machineState.label === 'snap') return; - - // transition state to 'snap' - transitionMachineState({ - label: 'snap', - startedAt: Date.now(), - startedPosition: scrollPosition, - }); - onSnap(); - }; - - const onSnap = () => { - if (machineState.label !== 'snap') return; - - // transition 'snap' itself - const { startedAt, startedPosition } = machineState; - const x = (Date.now() - startedAt) / SWIPE_SNAP_TRANSITION_DURATION; - if (x > 1) { - // transition to 'idle' - setScrollPosition(activeIndex); - transitionMachineState({ label: 'idle' }); - return; - } - - // 정방향 거리와 역방향 거리를 비교하여 가까운 거리로 transition - const candidateDistance1 = activeIndex - startedPosition; - const candidateDistance2 = (items.length - Math.abs(candidateDistance1)) * (candidateDistance1 < 0 ? 1 : -1); - - const transitionDistance = - Math.abs(candidateDistance1) < Math.abs(candidateDistance2) ? candidateDistance1 : candidateDistance2; - - const processTransition = () => { - // scrollTo(startedPosition + timingFn(x) * transitionDistance); - setScrollPosition(startedPosition + timingFn(x) * transitionDistance); - rerender(); // force trigger re-render - }; - requestAnimationFrame(processTransition); - }; - - if (machineState.label === 'snap') onSnap(); - - const handleTouchStart: TouchEventHandler = (event) => { - const touch = event.touches[0]; - if (!touch) return; - event.stopPropagation(); - - onTouchStart({ x: touch.pageX, y: touch.pageY }); - }; - - const handleTouchMove: TouchEventHandler = (event) => { - const touch = event.touches[0]; - if (!touch) return; - event.stopPropagation(); - - onTouchMove({ x: touch.pageX, y: touch.pageY }); - }; - - const handleTouchEnd: TouchEventHandler = (event) => { - event.stopPropagation(); - onTouchEnd(); - }; - - const handleMouseDown: MouseEventHandler = (event) => { - event.stopPropagation(); - onTouchStart({ x: event.pageX, y: event.pageY }); - }; - - const handleMouseMove: MouseEventHandler = (event) => { - event.stopPropagation(); - onTouchMove({ x: event.pageX, y: event.pageY }); - }; - - const handleMouseUp: MouseEventHandler = (event) => { - event.stopPropagation(); - onTouchEnd(); - }; - - const handleMouseLeave: MouseEventHandler = (event) => { - event.stopPropagation(); - onTouchEnd(); - }; - - const handleKeyDown = useEffectEvent((event: KeyboardEvent) => { - switch (event.key) { - case 'ArrowUp': - setActiveIndex(activeIndex - 1); - onSnapStart(); - break; - case 'ArrowDown': - setActiveIndex(activeIndex + 1); - onSnapStart(); - break; - } - }); - - useEffect(() => { - window.addEventListener('keydown', handleKeyDown); - - return () => window.removeEventListener('keydown', handleKeyDown); - }, []); - - return ( - - - - ); -}; - -const Container = styled.div` - cursor: grab; - user-select: none; - position: relative; - overflow-y: hidden; -`; - -export default ScrollSnapImpl; diff --git a/client/src/components/ScrollSnap/components/ScrollSnapImpl/ScrollSnapImplItemList.tsx b/client/src/components/ScrollSnap/components/ScrollSnapImpl/ScrollSnapImplItemList.tsx deleted file mode 100644 index 9cb41cfb..00000000 --- a/client/src/components/ScrollSnap/components/ScrollSnapImpl/ScrollSnapImplItemList.tsx +++ /dev/null @@ -1,61 +0,0 @@ -// 음수 mod 시 양수 값을 얻기 위한 함수 -const mod = (n: number, m: number) => ((n % m) + m) % m; - -type ScrollSnapImplItemListProps = { - scrollPosition: number; - items: Item[]; - itemRenderer: (item: Item, index: number) => React.ReactNode; - enableRolling?: boolean; -}; - -const ScrollSnapImplItemList = (props: ScrollSnapImplItemListProps) => { - const { scrollPosition, items, itemRenderer, enableRolling } = props; - - // position of item, which user sees - // always positive integer - // 현재 화면에 표시되고 있는 아이템의 index - const focusedIndex = mod(Math.floor(scrollPosition), items.length); - - const indexedItems = items.map((item, index) => ({ index, item })); - - // 현재 화면의 아이템 및 위, 아래의 아이템을 표시한다 - const visibleItems = enableRolling - ? [ - indexedItems.at(focusedIndex - 1), // 현재에서 상단 (혹은 끝) 아이템 - indexedItems.at(focusedIndex), // 현재 아이템 - indexedItems.at((focusedIndex + 1) % indexedItems.length), // 현재에서 하단 (혹은 첫) 아이템 - ] - : [ - indexedItems[focusedIndex - 1], // 현재에서 상단 아이템 - indexedItems[focusedIndex], // 현재 아이템 - indexedItems[focusedIndex + 1], // 현재에서 하단 아이템 - ]; - const visiblePosition = mod(scrollPosition, 1); - - return ( -
- {([0, 1, -1] as const) - .map((visibleIndex) => ({ ...visibleItems[1 + visibleIndex], visibleIndex })) - .map(({ item, index, visibleIndex }) => ( -
- {item && typeof index === 'number' && itemRenderer(item, index)} -
- ))} -
- ); -}; - -export default ScrollSnapImplItemList; diff --git a/client/src/components/ScrollSnap/components/ScrollSnapItem.tsx b/client/src/components/ScrollSnap/components/ScrollSnapItem.tsx deleted file mode 100644 index 990b2edc..00000000 --- a/client/src/components/ScrollSnap/components/ScrollSnapItem.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { PropsWithChildren } from 'react'; -import styled from 'styled-components'; - -type ScrollSnapItemProps = PropsWithChildren<{ - // 이전 아이템인지, 현재 아이템인지, 이후 아이템인지 여부를 나타내는 숫자 - // -1 | 0 | 1 혹은 number - offset: number; - // 0.0 ~ 1.0 - position: number; -}>; - -/** - * scroll snap 컨테이너에서 snap 처리가 될 아이템 - * - * 높이는 부모의 100%로 고정되어 있다 - */ -const ScrollSnapItem = (props: ScrollSnapItemProps) => { - const { offset, position, children } = props; - - return {children}; -}; - -export default ScrollSnapItem; - -const Container = styled.div` - height: 100%; -`; diff --git a/client/src/components/ScrollSnap/hooks/useScrollSnapGuard.ts b/client/src/components/ScrollSnap/hooks/useScrollSnapGuard.ts deleted file mode 100644 index a1915edc..00000000 --- a/client/src/components/ScrollSnap/hooks/useScrollSnapGuard.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { EventHandler, SyntheticEvent } from 'react'; -import { useCallback, useMemo } from 'react'; - -/** - * ScrollSnap의 하위 요소에서 열린 모달에서 스크롤 혹은 스와이프를 할 시 - * ScrollSnap에 전파되어 스와이프가 동작하는 문제가 발생합니다. - * ScrollSnap의 스와이프가 동작하지 않도록 갖가지 이벤트의 propagation을 막아주는 훅입니다. - */ -const useScrollSnapGuard = () => { - const preventPropagation: EventHandler = useCallback((event) => { - event.stopPropagation(); - }, []); - - const handlers = useMemo( - () => ({ - onTouchStart: preventPropagation, - onTouchMove: preventPropagation, - onTouchEnd: preventPropagation, - onMouseDown: preventPropagation, - onMouseMove: preventPropagation, - onMouseUp: preventPropagation, - onMouseLeave: preventPropagation, - // onWheel: preventPropagation, - }), - [preventPropagation], - ); - - return handlers; -}; - -export default useScrollSnapGuard; diff --git a/client/src/components/ScrollSnap/index.ts b/client/src/components/ScrollSnap/index.ts deleted file mode 100644 index b01e8789..00000000 --- a/client/src/components/ScrollSnap/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './components/ScrollSnap'; diff --git a/client/src/components/ScrollSnap/types.ts b/client/src/components/ScrollSnap/types.ts deleted file mode 100644 index 43fae2c4..00000000 --- a/client/src/components/ScrollSnap/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ComponentPropsWithoutRef } from 'react'; - -export type TimingFn = (x: number) => number; - -export type ScrollSnapProps = ComponentPropsWithoutRef<'div'> & { - // position of currently active item. (equiv with index) - // always 0..items.length (positive integer) - activeIndex: number; - onActiveIndexChange: (activeIndex: number) => void; - // scroll position of container - scrollPosition: number; - onScrollPositionChange: (scrollPosition: number) => void; - timingFn?: TimingFn; - items: Item[]; - itemRenderer: (item: Item, index: number) => React.ReactNode; - // enable continuity scrolling at the end of item - enableRolling?: boolean; -}; diff --git a/client/src/pages/HomePage.tsx b/client/src/pages/HomePage.tsx index 24daab61..0e07cf21 100644 --- a/client/src/pages/HomePage.tsx +++ b/client/src/pages/HomePage.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from 'react'; +import { ScrollSnap, ScrollSnapProvider, easeOutExpo } from 'yozm-cafe-react-scroll-snap'; import CafeCard from '../components/CafeCard'; -import ScrollSnap from '../components/ScrollSnap'; import useCafes from '../hooks/useCafes'; import type { Cafe } from '../types'; import { withGAEvent } from '../utils/GoogleAnalytics'; -import { easeOutExpo } from '../utils/timingFunctions'; const PREFETCH_OFFSET = 2; @@ -28,16 +27,17 @@ const HomePage = () => { useEffect(withGAEvent('cafe_view', { cafeName: cafes[activeIndex].name }), [activeIndex]); return ( - + > + + ); }; diff --git a/client/src/utils/timingFunctions.ts b/client/src/utils/timingFunctions.ts deleted file mode 100644 index 1f36297a..00000000 --- a/client/src/utils/timingFunctions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const easeOutExpo = (x: number): number => { - return x === 1 ? 1 : 1 - Math.pow(2, -10 * x); -};