Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Gallery): add rtl support #8085

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@
opacity: 0;
}

.arrowAreaStretch {
.arrow.arrowAreaStretch {
inset-block-start: 0;
}

.arrowAreaFit {
.arrow.arrowAreaFit {
inset-block-start: 50%;
transform: translateY(-50%);
block-size: auto;
Expand Down
126 changes: 99 additions & 27 deletions packages/vkui/src/components/CarouselBase/CarouselBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useAdaptivityHasPointer } from '../../hooks/useAdaptivityHasPointer';
import { useDirection } from '../../hooks/useDirection';
import { useExternRef } from '../../hooks/useExternRef';
import { useMutationObserver } from '../../hooks/useMutationObserver';
import { useResizeObserver } from '../../hooks/useResizeObserver';
Expand All @@ -20,7 +21,14 @@
SLIDE_THRESHOLD,
SLIDES_MANAGER_STATE,
} from './constants';
import { calcMax, calcMin, calculateIndent, getLoopPoints, getTargetIndex } from './helpers';
import {
calcMax,
calcMin,
calculateIndent,
getLoopPoints,
getTargetIndex,
revertRtlValue,
} from './helpers';
import { useSlideAnimation } from './hooks';
import {
type BaseGalleryProps,
Expand Down Expand Up @@ -59,8 +67,10 @@
}: BaseGalleryProps): React.ReactNode => {
const slidesStore = React.useRef<Record<string, HTMLDivElement | null>>({});
const slidesManager = React.useRef<SlidesManagerState>(SLIDES_MANAGER_STATE);
const [directionRef, textDirection = 'ltr'] = useDirection();
const isRtl = textDirection === 'rtl';

const rootRef = useExternRef(getRootRef);
const rootRef = useExternRef(getRootRef, directionRef);
const viewportRef = useExternRef(getRef);
const layerRef = React.useRef<HTMLDivElement>(null);
const animationFrameRef = React.useRef<ReturnType<typeof requestAnimationFrame> | null>(null);
Expand All @@ -87,9 +97,12 @@
const localMin = slidesManager.current.min ?? 0;
const indent = shiftXCurrentRef.current + shiftXDeltaRef.current;

if (indent > localMax) {
const moreThanMax = (isRtl && indent < localMax) || (!isRtl && indent > localMax);
const lessThanMin = (isRtl && indent > localMin) || (!isRtl && indent < localMin);

if (moreThanMax) {
return localMax + Number((indent - localMax) / 3);
} else if (indent < localMin) {
} else if (lessThanMin) {
return localMin + Number((indent - localMin) / 3);
}

Expand All @@ -100,7 +113,8 @@
if (looped) {
return !slidesManager.current.isFullyVisible;
}
return !slidesManager.current.isFullyVisible && shiftXCurrentRef.current < 0;
const isStartShiftX = isRtl ? shiftXCurrentRef.current <= 0 : shiftXCurrentRef.current >= 0;
return !slidesManager.current.isFullyVisible && !isStartShiftX;
};

const calculateCanSlideRight = () => {
Expand All @@ -111,7 +125,7 @@
!slidesManager.current.isFullyVisible &&
// we can't move right when gallery layer fully scrolled right, if gallery aligned by left side
((align === 'left' &&
slidesManager.current.containerWidth - shiftXCurrentRef.current <
slidesManager.current.containerWidth - revertRtlValue(shiftXCurrentRef.current, isRtl) <
(slidesManager.current.layerWidth ?? 0)) ||
// otherwise we need to check current slide index (align = right or align = center)
(align !== 'left' && slideIndex < slidesManager.current.slides.length - 1))
Expand Down Expand Up @@ -146,34 +160,73 @@
}
};

const checkShiftOutOfBoundsFromStart = (shiftX: number, snaps: number[]) =>
(isRtl && shiftX < snaps[0]) || (!isRtl && shiftX > snaps[0]);

