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(HorizontalScroll, CardScroll): fix rtl view #8110

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const CardScrollPlayground = (props: ComponentPlaygroundProps) => {
{
padding: [false],
},
{
showArrows: ['always'],
$direction: 'rtl',
},
]}
>
{(props: CardScrollProps) => <CardScroll {...props}>{items}</CardScroll>}
Expand Down
118 changes: 94 additions & 24 deletions packages/vkui/src/components/CardScroll/CardScroll.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { baselineComponent } from '../../testing/utils';
import { DirectionProvider } from '../DirectionProvider/DirectionProvider';
import { CardScroll } from './CardScroll';
import styles from './CardScroll.module.css';
import horizontalScrollStyles from '../HorizontalScroll/HorizontalScroll.module.css';
Expand All @@ -24,18 +25,9 @@ const setupHorizontalScrollData = (element: HTMLElement, startScrollLeft = 0) =>
};
};

const mockCardScrollData = (container: HTMLElement, cardsCount: number, defaultScrollLeft = 50) => {
const mockCardScrollData = (cardsCount: number, defaultScrollLeft = 50, isRtl = false) => {
const startOffsetLeft = 12;
new Array(cardsCount).fill(0).forEach((_, index) => {
const width = 400;
const gap = 8;
const card = screen.getByTestId(`card-${index}`);

jest
.spyOn(card, 'offsetLeft', 'get')
.mockImplementation(() => startOffsetLeft + index * (width + gap));
jest.spyOn(card, 'offsetWidth', 'get').mockImplementation(() => width);
});
const containerWidth = 1009;

const originalGetComputedStyle = window.getComputedStyle;

Expand All @@ -44,7 +36,7 @@ const mockCardScrollData = (container: HTMLElement, cardsCount: number, defaultS
.mockImplementation((e) => {
return {
...originalGetComputedStyle(e),
marginRight: '8px',
marginInlineEnd: '8px',
getPropertyValue: (property: string) => {
if (property === '--vkui_internal--CardScroll_horizontal_padding') {
return '12px';
Expand All @@ -54,14 +46,46 @@ const mockCardScrollData = (container: HTMLElement, cardsCount: number, defaultS
};
});

const mockCard = (card: HTMLElement, index: number) => {
const width = 400;
const gap = 8;

const offsetLeft = startOffsetLeft + index * (width + gap);
const offset = isRtl ? containerWidth - offsetLeft - width : offsetLeft;

jest.spyOn(card, 'offsetLeft', 'get').mockImplementation(() => offset);
jest.spyOn(card, 'offsetWidth', 'get').mockImplementation(() => width);
};

const { container } = render(
<DirectionProvider value={isRtl ? 'rtl' : 'ltr'}>
<CardScroll size="s" prevButtonTestId="ScrollArrowLeft" nextButtonTestId="ScrollArrowRight">
{Array.from({ length: cardsCount }).map((_, index) => (
<div
key={index}
data-testid={`card-${index}`}
ref={(element) => {
if (element) {
mockCard(element, index);
}
}}
></div>
))}
</CardScroll>
</DirectionProvider>,
);

const cardScrollContainer = container.getElementsByClassName(styles.in)[0] as HTMLDivElement;
jest.spyOn(cardScrollContainer, 'offsetWidth', 'get').mockImplementation(() => 1009);
jest.spyOn(cardScrollContainer, 'offsetWidth', 'get').mockImplementation(() => containerWidth);

const horizontalScroll = container.getElementsByClassName(
horizontalScrollStyles.in,
)[0] as HTMLDivElement;
return {
horizontalScrollData: setupHorizontalScrollData(horizontalScroll, defaultScrollLeft),
horizontalScrollData: setupHorizontalScrollData(
horizontalScroll,
defaultScrollLeft * (isRtl ? -1 : 1),
),
horizontalScroll,
mocksRestore: () => {
[getComputedStyleInstance].forEach((mock) => mock.mockRestore());
Expand All @@ -72,21 +96,14 @@ const mockCardScrollData = (container: HTMLElement, cardsCount: number, defaultS
type PrepareDataParams = {
defaultScrollLeft?: number;
cardsCount?: number;
isRtl?: boolean;
};

const setup = ({ defaultScrollLeft = 50, cardsCount = 6 }: PrepareDataParams) => {
const { container } = render(
<CardScroll size="s" prevButtonTestId="ScrollArrowLeft" nextButtonTestId="ScrollArrowRight">
{new Array(cardsCount).fill(0).map((_, index) => (
<div key={index} data-testid={`card-${index}`}></div>
))}
</CardScroll>,
);

const setup = ({ defaultScrollLeft = 50, cardsCount = 6, isRtl = false }: PrepareDataParams) => {
const { horizontalScrollData, horizontalScroll, mocksRestore } = mockCardScrollData(
container,
cardsCount,
defaultScrollLeft,
isRtl,
);

fireEvent.mouseEnter(horizontalScroll);
Expand Down Expand Up @@ -163,4 +180,57 @@ describe('CardScroll', () => {
});
mocksRestore();
});

describe('check rtl working', () => {
it('check scroll by click arrow prev', async () => {
const { horizontalScrollData, mocksRestore } = setup({
defaultScrollLeft: 1470,
isRtl: true,
});

const arrowPrev = screen.getByTestId('ScrollArrowLeft');

fireEvent.click(arrowPrev);
await waitFor(() => {
expect(horizontalScrollData.scrollLeft).toBe(-639);
});

fireEvent.click(arrowPrev);
await waitFor(() => {
expect(horizontalScrollData.scrollLeft).toBe(0);
});

fireEvent.click(arrowPrev);
await waitFor(() => {
expect(horizontalScrollData.scrollLeft).toBe(0);
});

mocksRestore();
});

it('check scroll by click arrow next', async () => {
const { horizontalScrollData, mocksRestore } = setup({
isRtl: true,
});

const arrowNext = screen.getByTestId('ScrollArrowRight');

fireEvent.click(arrowNext);
await waitFor(() => {
expect(horizontalScrollData.scrollLeft).toBe(-816);
});

fireEvent.click(arrowNext);
await waitFor(() => {
expect(horizontalScrollData.scrollLeft).toBe(-1477);
});

fireEvent.click(arrowNext);
await waitFor(() => {
expect(horizontalScrollData.scrollLeft).toBe(-1477);
});

mocksRestore();
});
});
});
39 changes: 27 additions & 12 deletions packages/vkui/src/components/CardScroll/CardScroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useConfigDirection } from '../../hooks/useConfigDirection';
import { useDOM } from '../../lib/dom';
import type { HasComponent, HTMLAttributesWithRootRef } from '../../types';
import { HorizontalScroll, type HorizontalScrollProps } from '../HorizontalScroll/HorizontalScroll';
Expand Down Expand Up @@ -46,6 +47,7 @@
...restProps
}: CardScrollProps): React.ReactNode => {
const refContainer = React.useRef<HTMLDivElement>(null);
const direction = useConfigDirection();

const { window } = useDOM();

Expand All @@ -57,18 +59,33 @@
);
};

const slideOffsetFromStart = (slide: HTMLElement) => {
const containerWidth = refContainer.current?.offsetWidth || 0;
return direction === 'rtl'
? containerWidth - slide.offsetLeft - slide.offsetWidth
: slide.offsetLeft;
};

function getScrollToLeft(offset: number): number {
if (!refContainer.current) {
return offset;
}
const containerWidth = refContainer.current.offsetWidth;

const getMarginEnd = (el: HTMLElement) => {
const computedStyles = window!.getComputedStyle(el);
return (
parseInt(computedStyles.marginInlineEnd) ||
(direction === 'rtl'
? parseInt(computedStyles.marginLeft)
: parseInt(computedStyles.marginRight)) ||
0

Check warning on line 82 in packages/vkui/src/components/CardScroll/CardScroll.tsx

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/components/CardScroll/CardScroll.tsx#L79-L82

Added lines #L79 - L82 were not covered by tests
);
};

const slideIndex = ([...refContainer.current.children] as HTMLElement[]).findIndex(
(el: HTMLElement) =>
el.offsetLeft +
el.offsetWidth +
parseInt(window!.getComputedStyle(el).marginRight) -
offset >=
0,
slideOffsetFromStart(el) + el.offsetWidth + getMarginEnd(el) - offset >= 0,
);

if (slideIndex === -1) {
Expand All @@ -77,7 +94,7 @@

const slide = refContainer.current.children[slideIndex] as HTMLElement;
const padding = getPadding(refContainer.current);
const scrollTo = slide.offsetLeft - (containerWidth - slide.offsetWidth) + padding;
const scrollTo = slideOffsetFromStart(slide) - (containerWidth - slide.offsetWidth) + padding;

if (scrollTo <= 2 * padding) {
return 0;
Expand All @@ -90,19 +107,17 @@
if (!refContainer.current) {
return offset;
}

const containerWidth = refContainer.current.offsetWidth;
const slide = Array.prototype.find.call(
refContainer.current.children,
(el: HTMLElement) => el.offsetLeft + el.offsetWidth - offset > containerWidth,
) as HTMLElement;
const slide = Array.prototype.find.call(refContainer.current.children, (el: HTMLElement) => {
return slideOffsetFromStart(el) + el.offsetWidth - offset > containerWidth;
}) as HTMLElement;

if (!slide) {
return offset;
}

const padding = getPadding(refContainer.current);
return slide.offsetLeft - padding;
return slideOffsetFromStart(slide) - padding;
}

return (
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export const HorizontalScrollMobilePlayground = (props: ComponentPlaygroundProps
arrowSize: ['s', 'm'],
children: [items],
},
{
showArrows: ['always'],
$direction: 'rtl',
children: [items],
},
]}
>
{baseRender}
Expand All @@ -47,6 +52,11 @@ export const HorizontalScrollSmallTabletPlayground = (props: ComponentPlayground
arrowOffsetY: [-10],
children: [items],
},
{
showArrows: ['always'],
dir: ['rtl'],
children: [items],
},
]}
>
{baseRender}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
*/
overflow-x: hidden;
isolation: isolate;

--vkui_internal--HorizontalScroll_shift_direction: 1;
}

.rtl {
--vkui_internal--HorizontalScroll_shift_direction: -1;
}

.in {
Expand Down Expand Up @@ -48,11 +54,11 @@
}

.arrowLeft:hover ~ .in .inWrapper {
transform: translateX(8px);
transform: translateX(calc(8px * var(--vkui_internal--HorizontalScroll_shift_direction)));
}

.arrowRight:hover ~ .in .inWrapper {
transform: translateX(-8px);
transform: translateX(calc(-8px * var(--vkui_internal--HorizontalScroll_shift_direction)));
}

/**
Expand Down
Loading
Loading