diff --git a/packages/vkui/src/components/BaseGallery/BaseGallery.tsx b/packages/vkui/src/components/BaseGallery/BaseGallery.tsx index 6840dc42b6..aa220dee1a 100644 --- a/packages/vkui/src/components/BaseGallery/BaseGallery.tsx +++ b/packages/vkui/src/components/BaseGallery/BaseGallery.tsx @@ -8,7 +8,9 @@ import { useGlobalEventListener } from '../../hooks/useGlobalEventListener'; import { useDOM } from '../../lib/dom'; import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { RootComponent } from '../RootComponent/RootComponent'; -import { type CustomTouchEvent, Touch } from '../Touch/Touch'; +import { type CustomTouchEvent } from '../Touch/Touch'; +import { Bullets } from './Bullets'; +import { GalleryViewPort } from './GalleryViewPort'; import { ScrollArrows } from './ScrollArrows'; import { calcMax, calcMin } from './helpers'; import type { BaseGalleryProps, GallerySlidesState, LayoutState, ShiftingState } from './types'; @@ -34,10 +36,6 @@ const SHIFT_DEFAULT_STATE = { indent: 0, }; -const stylesBullets = { - dark: styles.bulletsDark, - light: styles.bulletsLight, -}; export const BaseGallery = ({ bullets = false, getRootRef, @@ -55,6 +53,10 @@ export const BaseGallery = ({ getRef, arrowSize, arrowAreaHeight, + slideTestId, + bulletTestId, + nextArrowTestId, + prevArrowTestId, ...restProps }: BaseGalleryProps): React.ReactNode => { const slidesStore = React.useRef>({}); @@ -331,33 +333,26 @@ export const BaseGallery = ({ )} getRootRef={rootRef} > - -
- {React.Children.map(children, (item: React.ReactNode, i: number) => ( -
setSlideRef(el, i)}> - {item} -
- ))} -
-
+ {children} + {bullets && ( -
- {React.Children.map(children, (_item: React.ReactNode, index: number) => ( -
- ))} -
+ )} ); diff --git a/packages/vkui/src/components/BaseGallery/Bullets.tsx b/packages/vkui/src/components/BaseGallery/Bullets.tsx new file mode 100644 index 0000000000..35919b2d47 --- /dev/null +++ b/packages/vkui/src/components/BaseGallery/Bullets.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { classNames } from '@vkontakte/vkjs'; +import { type BaseGalleryProps } from './types'; +import styles from './BaseGallery.module.css'; + +export interface BulletsTestIds { + /** + * Передает атрибут `data-testid` для bullets + */ + bulletTestId?: (index: number, active: boolean) => string; +} + +interface BulletsProps extends BulletsTestIds { + bullets: Exclude; + slideIndex: number; + count: number; +} + +const stylesBullets = { + dark: styles.bulletsDark, + light: styles.bulletsLight, +}; + +export const Bullets: React.FC = ({ bullets, slideIndex, count, bulletTestId }) => { + return ( +
+ {Array.from({ length: count }).map((_, index) => ( +
+ ))} +
+ ); +}; diff --git a/packages/vkui/src/components/BaseGallery/CarouselBase/CarouselBase.tsx b/packages/vkui/src/components/BaseGallery/CarouselBase/CarouselBase.tsx index 55434bc43e..2a33ce4b81 100644 --- a/packages/vkui/src/components/BaseGallery/CarouselBase/CarouselBase.tsx +++ b/packages/vkui/src/components/BaseGallery/CarouselBase/CarouselBase.tsx @@ -10,7 +10,9 @@ import { useDOM } from '../../../lib/dom'; import { useIsomorphicLayoutEffect } from '../../../lib/useIsomorphicLayoutEffect'; import { warnOnce } from '../../../lib/warnOnce'; import { RootComponent } from '../../RootComponent/RootComponent'; -import { type CustomTouchEvent, Touch } from '../../Touch/Touch'; +import { type CustomTouchEvent } from '../../Touch/Touch'; +import { Bullets } from '../Bullets'; +import { GalleryViewPort } from '../GalleryViewPort'; import { ScrollArrows } from '../ScrollArrows'; import { type BaseGalleryProps, type GallerySlidesState } from '../types'; import { @@ -24,11 +26,6 @@ import { useSlideAnimation } from './hooks'; import type { ControlElementsState, SlidesManagerState } from './types'; import styles from '../BaseGallery.module.css'; -const stylesBullets = { - dark: styles.bulletsDark, - light: styles.bulletsLight, -}; - const warn = warnOnce('Gallery'); export const CarouselBase = ({ @@ -48,6 +45,10 @@ export const CarouselBase = ({ getRef, arrowSize, arrowAreaHeight, + slideTestId, + bulletTestId, + nextArrowTestId, + prevArrowTestId, ...restProps }: BaseGalleryProps): React.ReactNode => { const slidesStore = React.useRef>({}); @@ -350,33 +351,26 @@ export const CarouselBase = ({ )} getRootRef={rootRef} > - -
- {React.Children.map(children, (item: React.ReactNode, i: number) => ( -
setSlideRef(el, i)}> - {item} -
- ))} -
-
+ {children} + {bullets && ( -
- {React.Children.map(children, (_item: React.ReactNode, index: number) => ( -
- ))} -
+ )} ); diff --git a/packages/vkui/src/components/BaseGallery/GalleryViewPort.tsx b/packages/vkui/src/components/BaseGallery/GalleryViewPort.tsx new file mode 100644 index 0000000000..82a3b8e8d7 --- /dev/null +++ b/packages/vkui/src/components/BaseGallery/GalleryViewPort.tsx @@ -0,0 +1,56 @@ +'use client'; + +import * as React from 'react'; +import { type HasChildren } from '../../types'; +import { type CustomTouchEvent, Touch } from '../Touch/Touch'; +import { type BaseGalleryProps } from './types'; +import styles from './BaseGallery.module.css'; + +type GalleryViewPortProps = Pick & + HasChildren & { + onStart: (e: CustomTouchEvent) => void; + onMoveX: (e: CustomTouchEvent) => void; + onEnd: (e: CustomTouchEvent) => void; + viewportRef: React.Ref; + setSlideRef: (slideRef: HTMLDivElement | null, slideIndex: number) => void; + layerRef?: React.Ref; + layerStyle?: React.CSSProperties; + }; + +export const GalleryViewPort: React.FC = ({ + slideTestId, + slideWidth, + onStart, + onMoveX, + onEnd, + viewportRef, + layerRef, + layerStyle, + children, + setSlideRef, +}) => { + return ( + +
+ {React.Children.map(children, (item: React.ReactNode, i: number) => ( +
setSlideRef(el, i)} + > + {item} +
+ ))} +
+
+ ); +}; diff --git a/packages/vkui/src/components/BaseGallery/ScrollArrows.tsx b/packages/vkui/src/components/BaseGallery/ScrollArrows.tsx index d5e9a8636c..3bb995ce71 100644 --- a/packages/vkui/src/components/BaseGallery/ScrollArrows.tsx +++ b/packages/vkui/src/components/BaseGallery/ScrollArrows.tsx @@ -22,8 +22,20 @@ export const getArrowClassName = ( ); }; +export interface ScrollArrowsTestIds { + /** + * Передает атрибут `data-testid` для кнопки перехода к следующему слайду + */ + nextArrowTestId?: string; + /** + * Передает атрибут `data-testid` для кнопки перехода к предыдущему слайду + */ + prevArrowTestId?: string; +} + interface ScrollArrowsProps - extends Pick { + extends Pick, + ScrollArrowsTestIds { hasPointer?: boolean; canSlideLeft: boolean; canSlideRight: boolean; @@ -40,6 +52,8 @@ export const ScrollArrows: React.FC = ({ showArrows = false, arrowSize = 'm', arrowAreaHeight = 'stretch', + nextArrowTestId, + prevArrowTestId, }) => { return showArrows && hasPointer ? ( <> @@ -49,6 +63,7 @@ export const ScrollArrows: React.FC = ({ direction="left" onClick={onSlideLeft} size={arrowSize} + data-testid={prevArrowTestId} /> )} {canSlideRight && ( @@ -57,6 +72,7 @@ export const ScrollArrows: React.FC = ({ direction="right" onClick={onSlideRight} size={arrowSize} + data-testid={nextArrowTestId} /> )} diff --git a/packages/vkui/src/components/BaseGallery/types.ts b/packages/vkui/src/components/BaseGallery/types.ts index c617e90a91..e6c9c7f27b 100644 --- a/packages/vkui/src/components/BaseGallery/types.ts +++ b/packages/vkui/src/components/BaseGallery/types.ts @@ -2,6 +2,8 @@ import type * as React from 'react'; import type { HasAlign, HasRef, HTMLAttributesWithRootRef } from '../../types'; import type { ScrollArrowProps } from '../ScrollArrow/ScrollArrow'; import type { CustomTouchEvent, CustomTouchEventHandler } from '../Touch/Touch'; +import { type BulletsTestIds } from './Bullets'; +import { type ScrollArrowsTestIds } from './ScrollArrows'; export interface GallerySlidesState { coordX: number; @@ -28,7 +30,9 @@ export interface LayoutState { export interface BaseGalleryProps extends Omit, 'onChange' | 'onDragStart' | 'onDragEnd'>, HasAlign, - HasRef { + HasRef, + BulletsTestIds, + ScrollArrowsTestIds { slideWidth?: string | number; slideIndex?: number; onDragStart?: CustomTouchEventHandler; @@ -62,4 +66,8 @@ export interface BaseGalleryProps * Текст для кнопки-стрелки вправо (вперед). Делает ее доступной для ассистивных технологий */ arrowNextLabel?: string; + /** + * Передает атрибут `data-testid` для слайда + */ + slideTestId?: (index: number) => string; } diff --git a/packages/vkui/src/components/Gallery/Gallery.test.tsx b/packages/vkui/src/components/Gallery/Gallery.test.tsx index 3131864f76..e5893b216f 100644 --- a/packages/vkui/src/components/Gallery/Gallery.test.tsx +++ b/packages/vkui/src/components/Gallery/Gallery.test.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { noop } from '@vkontakte/vkjs'; import { baselineComponent } from '../../testing/utils'; import type { AlignType } from '../../types'; import { ANIMATION_DURATION } from '../BaseGallery/CarouselBase/constants'; import { Gallery } from './Gallery'; -import styles from '../BaseGallery/BaseGallery.module.css'; const mockRAF = () => { let lastTime = 0; @@ -48,18 +47,14 @@ const Slide = ({ children: React.ReactNode; width?: number; getRef: React.Ref; - ['data-testid']: string; }) => ( -
+
{children}
); -const checkActiveSlide = (container: HTMLElement, slideIndex: number) => { - const bullets = Array.from(container.getElementsByClassName(styles.bullet)); - expect(bullets.indexOf(container.getElementsByClassName(styles.bulletActive)[0])).toBe( - slideIndex, - ); +const checkActiveSlide = (slideIndex: number) => { + expect(screen.queryByTestId(`bullet-${slideIndex}-active`)).toBeTruthy(); }; const checkTransformX = (value: string, expectedX: number) => { @@ -67,6 +62,12 @@ const checkTransformX = (value: string, expectedX: number) => { expect(match?.[1] && parseInt(match[1])).toEqual(expectedX); }; +const getArrows = (): HTMLElement[] => { + return [screen.queryByTestId('prev-arrow'), screen.queryByTestId('next-arrow')].filter( + Boolean, + ) as HTMLElement[]; +}; + const setup = ({ defaultSlideIndex, slideWidth, @@ -163,13 +164,13 @@ const setup = ({ slideWidth={isCustomSlideWidth ? 'custom' : undefined} getRootRef={mockContainerData} getRef={mockViewportData} + slideTestId={(index) => `slide-${index + 1}`} + bulletTestId={(index, active) => (active ? `bullet-${index}-active` : `bullet-${index}`)} + prevArrowTestId="prev-arrow" + nextArrowTestId="next-arrow" > {Array.from({ length: numberOfSlides }).map((_v, index) => ( - mockSlideData(e, index)} - > + mockSlideData(e, index)}> {index + 1} ))} @@ -290,21 +291,18 @@ describe('Gallery', () => { onNext, onChange, }); - const { - component: { container }, - rerender, - } = mockedData; + const { rerender } = mockedData; - checkActiveSlide(container, 1); + checkActiveSlide(1); - const [leftArrow, rightArrow] = Array.from(container.getElementsByClassName(styles.arrow)); + const [leftArrow, rightArrow] = getArrows(); fireEvent.click(rightArrow); expect(onNext).toHaveBeenCalledTimes(1); expect(onChange.mock.calls).toEqual([[2]]); rerender({ slideIndex: 2 }); - checkActiveSlide(container, 2); + checkActiveSlide(2); checkTransformX(mockedData.layerTransform, -400); fireEvent.click(leftArrow); @@ -313,7 +311,7 @@ describe('Gallery', () => { expect(onChange.mock.calls).toEqual([[2], [1]]); rerender({ slideIndex: 1 }); - checkActiveSlide(container, 1); + checkActiveSlide(1); checkTransformX(mockedData.layerTransform, -200); }); @@ -332,11 +330,8 @@ describe('Gallery', () => { onDragEnd, onChange, }); - const { - component: { container }, - } = mockedData; - checkActiveSlide(container, 0); + checkActiveSlide(0); simulateDrag(mockedData.viewPort, [2, 0]); @@ -402,12 +397,9 @@ describe('Gallery', () => { onDragEnd, onChange, }); - const { - component: { container }, - rerender, - } = mockedData; + const { rerender } = mockedData; - checkActiveSlide(container, 0); + checkActiveSlide(0); simulateDrag(mockedData.viewPort, [150, 0]); @@ -417,7 +409,7 @@ describe('Gallery', () => { rerender({ slideIndex: 1 }); checkTransformX(mockedData.layerTransform, -180); - checkActiveSlide(container, 1); + checkActiveSlide(1); simulateDrag(mockedData.viewPort, [0, 150]); @@ -427,7 +419,7 @@ describe('Gallery', () => { rerender({ slideIndex: 0 }); checkTransformX(mockedData.layerTransform, 0); - checkActiveSlide(container, 0); + checkActiveSlide(0); }); it('check correct navigation by dragging with align right', () => { @@ -447,11 +439,8 @@ describe('Gallery', () => { onDragEnd, onChange, }); - const { - component: { container }, - } = mockedData; - checkActiveSlide(container, 0); + checkActiveSlide(0); simulateDrag(mockedData.viewPort, [150, 0]); @@ -475,11 +464,8 @@ describe('Gallery', () => { onDragEnd, onChange, }); - const { - component: { container }, - } = mockedData; - checkActiveSlide(container, 0); + checkActiveSlide(0); simulateDrag(mockedData.viewPort, [10, 0]); @@ -503,12 +489,9 @@ describe('Gallery', () => { onDragEnd, onChange, }); - const { - component: { container }, - rerender, - } = mockedData; + const { rerender } = mockedData; - checkActiveSlide(container, 4); + checkActiveSlide(4); simulateDrag(mockedData.viewPort, [200, 0]); @@ -541,21 +524,18 @@ describe('Gallery', () => { onNext, onChange, }); - const { - component: { container }, - rerender, - } = mockedData; + const { rerender } = mockedData; - checkActiveSlide(container, 0); + checkActiveSlide(0); - const [leftArrow, rightArrow] = Array.from(container.getElementsByClassName(styles.arrow)); + const [leftArrow, rightArrow] = getArrows(); fireEvent.click(leftArrow); expect(onPrev).toHaveBeenCalledTimes(1); expect(onChange.mock.calls).toEqual([[4]]); rerender({ slideIndex: 4 }); - checkActiveSlide(container, 4); + checkActiveSlide(4); checkTransformX(mockedData.layerTransform, -800); fireEvent.click(rightArrow); @@ -564,7 +544,7 @@ describe('Gallery', () => { expect(onChange.mock.calls).toEqual([[4], [0]]); rerender({ slideIndex: 0 }); - checkActiveSlide(container, 0); + checkActiveSlide(0); checkTransformX(mockedData.layerTransform, 0); }); @@ -584,12 +564,9 @@ describe('Gallery', () => { onDragEnd, onChange, }); - const { - component: { container }, - rerender, - } = mockedData; + const { rerender } = mockedData; - checkActiveSlide(container, 0); + checkActiveSlide(0); simulateDrag(mockedData.viewPort, [0, 150]); @@ -600,7 +577,7 @@ describe('Gallery', () => { checkTransformX(mockedData.layerTransform, -710); checkTransformX(mockedData.getSlideMockData(0).transform, 900); - checkActiveSlide(container, 4); + checkActiveSlide(4); simulateDrag(mockedData.viewPort, [150, 0]); @@ -611,7 +588,7 @@ describe('Gallery', () => { checkTransformX(mockedData.layerTransform, 10); checkTransformX(mockedData.getSlideMockData(0).transform, 0); - checkActiveSlide(container, 0); + checkActiveSlide(0); }); it('check dev error when slides width incorrect', () => { @@ -657,13 +634,10 @@ describe('Gallery', () => { onDragEnd, onChange, }); - const { - component: { container }, - rerender, - } = mockedData; + const { rerender } = mockedData; - checkActiveSlide(container, 1); - expect(Array.from(container.getElementsByClassName(styles.arrow))).toHaveLength(1); + checkActiveSlide(1); + expect(getArrows()).toHaveLength(1); simulateDrag(mockedData.viewPort, [150, 0]); @@ -680,7 +654,7 @@ describe('Gallery', () => { rerender({ slideIndex: 2 }); - expect(Array.from(container.getElementsByClassName(styles.arrow))).toHaveLength(1); + expect(getArrows()).toHaveLength(1); simulateDrag(mockedData.viewPort, [150, 0]); @@ -695,7 +669,7 @@ describe('Gallery', () => { rerender({ slideIndex: 2 }); - expect(Array.from(container.getElementsByClassName(styles.arrow))).toHaveLength(0); + expect(getArrows()).toHaveLength(0); simulateDrag(mockedData.viewPort, [150, 0]);