Skip to content

Commit

Permalink
fix(HorizontalScroll): support rtl
Browse files Browse the repository at this point in the history
- Fixes #5837
  • Loading branch information
SevereCloud committed Sep 20, 2023
1 parent 9525ded commit 2d20d37
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 19 deletions.
49 changes: 30 additions & 19 deletions packages/vkui/src/components/HorizontalScroll/HorizontalScroll.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,14 +15,15 @@ interface ScrollContext {
scrollAnimationDuration: number;
animationQueue: VoidFunction[];
getScrollPosition: (currentPosition: number) => number;
onScrollToRightBorder: VoidFunction;
onScrollToEndBorder: VoidFunction;
onScrollEnd: VoidFunction;
onScrollStart: VoidFunction;
/**
* Начальная ширина прокрутки.
* В некоторых случаях может отличаться от текущей ширины прокрутки из-за transforms: translate
*/
initialScrollWidth: number;
textDirection: 'ltr' | 'rtl';
}

export type ScrollPositionHandler = (currentPosition: number) => number;
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}
Expand Down Expand Up @@ -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<HTMLDivElement>();

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<VoidFunction[]>([]);

Expand All @@ -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(() => {
Expand All @@ -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(
Expand Down
63 changes: 63 additions & 0 deletions packages/vkui/src/hooks/useDirection.ts
Original file line number Diff line number Diff line change
@@ -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 <div ref={ref} style={{writingMode: 'vertical-rl'}}>我家没有电脑。</div>
* }
* ```
*/
export function useDirection<T extends HTMLElement>(): [
React.RefObject<T>,
Direction | undefined,
WritingMode | undefined,
] {
const ref = React.useRef<T>(null);

const [direction, setDirection] = React.useState<Direction | undefined>(undefined);
const [writingMode, setWritingMode] = React.useState<WritingMode | undefined>(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];
}

0 comments on commit 2d20d37

Please sign in to comment.