From 115707ee189650b96270ac2c0c0a53a714c12d02 Mon Sep 17 00:00:00 2001 From: fluixyz-unnatural Date: Wed, 9 Aug 2023 14:36:44 +0900 Subject: [PATCH 1/2] fix(react-sandbox): use new storybook --- .../src/components/Carousel/index.story.tsx | 38 +++++-------------- .../src/components/Carousel/index.tsx | 5 +-- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/packages/react-sandbox/src/components/Carousel/index.story.tsx b/packages/react-sandbox/src/components/Carousel/index.story.tsx index ea4ad4c8e..21cc99c2e 100644 --- a/packages/react-sandbox/src/components/Carousel/index.story.tsx +++ b/packages/react-sandbox/src/components/Carousel/index.story.tsx @@ -1,6 +1,6 @@ -import { boolean, number, select, withKnobs } from '@storybook/addon-knobs' +import { number } from '@storybook/addon-knobs' import styled, { css } from 'styled-components' -import Carousel from '.' +import Carousel, { CarouselProps } from '.' const dummyText = css` color: ${({ theme }) => theme.color.text4}; @@ -19,43 +19,20 @@ const Dummy = styled.div` ` export default { title: 'Sandbox/Carousel', - decorators: [withKnobs], + component: Carousel, } -export const _Carousel = () => { - const hasGradient = boolean('Gradient', false) - const fadeInGradient = boolean('FadeInGradient', false) - const buttonOffset = number('buttonOffset', 0) - const buttonPadding = number('buttonPadding', 16) - const defaultScrollAlign = select( - 'scrollAlign', - { - Left: 'left', - Center: 'center', - Right: 'right', - }, - 'left' - ) - const defaultScrollOffset = number('scrollOffset', 0) +const DefaultStory = (args: CarouselProps) => { const itemCount = number('Item count', 20) const itemSize = number('Item size', 118) const items = Array.from({ length: itemCount }) return ( - + {items.map((_value, index) => ( - + Dummy ))} @@ -64,6 +41,9 @@ export const _Carousel = () => { ) } + +export const Default = DefaultStory.bind({}) + const Base = styled.div` width: 100%; padding: 0 108px; diff --git a/packages/react-sandbox/src/components/Carousel/index.tsx b/packages/react-sandbox/src/components/Carousel/index.tsx index 54fedfc59..0ec4f428a 100644 --- a/packages/react-sandbox/src/components/Carousel/index.tsx +++ b/packages/react-sandbox/src/components/Carousel/index.tsx @@ -38,7 +38,7 @@ export type CarouselGradientProps = type CarouselAppearanceProps = CarouselBaseAppearanceProps & CarouselGradientProps -type Props = CarouselAppearanceProps & { +export type CarouselProps = CarouselAppearanceProps & { onScroll?: (left: number) => void onResize?: (width: number) => void children: React.ReactNode @@ -63,9 +63,8 @@ export default function Carousel({ onScrollStateChange, scrollAmountCoef = SCROLL_AMOUNT_COEF, ...options -}: Props) { +}: CarouselProps) { // スクロール位置を保存する - // アニメーション中の場合は、アニメーション終了時のスクロール位置が保存される const [scrollLeft, setScrollLeft] = useDebounceAnimationState(0) // アニメーション中かどうか const animation = useRef(false) From 4be112d39a19a3c2d76ee9a0551d8a0f377e618f Mon Sep 17 00:00:00 2001 From: fluixyz-unnatural Date: Wed, 9 Aug 2023 14:40:41 +0900 Subject: [PATCH 2/2] fix(react-sandbox): change manual scroll behavior --- .../src/components/Carousel/index.tsx | 105 +++++++++--------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/packages/react-sandbox/src/components/Carousel/index.tsx b/packages/react-sandbox/src/components/Carousel/index.tsx index 0ec4f428a..310c3b335 100644 --- a/packages/react-sandbox/src/components/Carousel/index.tsx +++ b/packages/react-sandbox/src/components/Carousel/index.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { animated, useSpring } from 'react-spring' import styled, { css } from 'styled-components' import { useDebounceAnimationState } from '../../foundation/hooks' -import { passiveEvents, isEdge } from '../../foundation/support' +import { isEdge } from '../../foundation/support' import { useIsomorphicLayoutEffect } from '../../hooks' import CarouselButton, { Direction } from '../CarouselButton' @@ -66,18 +66,24 @@ export default function Carousel({ }: CarouselProps) { // スクロール位置を保存する const [scrollLeft, setScrollLeft] = useDebounceAnimationState(0) - // アニメーション中かどうか - const animation = useRef(false) + + // アニメーション中の場合はアニメーション終了時のスクロール位置を保持する + // それ以外の時は undefined + const [animationTarget, setAnimationTarget] = useState( + undefined + ) + // スクロール可能な領域を保存する const [maxScrollLeft, setMaxScrollLeft] = useState(0) + // 左右のボタンの表示状態を保存する const [leftShow, setLeftShow] = useState(false) const [rightShow, setRightShow] = useState(false) - // const [props, set, stop] = useSpring(() => ({ - // scroll: 0 - // })) - const [styles, set] = useSpring(() => ({ scroll: 0 })) + const [styles, set] = useSpring(() => ({ + scroll: 0, + onRest: () => setAnimationTarget(undefined), + })) const ref = useRef(null) const visibleAreaRef = useRef(null) @@ -88,61 +94,59 @@ export default function Carousel({ return } const { clientWidth } = visibleAreaRef.current + // アニメーション中は現在の終了地点から次の終了地点を計算する + const from = animationTarget ?? scrollLeft // スクロール領域を超えないように、アニメーションを開始 // アニメーション中にアニメーションが開始されたときに、アニメーション終了予定の位置から再度計算するようにする const scroll = Math.min( - scrollLeft + clientWidth * scrollAmountCoef, + from + clientWidth * scrollAmountCoef, maxScrollLeft ) - setScrollLeft(scroll, true) - set({ scroll, from: { scroll: scrollLeft }, reset: !animation.current }) - animation.current = true - }, [ - animation, - maxScrollLeft, - scrollLeft, - set, - scrollAmountCoef, - setScrollLeft, - ]) + setAnimationTarget(scroll) + set({ scroll, from: { scroll: scrollLeft } }) + }, [animationTarget, scrollLeft, scrollAmountCoef, maxScrollLeft, set]) const handleLeft = useCallback(() => { if (visibleAreaRef.current === null) { return } const { clientWidth } = visibleAreaRef.current - const scroll = Math.max(scrollLeft - clientWidth * scrollAmountCoef, 0) - setScrollLeft(scroll, true) - set({ scroll, from: { scroll: scrollLeft }, reset: !animation.current }) - animation.current = true - }, [animation, scrollLeft, set, scrollAmountCoef, setScrollLeft]) + const from = animationTarget ?? scrollLeft + const scroll = Math.max(from - clientWidth * scrollAmountCoef, 0) + setAnimationTarget(scroll) + set({ scroll, from: { scroll: scrollLeft } }) + }, [animationTarget, scrollLeft, scrollAmountCoef, set]) + + // ボタン以外からスクロールされた場合、ボタンによるアニメーションを中断する + const handleAnimationStop = useCallback(() => { + styles.scroll.stop() + }, [styles.scroll]) // スクロール可能な場合にボタンを表示する // scrollLeftが変化したときに処理する (アニメーション開始時 & 手動スクロール時) useEffect(() => { - const newleftShow = scrollLeft > 0 - const newrightShow = scrollLeft < maxScrollLeft && maxScrollLeft > 0 + // 左にスクロール可能 && アニメーション終了時も左にスクロール可能 + const newleftShow = + scrollLeft > 0 && (animationTarget === undefined || animationTarget > 0) + // 右にスクロール可能 && アニメーション終了時も右にスクロール可能 + const newrightShow = + scrollLeft < maxScrollLeft && + maxScrollLeft > 0 && + (animationTarget === undefined || animationTarget < maxScrollLeft) if (newleftShow !== leftShow || newrightShow !== rightShow) { setLeftShow(newleftShow) setRightShow(newrightShow) onScrollStateChange?.(newleftShow || newrightShow) } - }, [leftShow, maxScrollLeft, onScrollStateChange, rightShow, scrollLeft]) + }, [ + animationTarget, + leftShow, + maxScrollLeft, + onScrollStateChange, + rightShow, + scrollLeft, + ]) - const handleScroll = useCallback(() => { - if (ref.current === null) { - return - } - // 手動でスクロールが開始されたときにアニメーションを中断 - if (animation.current) { - styles.scroll.stop() - animation.current = false - } - // スクロール位置を保存 (アニメーションの基準になる) - const manualScrollLeft = ref.current.scrollLeft - // 過剰にsetStateが走らないようにDebouceする - setScrollLeft(manualScrollLeft) - }, [animation, setScrollLeft, styles]) // リサイズが起きたときに、アニメーション用のスクロール領域 & ボタンの表示状態 を再計算する const handleResize = useCallback(() => { @@ -164,12 +168,6 @@ export default function Carousel({ return } - elm.addEventListener( - 'wheel', - handleScroll, - passiveEvents() && { passive: true } - ) - const resizeObserver = new ResizeObserver(handleResize) resizeObserver.observe(elm) @@ -177,11 +175,10 @@ export default function Carousel({ resizeObserverInner.observe(innerElm) return () => { - elm.removeEventListener('wheel', handleScroll) resizeObserver.disconnect() resizeObserverInner.disconnect() } - }, [handleResize, handleScroll]) + }, [handleResize]) // 初期スクロールを行う useIsomorphicLayoutEffect(() => { @@ -215,7 +212,11 @@ export default function Carousel({ if (onScroll) { onScroll(ref.current.scrollLeft) } - }, [onScroll]) + // スクロール位置を保存 (アニメーションの基準になる) + const currentScrollLeft = ref.current.scrollLeft + // 過剰にsetStateが走らないようにDebouceする + setScrollLeft(currentScrollLeft) + }, [onScroll, setScrollLeft]) const [disableGradient, setDisableGradient] = useState(false) @@ -238,6 +239,8 @@ export default function Carousel({ ref={ref} scrollLeft={styles.scroll} onScroll={handleScrollMove} + // タップされた時を手動スクロール開始とみなして自動スクロールを止める + onTouchStart={handleAnimationStop} > {children} @@ -278,6 +281,8 @@ export default function Carousel({ ref={ref} scrollLeft={styles.scroll} onScroll={handleScrollMove} + // タップされた時を手動スクロール開始とみなして自動スクロールを止める + onTouchStart={handleAnimationStop} > {children}