const checkShiftOutOfBoundsFromEnd = (shiftX: number, slides: GallerySlidesState[]) => {
/**
* Поскольку при `align="center"` слайды сдвинуты, прежде чем рассчитать крайнюю правую точку,
* нужно вычесть сдвиг слайдов
*/
const firstSlideShift =
align === 'center'
? (slidesManager.current.containerWidth - slidesManager.current.slides[0].width) / 2
: 0;

const lastPoint =
slides[slides.length - 1].width + slides[slides.length - 1].coordX - firstSlideShift;

return (isRtl && shiftX >= lastPoint) || (!isRtl && shiftX <= -lastPoint);
};

const requestTransform = (shiftX: number, animation = false) => {
const { snaps, contentSize, slides } = slidesManager.current;

if (animationFrameRef.current !== null) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
if (looped && shiftX > snaps[0]) {
shiftXCurrentRef.current = -contentSize + snaps[0];
/**
* Для бесконечной галереи проверяем, что при dnd мы прокрутили левее, чем первый слайд,
* чтобы сбросить `shiftXCurrentRef`
*/
if (looped && checkShiftOutOfBoundsFromStart(shiftX, snaps)) {
const firstSnap = revertRtlValue(snaps[0], isRtl);
shiftXCurrentRef.current = revertRtlValue(-contentSize + firstSnap, isRtl);
shiftX = shiftXCurrentRef.current + shiftXDeltaRef.current;
}
const lastPoint = slides[slides.length - 1].width + slides[slides.length - 1].coordX;

if (looped && shiftX <= -lastPoint) {
/**
* Для бесконечной галереи проверяем, что при dnd мы прокрутили правее, чем последний слайд,
* чтобы правильно пересчитать `shiftXCurrentRef`
*/
if (looped && checkShiftOutOfBoundsFromEnd(shiftX, slides)) {
shiftXCurrentRef.current = Math.abs(shiftXDeltaRef.current) + snaps[0];
}
transformCssStyles(shiftX, animation);
});
};

const initializeSlides = () => {
if (!rootRef.current || !viewportRef.current) {
if (!rootRef.current || !viewportRef.current || !layerRef.current) {
return;
}
const layerOffsetWidth = layerRef.current.offsetWidth;

const calcRtlCoord = (element: HTMLDivElement) => {
const offsetLeft = element.offsetLeft;
const offsetWidth = element.offsetWidth;
return layerOffsetWidth - offsetLeft - offsetWidth;
};

let localSlides =
React.Children.map(children, (_item, i): GallerySlidesState => {
const elem = slidesStore.current[i] || { offsetLeft: 0, offsetWidth: 0 };
return { coordX: elem.offsetLeft, width: elem.offsetWidth };
const elem = slidesStore.current[i];
if (!elem) {
return { coordX: 0, width: 0 };

Check warning on line 226 in packages/vkui/src/components/CarouselBase/CarouselBase.tsx

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/components/CarouselBase/CarouselBase.tsx#L226

Added line #L226 was not covered by tests
}
const coordX = isRtl ? calcRtlCoord(elem) : elem.offsetLeft;
return { coordX, width: elem.offsetWidth };
}) || [];

if (localSlides.length === 0) {
Expand Down Expand Up @@ -223,6 +276,7 @@
slides: localSlides,
containerWidth,
isCenterAlign,
isRtl,
}),
min:
looped || onlyOneSlide
Expand All @@ -234,22 +288,33 @@
viewportOffsetWidth,
isFullyVisible,
align,
isRtl,
}),
};
const snaps = localSlides.map((_, index) =>
calculateIndent(index, slidesManager.current, isCenterAlign, looped),
calculateIndent({
targetIndex: index,
slidesManager: slidesManager.current,
isCenter: isCenterAlign,
looped,
isRtl,
}),
);

let contentSize = -snaps[snaps.length - 1] + localSlides[localSlides.length - 1].width;
let contentSize = Math.abs(snaps[snaps.length - 1]) + localSlides[localSlides.length - 1].width;
if (align === 'center') {
contentSize += snaps[0];
contentSize += revertRtlValue(snaps[0], isRtl);
}

