From c673eb7843219233f981529f2e98b6910562c1fa Mon Sep 17 00:00:00 2001 From: Daniil Suvorov Date: Wed, 20 Sep 2023 20:36:04 +0300 Subject: [PATCH 1/4] fix(HorizontalScroll): support rtl - Fixes #5837 --- .../HorizontalScroll/HorizontalScroll.tsx | 49 +++++++++------ packages/vkui/src/hooks/useDirection.ts | 63 +++++++++++++++++++ 2 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 packages/vkui/src/hooks/useDirection.ts diff --git a/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx b/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx index fece771cda..949521d28b 100644 --- a/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx +++ b/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { classNames, noop } from '@vkontakte/vkjs'; import { useAdaptivityHasPointer } from '../../hooks/useAdaptivityHasPointer'; +import { useDirection } from '../../hooks/useDirection'; import { useEventListener } from '../../hooks/useEventListener'; import { useExternRef } from '../../hooks/useExternRef'; import { easeInOutSine } from '../../lib/fx'; @@ -14,7 +15,7 @@ interface ScrollContext { scrollAnimationDuration: number; animationQueue: VoidFunction[]; getScrollPosition: (currentPosition: number) => number; - onScrollToRightBorder: VoidFunction; + onScrollToEndBorder: VoidFunction; onScrollEnd: VoidFunction; onScrollStart: VoidFunction; /** @@ -22,6 +23,7 @@ interface ScrollContext { * В некоторых случаях может отличаться от текущей ширины прокрутки из-за transforms: translate */ initialScrollWidth: number; + textDirection: 'ltr' | 'rtl'; } export type ScrollPositionHandler = (currentPosition: number) => number; @@ -75,29 +77,31 @@ function doScroll({ scrollElement, getScrollPosition, animationQueue, - onScrollToRightBorder, + onScrollToEndBorder, onScrollEnd, onScrollStart, initialScrollWidth, scrollAnimationDuration = SCROLL_ONE_FRAME_TIME, + textDirection, }: ScrollContext) { if (!scrollElement || !getScrollPosition) { return; } /** - * максимальное значение сдвига влево + * крайнее значение сдвига */ - const maxLeft = initialScrollWidth - scrollElement.offsetWidth; + const extremeScrollLeft = + (textDirection === 'ltr' ? 1 : -1) * (initialScrollWidth - scrollElement.offsetWidth); - let startLeft = roundUpElementScrollLeft(scrollElement); - let endLeft = getScrollPosition(startLeft); + let startScrollLeft = roundUpElementScrollLeft(scrollElement); + let endScrollLeft = getScrollPosition(startScrollLeft); onScrollStart(); - if (endLeft >= maxLeft) { - onScrollToRightBorder(); - endLeft = maxLeft; + if (Math.abs(endScrollLeft) >= Math.abs(extremeScrollLeft)) { + onScrollToEndBorder(); + endScrollLeft = extremeScrollLeft; } const startTime = now(); @@ -113,10 +117,12 @@ function doScroll({ const value = easeInOutSine(elapsed); - const currentLeft = startLeft + (endLeft - startLeft) * value; - scrollElement.scrollLeft = Math.ceil(currentLeft); + const currentScrollLeft = startScrollLeft + (endScrollLeft - startScrollLeft) * value; + scrollElement.scrollLeft = Math.ceil(currentScrollLeft); - if (roundUpElementScrollLeft(scrollElement) !== Math.max(0, endLeft) && elapsed !== 1) { + const scrollEnd = + textDirection === 'ltr' ? Math.max(0, endScrollLeft) : Math.min(0, endScrollLeft); + if (roundUpElementScrollLeft(scrollElement) !== scrollEnd && elapsed !== 1) { requestAnimationFrame(scroll); return; } @@ -146,10 +152,14 @@ export const HorizontalScroll = ({ }: HorizontalScrollProps) => { const [canScrollLeft, setCanScrollLeft] = React.useState(false); const [canScrollRight, setCanScrollRight] = React.useState(false); + const [directionRef, textDirection = 'ltr'] = useDirection(); + + const setCanScrollStart = textDirection === 'ltr' ? setCanScrollLeft : setCanScrollRight; + const setCanScrollEnd = textDirection === 'ltr' ? setCanScrollRight : setCanScrollLeft; const isCustomScrollingRef = React.useRef(false); - const scrollerRef = useExternRef(getRef); + const scrollerRef = useExternRef(getRef, directionRef); const animationQueue = React.useRef([]); @@ -164,18 +174,19 @@ export const HorizontalScroll = ({ scrollElement, getScrollPosition, animationQueue: animationQueue.current, - onScrollToRightBorder: () => setCanScrollRight(false), + onScrollToEndBorder: () => setCanScrollEnd(false), onScrollEnd: () => (isCustomScrollingRef.current = false), onScrollStart: () => (isCustomScrollingRef.current = true), initialScrollWidth: scrollElement?.firstElementChild?.scrollWidth || 0, scrollAnimationDuration, + textDirection, }), ); if (animationQueue.current.length === 1) { animationQueue.current[0](); } }, - [scrollAnimationDuration, scrollerRef], + [scrollerRef, scrollAnimationDuration, textDirection, setCanScrollEnd], ); const scrollToLeft = React.useCallback(() => { @@ -194,13 +205,13 @@ export const HorizontalScroll = ({ if (showArrows && hasPointer && scrollerRef.current && !isCustomScrollingRef.current) { const scrollElement = scrollerRef.current; - setCanScrollLeft(scrollElement.scrollLeft > 0); - setCanScrollRight( - roundUpElementScrollLeft(scrollElement) + scrollElement.offsetWidth < + setCanScrollStart(Math.abs(scrollElement.scrollLeft) > 0); + setCanScrollEnd( + Math.abs(roundUpElementScrollLeft(scrollElement)) + scrollElement.offsetWidth < scrollElement.scrollWidth, ); } - }, [hasPointer, scrollerRef, showArrows]); + }, [showArrows, hasPointer, scrollerRef, setCanScrollStart, setCanScrollEnd]); const scrollEvent = useEventListener('scroll', calculateArrowsVisibility); React.useEffect( diff --git a/packages/vkui/src/hooks/useDirection.ts b/packages/vkui/src/hooks/useDirection.ts new file mode 100644 index 0000000000..60391c87f8 --- /dev/null +++ b/packages/vkui/src/hooks/useDirection.ts @@ -0,0 +1,63 @@ +import React from 'react'; +import { useDOM } from '../lib/dom'; +import { useIsomorphicLayoutEffect } from '../lib/useIsomorphicLayoutEffect'; + +type Direction = 'ltr' | 'rtl'; +type WritingMode = 'horizontal-tb' | 'vertical-rl' | 'vertical-lr'; + +/** + * Определяет направление текста элемента. + * + * ## Ограничения + * + * - Не следит за изменением направлением. + * - Определяется только на второй рендер. + * + * ## Пример + * + * ```jsx + * import { strict as assert } from 'node:assert'; + * + * const Component = () => { + * const [ref, direction, writingMode] = useDirection(); + * + * React.useEffect(()=>{ + * if (!direction || !writingMode) { + * return + * } + * + * assert.equal(direction, 'ltr') + * assert.equal(writingMode, 'vertical-rl') + * }, [direction, writingMode]) + * + * return
我家没有电脑。
+ * } + * ``` + */ +export function useDirection(): [ + React.RefObject, + Direction | undefined, + WritingMode | undefined, +] { + const ref = React.useRef(null); + + const [direction, setDirection] = React.useState(undefined); + const [writingMode, setWritingMode] = React.useState(undefined); + + const { window } = useDOM(); + + const update = () => { + if (!window || !ref.current) { + return; + } + + const styleDeclaration = window.getComputedStyle(ref.current); + + setDirection(styleDeclaration.direction as Direction); + setWritingMode(styleDeclaration.writingMode as WritingMode); + }; + + useIsomorphicLayoutEffect(update, [window]); + + return [ref, direction, writingMode]; +} From 12a9158dcb1d3420ca41cecb58885878979d584b Mon Sep 17 00:00:00 2001 From: Daniil Suvorov Date: Wed, 27 Sep 2023 15:55:33 +0300 Subject: [PATCH 2/4] fix: add roundingAwayFromZero --- .../HorizontalScroll/HorizontalScroll.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx b/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx index 949521d28b..e837ce1ce7 100644 --- a/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx +++ b/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx @@ -60,11 +60,27 @@ function now() { return performance && performance.now ? performance.now() : Date.now(); } +/** + * Округление к большему по модулю + * + * ## Пример + * + * ```ts + * import { strict as assert } from 'node:assert'; + * + * assert.equal(roundingAwayFromZero(5.1), 6) + * assert.equal(roundingAwayFromZero(-5.1), -6) + * ``` + */ +function roundingAwayFromZero(value: number): number { + return value > 0 ? Math.ceil(value) : Math.floor(value); +} + /** * Округляем el.scrollLeft * https://github.com/VKCOM/VKUI/pull/2445 */ -const roundUpElementScrollLeft = (el: HTMLElement) => Math.ceil(el.scrollLeft); +const roundUpElementScrollLeft = (el: HTMLElement) => roundingAwayFromZero(el.scrollLeft); /** * Код анимации скрола, на основе полифила: https://github.com/iamdustan/smoothscroll @@ -118,7 +134,7 @@ function doScroll({ const value = easeInOutSine(elapsed); const currentScrollLeft = startScrollLeft + (endScrollLeft - startScrollLeft) * value; - scrollElement.scrollLeft = Math.ceil(currentScrollLeft); + scrollElement.scrollLeft = roundingAwayFromZero(currentScrollLeft); const scrollEnd = textDirection === 'ltr' ? Math.max(0, endScrollLeft) : Math.min(0, endScrollLeft); @@ -205,7 +221,7 @@ export const HorizontalScroll = ({ if (showArrows && hasPointer && scrollerRef.current && !isCustomScrollingRef.current) { const scrollElement = scrollerRef.current; - setCanScrollStart(Math.abs(scrollElement.scrollLeft) > 0); + setCanScrollStart(scrollElement.scrollLeft !== 0); setCanScrollEnd( Math.abs(roundUpElementScrollLeft(scrollElement)) + scrollElement.offsetWidth < scrollElement.scrollWidth, From 670d04296830dd7802cd0b39ab301798d1573c7a Mon Sep 17 00:00:00 2001 From: Daniil Suvorov Date: Tue, 3 Oct 2023 18:22:28 +0300 Subject: [PATCH 3/4] fix: check overflow zero --- .../src/components/HorizontalScroll/HorizontalScroll.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx b/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx index e837ce1ce7..3e8ec31117 100644 --- a/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx +++ b/packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx @@ -115,6 +115,13 @@ function doScroll({ onScrollStart(); + /** + * Если окончание прокрутки вышло за ноль + */ + if (startScrollLeft * endScrollLeft < 0) { + endScrollLeft = 0; + } + if (Math.abs(endScrollLeft) >= Math.abs(extremeScrollLeft)) { onScrollToEndBorder(); endScrollLeft = extremeScrollLeft; From 07cb92bd21c76b9ef2e8e84cdbf8053f02f22fd2 Mon Sep 17 00:00:00 2001 From: Daniil Suvorov Date: Tue, 3 Oct 2023 18:31:28 +0300 Subject: [PATCH 4/4] style: fix --- packages/vkui/src/hooks/useDirection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vkui/src/hooks/useDirection.ts b/packages/vkui/src/hooks/useDirection.ts index 60391c87f8..07012d30cb 100644 --- a/packages/vkui/src/hooks/useDirection.ts +++ b/packages/vkui/src/hooks/useDirection.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { useDOM } from '../lib/dom'; import { useIsomorphicLayoutEffect } from '../lib/useIsomorphicLayoutEffect';