slidesManager.current.snaps = snaps;
slidesManager.current.contentSize = contentSize;
// Если галерея не зациклена и слайд всего один, то рассчитывать loopPoints тоже не надо
if (looped && !onlyOneSlide && !isFullyVisible) {
slidesManager.current.loopPoints = getLoopPoints(slidesManager.current, containerWidth);
slidesManager.current.loopPoints = getLoopPoints(
slidesManager.current,
containerWidth,
isRtl,
);
}

shiftXCurrentRef.current = snaps[slideIndex];
Expand All @@ -276,15 +341,20 @@
const indent = snaps[slideIndex];
let startPoint = shiftXCurrentRef.current;

const fromLastToFirst = isRtl
? shiftXCurrentRef.current >= snaps[snaps.length - 1]
: shiftXCurrentRef.current <= snaps[snaps.length - 1];
/**
* Переключаемся с последнего элемента на первый
* Для корректной анимации мы прокручиваем последний слайд на всю длину (shiftX) "вперед"
* В конце анимации при отрисовке следующего кадра задаем всем слайдам начальные значения
*/
if (indent === snaps[0] && shiftXCurrentRef.current <= snaps[snaps.length - 1]) {
const distance =
Math.abs(snaps[snaps.length - 1]) + slides[slides.length - 1].width + startPoint;

if (indent === snaps[0] && fromLastToFirst) {
const endEdge = revertRtlValue(
Math.abs(snaps[snaps.length - 1]) + slides[slides.length - 1].width,
isRtl,
);
const distance = endEdge + startPoint;
addToAnimationQueue(
getAnimateFunction((progress) => {
const shiftX = startPoint + progress * distance * -1;
Expand All @@ -305,15 +375,16 @@
* В следующем кадре начинаем анимация прокрутки "назад"
*/
} else if (indent === snaps[snaps.length - 1] && shiftXCurrentRef.current === snaps[0]) {
startPoint = indent - slides[slides.length - 1].width;
startPoint = indent - revertRtlValue(slides[slides.length - 1].width, isRtl);

addToAnimationQueue(() => {
requestAnimationFrame(() => {
const shiftX = indent - slides[slides.length - 1].width;
const shiftX = indent - revertRtlValue(slides[slides.length - 1].width, isRtl);
transformCssStyles(shiftX);

getAnimateFunction((progress) => {
transformCssStyles(startPoint + progress * slides[slides.length - 1].width);
const diff = revertRtlValue(progress * slides[slides.length - 1].width, isRtl);
transformCssStyles(startPoint + diff);
})();
});
});
Expand Down Expand Up @@ -382,15 +453,15 @@

useMutationObserver(layerRef, initializeSlides);

useIsomorphicLayoutEffect(initializeSlides, [align, slideWidth, looped]);
useIsomorphicLayoutEffect(initializeSlides, [align, slideWidth, looped, isRtl]);

const calculateMinDeltaXToSlide = () => {
return slidesManager.current.slides[slideIndex].width * SLIDE_THRESHOLD;
};

const slideLeft = (event: React.MouseEvent) => {
if (slideIndex > 0) {
shiftXCurrentRef.current += calculateMinDeltaXToSlide();
shiftXCurrentRef.current += revertRtlValue(calculateMinDeltaXToSlide(), isRtl);
}
onChange?.(
(slideIndex - 1 + slidesManager.current.slides.length) % slidesManager.current.slides.length,
Expand All @@ -400,7 +471,7 @@

const slideRight = (event: React.MouseEvent) => {
if (slideIndex < slidesManager.current.slides.length - 1) {
shiftXCurrentRef.current -= calculateMinDeltaXToSlide();
shiftXCurrentRef.current -= revertRtlValue(calculateMinDeltaXToSlide(), isRtl);
}
onChange?.((slideIndex + 1) % slidesManager.current.slides.length);
onNextClick?.(event);
Expand Down Expand Up @@ -441,6 +512,7 @@
currentShiftXDelta: shiftXDeltaRef.current,
max: slidesManager.current.max,
looped,
isRtl,
});
}
onDragEnd?.(e, targetIndex);
Expand Down
Loading