diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 6648d7b5e..48ed73caa 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -15,6 +15,7 @@ import {Theme} from '../src'; import {GlobalThemeController} from './theme/utils/global-theme-controller'; import '../styles/styles.scss'; +import '@gravity-ui/uikit/styles/fonts.css'; const withContextProvider: Decorator = (Story, context) => { const theme = context.globals.theme; diff --git a/.storybook/stories/documentation/Blocks.mdx b/.storybook/stories/documentation/Blocks.mdx index b70ea7b28..c7a036cfa 100644 --- a/.storybook/stories/documentation/Blocks.mdx +++ b/.storybook/stories/documentation/Blocks.mdx @@ -49,6 +49,8 @@ _[Common field types](?id=documentation-types&viewMode=docs)_ ## [Share](?path=/story/blocks-share--docs&viewMode=docs) +## [SliderOld](?path=/story/blocks-sliderold-deprecated--docs&viewMode=docs) (Deprecated, use Slider instead) + ## [Slider](?path=/story/blocks-slider--docs&viewMode=docs) ## [Table](?path=/story/blocks-table--docs&viewMode=docs) diff --git a/README.md b/README.md index fe24306e6..f857f8b9e 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,12 @@ To use mixins and constructor style variables when creating custom blocks, add i @import '~@gravity-ui/page-constructor/styles/styles.scss'; ``` +To use default font, add import in your file: + +```css +@import '~@gravity-ui/page-constructor/styles/fonts.scss'; +``` + ### Loadable blocks It's sometimes necessary that a block renders itself based on data to be loaded. In this case, loadable blocks are used. diff --git a/playwright/playwright/index.scss b/playwright/playwright/index.scss index 7e9ada0b9..83b412e1a 100644 --- a/playwright/playwright/index.scss +++ b/playwright/playwright/index.scss @@ -1,2 +1,3 @@ @import '../../styles/styles.scss'; +@import '../../styles/fonts.scss'; @import '../../styles/storybook/index.scss'; diff --git a/src/blocks/HeaderSlider/HeaderSlider.scss b/src/blocks/HeaderSlider/HeaderSlider.scss index 2d7483b27..baf90eb0e 100644 --- a/src/blocks/HeaderSlider/HeaderSlider.scss +++ b/src/blocks/HeaderSlider/HeaderSlider.scss @@ -47,14 +47,6 @@ $block: '.#{$ns}header-slider-block'; } } - @media (max-width: map-get($gridBreakpoints, 'md')) { - &.#{$ns}SliderBlock { - margin-left: -$indentXXXS; - padding-left: 0; - width: calc(100% + #{$indentXXXS}); - } - } - @media (max-width: map-get($gridBreakpoints, 'sm')) { &__item-content { .#{$ns}header-block__content { @@ -66,11 +58,5 @@ $block: '.#{$ns}header-slider-block'; padding-left: $indentXS + $indentXXXS; } } - - .slick-track { - .slick-slide { - max-width: 100%; - } - } } } diff --git a/src/blocks/HeaderSlider/HeaderSlider.tsx b/src/blocks/HeaderSlider/HeaderSlider.tsx index af50ec41e..d4a921195 100644 --- a/src/blocks/HeaderSlider/HeaderSlider.tsx +++ b/src/blocks/HeaderSlider/HeaderSlider.tsx @@ -1,10 +1,10 @@ import React, {useContext} from 'react'; +import {SliderBlock} from '../../blocks'; import {MobileContext} from '../../context/mobileContext'; import {HeaderSliderBlockProps, SliderType} from '../../models'; import {block} from '../../utils'; import Header from '../Header/Header'; -import {SliderBlock} from '../index'; import './HeaderSlider.scss'; diff --git a/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-chromium-linux.png b/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-chromium-linux.png index 10b9c3067..96fc57516 100644 Binary files a/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-chromium-linux.png and b/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-chromium-linux.png differ diff --git a/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-webkit-linux.png b/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-webkit-linux.png index afc325b6c..51338c8b5 100644 Binary files a/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-webkit-linux.png and b/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-Default-light-webkit-linux.png differ diff --git a/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-ImageSlider-light-chromium-linux.png b/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-ImageSlider-light-chromium-linux.png index 32dfad30e..14c9e5ffc 100644 Binary files a/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-ImageSlider-light-chromium-linux.png and b/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-ImageSlider-light-chromium-linux.png differ diff --git a/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-ImageSlider-light-webkit-linux.png b/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-ImageSlider-light-webkit-linux.png index 6a8addb36..21029100f 100644 Binary files a/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-ImageSlider-light-webkit-linux.png and b/src/blocks/Media/__snapshots__/Media.visual.test.tsx-snapshots/Media-render-stories-ImageSlider-light-webkit-linux.png differ diff --git a/src/blocks/Media/__stories__/Media.stories.tsx b/src/blocks/Media/__stories__/Media.stories.tsx index d645f3165..db0cafdfb 100644 --- a/src/blocks/Media/__stories__/Media.stories.tsx +++ b/src/blocks/Media/__stories__/Media.stories.tsx @@ -51,6 +51,19 @@ const DefaultTemplate: StoryFn = (args) => ( }), links: data.common.links as LinkProps[], }, + { + ...args, + media: { + light: { + ...data.default.content.media.light, + fullscreen: true, + }, + dark: { + ...data.default.content.media.dark, + fullscreen: true, + }, + }, + }, ], }} /> diff --git a/src/blocks/Slider/Arrow/Arrow.scss b/src/blocks/Slider/Arrow/Arrow.scss index 617474719..25b362088 100644 --- a/src/blocks/Slider/Arrow/Arrow.scss +++ b/src/blocks/Slider/Arrow/Arrow.scss @@ -15,25 +15,17 @@ $block: '.#{$ns}slider-block-arrow'; $root: &; - width: $sliderArrowSize; - height: $sliderArrowSize; - cursor: pointer; - - &_type_left { - #{$root}__icon-wrapper { - transform: rotate(180deg); - } - margin-right: 16px; - } - &__button { @include reset-button-style(); + @include focusable(); + } + + &__inner { @include shadow(); @extend %flex; width: $sliderArrowSize; height: $sliderArrowSize; - color: var(--g-color-text-secondary); border-radius: 100%; background-color: var(--g-color-base-background); @@ -41,14 +33,25 @@ $block: '.#{$ns}slider-block-arrow'; transition: box-shadow 0.3s $ease-out-cubic, color 1s $ease-out-cubic; - @include focusable(); - } + &_type_left { + #{$root}__icon-wrapper { + transform: rotate(180deg); + } + } - &:hover { - #{$root}__button { + &:hover { color: var(--g-color-text-primary); box-shadow: 0 2px 12px var(--pc-color-sfx-shadow), 0 4px 24px var(--pc-color-sfx-shadow); } + + &_transparent { + background-color: transparent; + box-shadow: none; + + &:hover { + box-shadow: none; + } + } } &__icon-wrapper { diff --git a/src/blocks/Slider/Arrow/Arrow.tsx b/src/blocks/Slider/Arrow/Arrow.tsx index 5afc96f5e..3771ef585 100644 --- a/src/blocks/Slider/Arrow/Arrow.tsx +++ b/src/blocks/Slider/Arrow/Arrow.tsx @@ -13,25 +13,37 @@ export type ArrowType = 'left' | 'right'; export interface ArrowProps { type: ArrowType; - handleClick?: (direction: ArrowType) => void; + transparent?: boolean; + onClick?: () => void; size?: number; + extraProps?: React.ButtonHTMLAttributes; } -const Arrow = ({type, handleClick, className, size = 16}: ArrowProps & ClassNameProps) => ( -
+const Arrow = ({ + type, + transparent, + onClick, + className, + size = 16, + extraProps, +}: ArrowProps & ClassNameProps) => ( +
); diff --git a/src/blocks/Slider/Slider.scss b/src/blocks/Slider/Slider.scss index d433b6a85..dbc53deab 100644 --- a/src/blocks/Slider/Slider.scss +++ b/src/blocks/Slider/Slider.scss @@ -1,74 +1,81 @@ @import '../../../styles/mixins'; @import '../../../styles/variables'; -@import './slick.scss'; - -$slideOffset: 8px; $block: '.#{$ns}SliderBlock'; +$fullscreenArrowArea: 120px; @mixin fullscreen-card() { - .slick-slide { - width: 100%; - } - @media (max-width: map-get($gridBreakpoints, 'sm')) { - &:not(&_one-slide) { + &#{$block}:not(#{$block}_one-slide) { margin-left: 0; padding-left: 0; width: 100%; overflow: inherit; - - .slick-list { - margin: 0; - } - - .slick-slide { - &:last-child { - padding-right: 10px; - } - } } } } +$dotsCn: '.swiper-container-horizontal .swiper-pagination-bullets'; +$wrapperCn: '.swiper-wrapper'; + +$dotSize: 8px; + #{$block} { $root: &; + position: relative; + + &__slider { + @include add-specificity(&) { + padding: $indentSM 0 $indentL; + margin: 0 -#{$gridGutter}; + } + } - .slick-list { - padding: 24px 0 $indentS; - margin: 0 -#{$gridGutter}; + &_without-dots { + #{$root}__slider { + padding-bottom: $indentS; + } } - .slick-slide { + &__slide.swiper-slide { padding: 0 #{$gridGutter}; box-sizing: border-box; - flex-shrink: 0; - } + height: auto; - .slick-track { - display: flex; - min-width: 100%; + @keyframes safari-fix { + from { + transform: translateX(0.001px); + } + to { + transform: translateX(0); + } + } - .slick-slide { - height: auto; + // fix text under video in safari + &.swiper-slide-visible { + animation: safari-fix 300ms; } - .slick-slide > div { - display: flex; + #{$root}__slide-item { width: 100%; height: 100%; } } - .slick-arrow { + #{$root}__slide-item { + width: 100%; + height: 100%; + } + + &__arrow { position: absolute; - top: -#{$sliderArrowSize}; + top: -2px; right: 0; - z-index: 2; - &.slick-prev { + &_prev { right: $sliderArrowSize; + margin-right: $indentXS; } } @@ -79,35 +86,14 @@ $block: '.#{$ns}SliderBlock'; } } - &__dots { - display: flex; - justify-content: center; - width: 100%; - } - - &__dots-list { - @include reset-list-style(); - - position: relative; - display: inline-flex; - justify-content: center; - - li#{$root}__bar, - li#{$root}__accessible-bar, - li#{$root}__dot { - margin: calc(#{$indentXXS} / 2) $indentXXXS; - top: 0; - } - } - &__dot { - margin-top: $indentXXS; - width: 8px; - height: 8px; + width: $dotSize; + height: $dotSize; - border-radius: 100%; + border-radius: 50%; background-color: var(--g-color-line-generic-accent); cursor: pointer; + display: inline-block; transition: background-color 1s; @@ -116,36 +102,15 @@ $block: '.#{$ns}SliderBlock'; } & + & { - margin-left: 16px; + margin-left: $indentXS; } &_active { + opacity: 1; background-color: var(--g-color-line-generic-active); } } - &__bar, - &__accessible-bar { - position: absolute; - top: $indentXXS; - left: 0; - width: 24px; - height: 8px; - border-radius: $borderRadius; - } - - &__bar { - transition: left 0.3s; - background-color: var(--pc-color-line-generic-active-solid); - } - - &_align-left { - .slick-track { - /* stylelint-disable-next-line declaration-no-important */ - width: inherit !important; - } - } - &_only-arrows { padding-top: $sliderArrowSize; } @@ -177,7 +142,8 @@ $block: '.#{$ns}SliderBlock'; } &__animate-slides { - @include animate-slides('.slick-slide'); + @include animate-slides(#{$root}__slide); + @include animate-slides(#{$root}__dot); } &_type_media-card { @@ -185,44 +151,43 @@ $block: '.#{$ns}SliderBlock'; padding: 0; - #{$root}__dots { - position: absolute; - bottom: 24px; - left: 0; - width: 100%; + & #{$dotsCn} { + bottom: $indentSM; + } + + #{$root}__slider { + padding: 0; } &:hover { - .slick-arrow { + #{$root}__arrow { display: flex; } } - .slick-arrow { + #{$root}__arrow { display: none; width: 64px; top: 50%; transform: translate(0, -50%); - } - - .slick-prev { - left: 0; - } - - .slick-next { - right: 0; - } - .slick-list { - padding: 0; + &_prev { + left: 0; + margin-right: 0; + } } @media (max-width: map-get($gridBreakpoints, 'md')) { &:hover { - .slick-arrow { + #{$root}__arrow { display: none; } } + + & #{$dotsCn} { + left: $indentSM; + width: calc(100% - $indentSM); + } } } @@ -231,49 +196,37 @@ $block: '.#{$ns}SliderBlock'; $arrowWidth: 68px; $arrowHeight: 68px; - $arrowIndent: 16px; - padding-top: 0; #{$root}__wrapper { position: relative; } - #{$root}__dots { - position: absolute; - bottom: 16px; - left: 50%; - transform: translateX(-50%); - z-index: 100; + & #{$dotsCn} { + bottom: $indentM; + } + + #{$root}__slider { + padding: 0; + margin: 0; } - .slick-arrow { + #{$root}__arrow { top: 50%; transform: translateY(-50%); - right: $arrowIndent; - - &.slick-prev { - left: $arrowIndent; - } - - button { - background-color: transparent; - box-shadow: none; + width: $arrowWidth; + height: $arrowHeight; - &:hover { - box-shadow: none; - } - } + right: 0; - &:hover { - & button { - box-shadow: none; - } + &_prev { + left: 0; + margin-right: 0; } } - &:has(.slick-active .#{$ns}header-block_controls-view_light) { - .#{$ns}slider-block-arrow__button { + &:has(.swiper-slide-active .#{$ns}header-block_controls-view_light) { + .#{$ns}slider-block-arrow__inner { color: var(--g-color-text-dark-primary); } @@ -286,8 +239,8 @@ $block: '.#{$ns}SliderBlock'; } } - &:has(.slick-active .#{$ns}header-block_controls-view_dark) { - .#{$ns}slider-block-arrow__button { + &:has(.swiper-slide-active .#{$ns}header-block_controls-view_dark) { + .#{$ns}slider-block-arrow__inner { color: var(--g-color-text-light-primary); } @@ -300,63 +253,108 @@ $block: '.#{$ns}SliderBlock'; } } - .slick-slide { + #{$root}__slide { padding: 0; + } + + @media (max-width: map-get($gridBreakpoints, 'md')) { + &#{$root}:not(#{$root}_one-slide) { + margin-left: -$indentXXXS; + padding-left: 0; + width: calc(100% + #{$indentXXXS}); + } + } - @keyframes safari-fix { - from { - transform: translateX(0.001px); + @media (max-width: map-get($gridBreakpoints, 'sm')) { + #{$root}__arrow { + display: none; + } + &#{$root}:not(#{$root}_one-slide) { + #{$root}__slider { + margin-left: 0; } - to { - transform: translateX(0); + + #{$wrapperCn} { + padding-left: 0; } - } - // fix text under video in safari - &[aria-hidden='true'] { - animation: safari-fix 1000ms; + #{$root}__slide { + // to remove the indentation between slides + padding-right: 0; + padding-left: 0; + } } } + } - .slick-list { - padding: 0; + &_type_fullscreen-card { + @include fullscreen-card(); + padding-top: 0; + + #{$root}__slider { + padding: 24px 0 40px; + height: 100vh; margin: 0; + + & .swiper-pagination { + bottom: 11px; + } + + #{$root}__dot { + background-color: var(--g-color-text-light-hint); + + &_active { + background-color: var(--g-color-text-light-primary); + } + } + + #{$root}__slide { + height: 100%; + padding: 0 $fullscreenArrowArea; + } } - .slick-arrow { - width: $arrowWidth; - height: $arrowHeight; + &:hover { + #{$root}__arrow { + display: flex; + } + } - right: 0; - &.slick-prev { + #{$root}__arrow { + display: none; + width: $fullscreenArrowArea; + top: 40px; + bottom: 40px; + + &_prev { left: 0; + margin-right: 0; } } - @media (max-width: map-get($gridBreakpoints, 'sm')) { - .slick-arrow { - display: none; + @media (max-width: map-get($gridBreakpoints, 'md')) { + margin-left: 0; + + #{$root}_slider { + margin-left: 0; + width: 100%; } - &#{$root}:not(&_one-slide) { - .slick-list { - margin-left: 0; + &:hover { + #{$root}__arrow { + display: none; } + } + } - .slick-track { - padding-left: 0; + @media (max-width: map-get($gridBreakpoints, 'sm')) { + &#{$root}:not(#{$root}_one-slide) { + #{$root}__slider { + margin-left: 0; } - .slick-slide { - // to remove the indentation between slides - /* stylelint-disable declaration-no-important */ - padding-right: 0 !important; - padding-left: 0 !important; - /* stylelint-enable declaration-no-important */ - - &:last-child { - padding-right: 0; - } + #{$wrapperCn} { + padding-left: 0; } } } @@ -375,23 +373,25 @@ $block: '.#{$ns}SliderBlock'; } @media (max-width: map-get($gridBreakpoints, 'sm')) { - &:not(&_one-slide) { - margin-left: -($gridContainerMargin + $gridGutterMobile); - padding-left: $gridContainerMargin + $gridGutterMobile; - width: calc(100% + #{($gridContainerMargin + $gridGutterMobile) * 2}); + $horizontalPadding: $gridContainerMargin + $gridGutterMobile; + + &:not(#{&}_one-slide) { + margin-left: -($horizontalPadding); + padding-left: $horizontalPadding; + width: calc(100% + #{($horizontalPadding) * 2}); overflow-x: auto; - .slick-list { - margin-left: -($gridContainerMargin + $gridGutterMobile); + #{$root}__slider { + margin-left: #{-$horizontalPadding}; margin-right: 0; } - .slick-track { - padding-left: $gridContainerMargin + $gridGutterMobile - $slideOffset; + & #{$wrapperCn} { + padding-left: $horizontalPadding - $gridGutter; } - .slick-slide { - padding: 0 $slideOffset; + #{$root}__slide { + padding: 0 $fullscreenImageMobilePadding; } } } diff --git a/src/blocks/Slider/Slider.tsx b/src/blocks/Slider/Slider.tsx index c5473ca1e..3b4156905 100644 --- a/src/blocks/Slider/Slider.tsx +++ b/src/blocks/Slider/Slider.tsx @@ -1,513 +1,190 @@ -import React, { - Fragment, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, {Fragment, PropsWithChildren} from 'react'; -import {useUniqId} from '@gravity-ui/uikit'; -import debounce from 'lodash/debounce'; -import get from 'lodash/get'; -import noop from 'lodash/noop'; -import SlickSlider, {Settings} from 'react-slick'; +import SwiperCore, {A11y, Autoplay, Pagination} from 'swiper'; +import {Swiper, SwiperSlide} from 'swiper/react'; import Anchor from '../../components/Anchor/Anchor'; import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; -import OutsideClick from '../../components/OutsideClick/OutsideClick'; import Title from '../../components/Title/Title'; -import {BREAKPOINTS} from '../../constants'; -import {MobileContext} from '../../context/mobileContext'; -import {SSRContext} from '../../context/ssrContext'; -import {StylesContext} from '../../context/stylesContext/StylesContext'; -import useFocus from '../../hooks/useFocus'; -import { - ClassNameProps, - Refable, - SliderProps as SliderParams, - SliderType, - Timeout, -} from '../../models'; +import {ClassNameProps, Refable, SliderProps as SliderParams, SliderType} from '../../models'; import {block} from '../../utils'; -import Arrow, {ArrowType} from './Arrow/Arrow'; +import Arrow from './Arrow/Arrow'; import {i18n} from './i18n'; -import {SliderBreakpointParams} from './models'; -import { - getSliderResponsiveParams, - getSlidesCountByBreakpoint, - getSlidesToShowCount, - getSlidesToShowWithDefaults, - isFocusable, - useRovingTabIndex, -} from './utils'; +import {useSlider} from './useSlider'; +import {useSliderPagination} from './useSliderPagination'; import './Slider.scss'; +import 'swiper/swiper-bundle.css'; const b = block('SliderBlock'); -const slick = block('slick-origin'); - -const DOT_WIDTH = 8; -const DOT_GAP = 16; export interface SliderProps extends Omit, + Partial< + Pick< + Swiper, + | 'onSlideChange' + | 'onSlideChangeTransitionStart' + | 'onSlideChangeTransitionEnd' + | 'onActiveIndexChange' + | 'onBreakpoint' + > + >, Refable, - ClassNameProps, - Pick { + ClassNameProps { type?: string; anchorId?: string; - onAfterChange?: (index: number) => void; - onBeforeChange?: (current: number, next: number) => void; dotsClassName?: string; blockClassName?: string; arrowSize?: number; + initialSlide?: number; } -export const SliderBlock = (props: React.PropsWithChildren) => { +SwiperCore.use([Autoplay, A11y, Pagination]); + +export const SliderBlock = ({ + animated, + title, + description, + type, + anchorId, + arrows = true, + adaptive, + autoplay: autoplayMs, + dots = true, + initialSlide = 0, + className, + dotsClassName, + disclaimer, + children, + blockClassName, + arrowSize, + slidesToShow, + onSlideChange, + onSlideChangeTransitionStart, + onSlideChangeTransitionEnd, + onActiveIndexChange, + onBreakpoint, +}: PropsWithChildren) => { const { - animated, - title, - description, - type, - anchorId, - arrows = true, - adaptive, - autoplay: autoplaySpeed, - dots = true, - dotsClassName, - disclaimer, + autoplay, + isLocked, + childrenCount, + breakpoints, + onSwiper, + onImagesReady, + onPrev, + onNext, + setIsLocked, + } = useSlider({ + slidesToShow, children, - className, - blockClassName, - lazyLoad, - arrowSize, - onAfterChange: handleAfterChange, - onBeforeChange: handleBeforeChange, - } = props; - - const {isServer} = useContext(SSRContext); - const isMobile = useContext(MobileContext); - const [breakpoint, setBreakpoint] = useState(BREAKPOINTS.xl); - const sliderId = useUniqId(); - const disclosedChildren = useMemo( - () => discloseAllNestedChildren(children as React.ReactElement[], sliderId), - [children, sliderId], - ); - const childrenCount = disclosedChildren.length; - const isAutoplayEnabled = autoplaySpeed !== undefined && autoplaySpeed > 0; - const isUserInteractionRef = useRef(false); - - const [slidesToShow] = useState( - getSlidesToShowWithDefaults({ - contentLength: childrenCount, - breakpoints: props.slidesToShow, - mobileFullscreen: Boolean( - props.type && Object.values(SliderType).includes(props.type as SliderType), - ), - }), - ); - - const slidesToShowCount = getSlidesToShowCount(slidesToShow); - const slidesCountByBreakpoint = getSlidesCountByBreakpoint(breakpoint, slidesToShow); - - const [currentIndex, setCurrentIndex] = useState(0); - const [childStyles, setChildStyles] = useState({}); - const [slider, setSlider] = useState(); - const prevIndexRef = useRef(0); - const autoplayTimeId = useRef(); - const {hasFocus, unsetFocus} = useFocus(slider?.innerSlider?.list); - - const asUserInteraction = - (fn: (...args: T) => R) => - (...args: T): R => { - isUserInteractionRef.current = true; - return fn(...args); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - const onResize = useCallback( - debounce(() => { - if (!slider) { - return; - } - - const newBreakpoint = get(slider, 'state.breakpoint') || BREAKPOINTS.xl; - - if (newBreakpoint !== breakpoint) { - setBreakpoint(newBreakpoint); - setCurrentIndex(0); - - slider.slickGoTo(0); - } - }, 100), - [slider, breakpoint], - ); - - const scrollLastSlide = useCallback( - (current: number) => { - const lastSlide = childrenCount - slidesToShowCount; - - if (isAutoplayEnabled && lastSlide === current) { - // Slick doesn't support autoplay with no infinity scroll - autoplayTimeId.current = setTimeout(() => { - if (slider) { - slider.slickGoTo(0, false); - slider.slickPause(); - } - setTimeout(() => { - if (slider) { - slider.slickPlay(); - } - }, 500); - }, autoplaySpeed); - } - }, - [autoplaySpeed, childrenCount, isAutoplayEnabled, slider, slidesToShowCount], - ); - - useEffect(() => { - if (hasFocus && autoplayTimeId.current) { - clearTimeout(autoplayTimeId.current); - } else { - scrollLastSlide(currentIndex); - } - }, [currentIndex, hasFocus, scrollLastSlide]); - - useEffect(() => { - onResize(); - - window.addEventListener('resize', onResize, {passive: true}); - - return () => window.removeEventListener('resize', onResize); - }, [onResize]); - - const handleArrowClick = (direction: ArrowType) => { - let nextIndex; - - if (direction === 'right') { - nextIndex = - currentIndex === childrenCount - slidesCountByBreakpoint ? 0 : currentIndex + 1; - } else { - nextIndex = - currentIndex === 0 ? childrenCount - slidesCountByBreakpoint : currentIndex - 1; - } - - if (slider) { - slider.slickGoTo(nextIndex); - } - }; - - const onBeforeChange = useCallback( - (current: number, next: number) => { - if (handleBeforeChange) { - handleBeforeChange(current, next); - } - - prevIndexRef.current = current; - - setCurrentIndex(Math.ceil(next)); - }, - [handleBeforeChange], - ); - - const onAfterChange = useCallback( - (current: number) => { - if (handleAfterChange) { - handleAfterChange(current); - } - - if (autoplayTimeId.current) { - clearTimeout(autoplayTimeId.current); - } - - if (!hasFocus) { - scrollLastSlide(current); - } - - if (isUserInteractionRef.current) { - const focusIndex = - prevIndexRef.current >= current - ? current - : Math.max(current, prevIndexRef.current + slidesCountByBreakpoint); - - const firstNewSlide = document.getElementById(getSlideId(sliderId, focusIndex)); - if (firstNewSlide) { - const focusableChild = Array.from(firstNewSlide.querySelectorAll('*')).find( - isFocusable, - ) as HTMLElement | undefined; - focusableChild?.focus(); - } - } - - isUserInteractionRef.current = false; - }, - [handleAfterChange, hasFocus, scrollLastSlide, sliderId, slidesCountByBreakpoint], - ); - - const handleDotClick = (index: number) => { - const nextIndex = index > currentIndex ? index + 1 - slidesCountByBreakpoint : index; - - if (slider) { - slider.slickGoTo(nextIndex); - } - }; - - const barSlidesCount = childrenCount - slidesCountByBreakpoint + 1; - const barPosition = (DOT_GAP + DOT_WIDTH) * currentIndex; - const barWidth = DOT_WIDTH + (DOT_GAP + DOT_WIDTH) * (slidesCountByBreakpoint - 1); - - const {getRovingItemProps, rovingListProps} = useRovingTabIndex({ - itemCount: barSlidesCount, - activeIndex: currentIndex + 1, - firstIndex: 1, - uniqId: sliderId, + type, + autoplayMs, }); - const renderBar = () => { - return ( - slidesCountByBreakpoint > 1 && ( -
  • - ) - ); - }; + const isA11yControlHidden = Boolean(autoplay); + const controlTabIndex = isA11yControlHidden ? -1 : 0; - // renders additional bar, not visible in the layout but visible for screenreaders - const renderAccessibleBar = (index: number) => { - return ( - // To have this key differ from keys used in renderDot function, added `-accessible-bar` part - - {slidesCountByBreakpoint > 0 && ( -
  • - )} - - ); - }; - - const getCurrentSlideNumber = (index: number) => { - const currentIndexDiff = index - currentIndex; - - let currentSlideNumber; - if (0 <= currentIndexDiff && currentIndexDiff < slidesCountByBreakpoint) { - currentSlideNumber = currentIndex + 1; - } else if (currentIndexDiff >= slidesCountByBreakpoint) { - currentSlideNumber = index - slidesCountByBreakpoint + 2; - } else { - currentSlideNumber = index + 1; - } - return currentSlideNumber; - }; - const isVisibleSlide = (index: number) => { - const currentIndexDiff = index - currentIndex; - - const result = - slidesCountByBreakpoint > 0 && - 0 <= currentIndexDiff && - currentIndexDiff < slidesCountByBreakpoint; - return result; - }; + const paginationProps = useSliderPagination({ + enabled: dots, + isA11yControlHidden, + controlTabIndex, + bulletClass: b('dot', dotsClassName), + bulletActiveClass: b('dot_active'), + paginationLabel: i18n('pagination-label'), + }); - const renderDot = (index: number) => { - const isVisible = isVisibleSlide(index); - const currentSlideNumber = getCurrentSlideNumber(index); - const rovingItemProps = isVisible ? undefined : getRovingItemProps(currentSlideNumber); - return ( -
  • handleDotClick(index))} - onKeyDown={(e) => { - const key = e.key.toLowerCase(); - if (key === 'space' || key === 'enter') { - e.currentTarget.click(); - } - }} - role="menuitemradio" - aria-checked={false} - tabIndex={-1} - aria-hidden={isVisible} - aria-label={i18n('dot-label', { - index: currentSlideNumber, - count: barSlidesCount, - })} - {...rovingItemProps} + return ( +
    + {anchorId && } + - ); - }; - - const renderNavigation = () => { - if (childrenCount <= slidesCountByBreakpoint || !dots || childrenCount === 1) { - return null; - } - const dotsList = Array(childrenCount) - .fill(null) - .map((_item, index) => renderDot(index)); - dotsList.splice(currentIndex, 0, renderAccessibleBar(currentIndex)); - - return ( - <div className={b('dots', dotsClassName)}> - <ul - className={b('dots-list')} - role="menu" - aria-label={i18n('pagination-label')} - {...rovingListProps} + <AnimateBlock className={b('animate-slides')} animate={animated}> + <Swiper + className={b('slider', className)} + onSwiper={onSwiper} + speed={1000} + autoplay={autoplay} + autoHeight={adaptive} + initialSlide={initialSlide} + noSwiping={false} + breakpoints={breakpoints} + onSlideChange={onSlideChange} + onSlideChangeTransitionStart={onSlideChangeTransitionStart} + onSlideChangeTransitionEnd={onSlideChangeTransitionEnd} + onActiveIndexChange={onActiveIndexChange} + onBreakpoint={onBreakpoint} + onLock={() => setIsLocked(true)} + onUnlock={() => setIsLocked(false)} + onImagesReady={onImagesReady} + watchSlidesVisibility + watchOverflow + a11y={{ + slideLabelMessage: '', + paginationBulletMessage: i18n('dot-label', {index: '{{index}}'}), + }} + {...paginationProps} > - {renderBar()} - {dotsList} - </ul> - </div> - ); - }; - - const renderDisclaimer = () => { - return disclaimer ? ( - <div className={b('disclaimer', {size: disclaimer.size || 'm'})}>{disclaimer.text}</div> - ) : null; - }; - - const renderSlider = () => { - /* Disable adding of width in inline styles when SSR to prevent overriding of default styles */ - /* Calculate appropriate breakpoint for mobile devices with user agent */ - const variableWidth = isServer && isMobile; - - const settings = { - ref: (slickSlider: SlickSlider) => setSlider(slickSlider), - className: slick(null, className), - arrows, - variableWidth, - infinite: false, - speed: 1000, - adaptiveHeight: adaptive, - autoplay: isAutoplayEnabled, - autoplaySpeed, - slidesToShow: slidesToShowCount, - slidesToScroll: 1, - responsive: getSliderResponsiveParams(slidesToShow), - beforeChange: onBeforeChange, - afterChange: onAfterChange, - initialSlide: 0, - nextArrow: ( - <Arrow - type="right" - handleClick={asUserInteraction(handleArrowClick)} - size={arrowSize} - /> - ), - prevArrow: ( - <Arrow - type="left" - handleClick={asUserInteraction(handleArrowClick)} - size={arrowSize} - /> - ), - lazyLoad, - accessibility: false, - }; - - return ( - <OutsideClick onOutsideClick={isMobile ? unsetFocus : noop}> - <SlickSlider {...settings}>{disclosedChildren}</SlickSlider> + {React.Children.map(children, (elem, index) => ( + <SwiperSlide className={b('slide')} key={index}> + {({isVisible}) => ( + <div + className={b('slide-item')} + aria-hidden={!isA11yControlHidden && !isVisible} + > + {elem} + </div> + )} + </SwiperSlide> + ))} + </Swiper> + {arrows && !isLocked && ( + <Fragment> + <div aria-hidden={isA11yControlHidden}> + <Arrow + className={b('arrow', {prev: true})} + type="left" + transparent={type === SliderType.HeaderCard} + onClick={onPrev} + size={arrowSize} + extraProps={{tabIndex: controlTabIndex}} + /> + <Arrow + className={b('arrow', {next: true})} + type="right" + transparent={type === SliderType.HeaderCard} + onClick={onNext} + size={arrowSize} + extraProps={{tabIndex: controlTabIndex}} + /> + </div> + </Fragment> + )} <div className={b('footer')}> - {renderDisclaimer()} - {renderNavigation()} + {disclaimer ? ( + <div className={b('disclaimer', {size: disclaimer?.size || 'm'})}> + {disclaimer?.text} + </div> + ) : null} </div> - </OutsideClick> - ); - }; - - return ( - <StylesContext.Provider value={{...childStyles, setStyles: setChildStyles}}> - <div - className={b( - { - 'align-left': childrenCount < slidesCountByBreakpoint, - 'one-slide': childrenCount === 1, - 'only-arrows': !title?.text && !description && arrows, - mobile: isMobile, - type, - }, - blockClassName, - )} - > - {anchorId && <Anchor id={anchorId} />} - <Title - title={title} - subtitle={description} - className={b('header', {'no-description': !description})} - /> - <AnimateBlock className={b('animate-slides')} animate={animated}> - {renderSlider()} - </AnimateBlock> - </div> - </StylesContext.Provider> + </AnimateBlock> + </div> ); }; -function getSlideId(sliderId: string, index: number) { - return `slider-${sliderId}-child-${index}`; -} - -// TODO remove this and rework PriceDetailed CLOUDFRONT-12230 -function discloseAllNestedChildren( - children: React.ReactElement[], - sliderId: string, -): React.ReactElement[] { - if (!children) { - return []; - } - - let childIndex = 0; - const wrapped = (child: React.ReactElement) => { - const id = getSlideId(sliderId, childIndex++); - - return ( - <div key={id} id={id}> - {child} - </div> - ); - }; - - return React.Children.map(children, (child) => { - if (child) { - // TODO: if child has 'items' then 'items' determinate like nested children for Slider. - const nestedChildren = child.props.data?.items; - - if (nestedChildren) { - return nestedChildren.map((nestedChild: React.ReactElement) => { - return wrapped( - React.cloneElement(child, { - data: { - ...child.props.data, - items: [nestedChild], - }, - }), - ); - }); - } - } - return child && wrapped(child); - }).filter(Boolean); -} - export default SliderBlock; diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Banners-light-chromium-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Banners-light-chromium-linux.png index 12ed2a538..b5143c5d3 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Banners-light-chromium-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Banners-light-chromium-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Banners-light-webkit-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Banners-light-webkit-linux.png index c453fd40c..8273cc452 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Banners-light-webkit-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Banners-light-webkit-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Default-light-chromium-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Default-light-chromium-linux.png index 182eaf119..24dbc7d88 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Default-light-chromium-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Default-light-chromium-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Default-light-webkit-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Default-light-webkit-linux.png index 4700ed062..da803d2f2 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Default-light-webkit-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-Default-light-webkit-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-QuoteCards-light-chromium-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-QuoteCards-light-chromium-linux.png index 1d2b70628..b60e62625 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-QuoteCards-light-chromium-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-QuoteCards-light-chromium-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-QuoteCards-light-webkit-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-QuoteCards-light-webkit-linux.png index 3df57cbf4..f3042fbd6 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-QuoteCards-light-webkit-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-QuoteCards-light-webkit-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-SlidesToShow-light-chromium-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-SlidesToShow-light-chromium-linux.png index 225c568c6..ce6b27341 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-SlidesToShow-light-chromium-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-SlidesToShow-light-chromium-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-SlidesToShow-light-webkit-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-SlidesToShow-light-webkit-linux.png index c74c6c479..e90d35dd8 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-SlidesToShow-light-webkit-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-SlidesToShow-light-webkit-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutArrows-light-chromium-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutArrows-light-chromium-linux.png index 8f22599bc..cb9d2a775 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutArrows-light-chromium-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutArrows-light-chromium-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutArrows-light-webkit-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutArrows-light-webkit-linux.png index baadf4ec1..8ae6d00f4 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutArrows-light-webkit-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutArrows-light-webkit-linux.png differ diff --git a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutDots-light-chromium-linux.png b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutDots-light-chromium-linux.png index bea1e6825..e4712b314 100644 Binary files a/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutDots-light-chromium-linux.png and b/src/blocks/Slider/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-render-stories-WithoutDots-light-chromium-linux.png differ diff --git a/src/blocks/Slider/__stories__/Slider.mdx b/src/blocks/Slider/__stories__/Slider.mdx index 72f600724..4669dd2af 100644 --- a/src/blocks/Slider/__stories__/Slider.mdx +++ b/src/blocks/Slider/__stories__/Slider.mdx @@ -5,6 +5,10 @@ import * as SliderStories from './Slider.stories.tsx'; <Meta of={SliderStories} /> <StoryTemplate> + +##### Slider block. based on [swiper](https://v6.swiperjs.com/react) library. +<br/><br/> + ## Parameters The slider supports two types of content: loadable and from a config. Loadable content is loaded via the `loadable` property and content from a config via the `children` property. @@ -17,13 +21,13 @@ The slider supports two types of content: loadable and from a config. Loadable c `arrows? bool` — A flag that indicates whether to show navigation arrows. -`title: Title`: Title. +`title: Title` — Title. -`description?: string`: Description. +`description?: string` — Description. -`randomOrder?: bool`: Enables a random slide order. +`randomOrder?: bool` — Enables a random slide order. -`slidesToShow?: Record<'all' | 'sm' | 'md' | 'lg' | 'xl', number> | number`: How many slides to show on screens of different width. Overrides the default values. They can be overridden for each screen width. You can also set a single numeric value so that the number of slides is always the same (except for mobiles, where the value is always 1). +`slidesToShow?: Record<'all' | 'sm' | 'md' | 'lg' | 'xl', number> | number` — How many slides to show on screens of different width. Overrides the default values. They can be overridden for each screen width. You can also set a single numeric value so that the number of slides is always the same (except for mobiles, where the value is always 1). Default values: @@ -32,6 +36,14 @@ Default values: - `md`: 2 - `sm`: 1 +`type?: string` — Currently supported: `"media-card" | "header-card"`. + +`autoplay?: number` — Autoplay delay between transitions in ms. + +`arrowSize?: number` - Size of arrow icons. Default: `16`. + +`adaptive?: boolean` - Adapt slider height to the height of the active slide. Default: `false`. + `loadable: Loadable` — Loadable content, the following data sources are currently supported: - `events` — Events. @@ -50,4 +62,5 @@ The following blocks are currently supported: - [`MediaCard` — Card with an image](?path=/story/блоки-media--default&viewMode=docs) - [`PriceCard` — Price card](?path=/story/components-cards-pricecard--default&viewMode=docs) +`onSlideChange | onSlideChangeTransitionStart | onSlideChangeTransitionEnd | onActiveIndexChange | onBreakpoint` [events](https://v6.swiperjs.com/swiper-api#events) supported. If you need more please open an [issue](https://github.com/gravity-ui/page-constructor/issues) or make PR. </StoryTemplate> diff --git a/src/blocks/Slider/__tests__/Slider.test.tsx b/src/blocks/Slider/__tests__/Slider.test.tsx index 043dd9dee..5899638e4 100644 --- a/src/blocks/Slider/__tests__/Slider.test.tsx +++ b/src/blocks/Slider/__tests__/Slider.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {queryHelpers, render} from '@testing-library/react'; +import {render} from '@testing-library/react'; import {BasicCard} from '../../../sub-blocks'; import Slider from '../Slider'; @@ -9,15 +9,18 @@ const EXAMPLE_URL = 'https://example.com'; const SLIDER_TITLE = 'Slider title'; const CARD_TITLE = 'Card title'; const CARD_TEXT = 'Lorem ipsum'; -const CARDS_COUNT = 10; - -const slidesToShowValues = [3, 2, 1]; +const SLIDES_TO_SHOW = 3; +const CARDS_COUNTS = [1, 3, 5, 10]; describe('Slider', () => { - test.each(slidesToShowValues)('Has correct slider labels', async (slidesToShow) => { + test.each(CARDS_COUNTS)('Has correct slider slides', async (cardCount) => { const {container} = render( - <Slider title={{text: SLIDER_TITLE, url: EXAMPLE_URL}} slidesToShow={slidesToShow}> - {Array(CARDS_COUNT) + <Slider + title={{text: SLIDER_TITLE, url: EXAMPLE_URL}} + slidesToShow={SLIDES_TO_SHOW} + dots + > + {Array(cardCount) .fill(null) .map((_, index) => ( <BasicCard @@ -31,35 +34,33 @@ describe('Slider', () => { ); // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access - const barElement = container.querySelector('.pc-SliderBlock__bar'); - if (slidesToShow > 1) { - expect(barElement).toBeTruthy(); - } else { - expect(barElement).toBeFalsy(); - } + const slides = container.querySelectorAll('.pc-SliderBlock__slide'); + expect(slides.length).toEqual(cardCount); + }); - const barDotsCount = CARDS_COUNT - slidesToShow + 1; + test('Slider has no arrows', () => { + const {container} = render( + <Slider + title={{text: SLIDER_TITLE, url: EXAMPLE_URL}} + slidesToShow={SLIDES_TO_SHOW} + dots + arrows={false} + > + {Array(3) + .fill(null) + .map((_, index) => ( + <BasicCard + url={EXAMPLE_URL} + title={CARD_TITLE} + text={CARD_TEXT} + key={index} + /> + ))} + </Slider>, + ); - // Checking labels for the first slide - // There we have a bar covering `slidesToShow` dots // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access - const accessibleBarElement = container.querySelector('.pc-SliderBlock__accessible-bar'); - expect(accessibleBarElement?.getAttribute('aria-label')).toBe(`Page 1 of ${barDotsCount}`); - expect( - queryHelpers.queryAllByAttribute('aria-label', container, `Page 1 of ${barDotsCount}`), - ).toHaveLength(slidesToShow + 1); - - // Checking labels for the slides starting from 2 - Array(barDotsCount - 1) - .fill(null) - .forEach((_, index) => { - expect( - queryHelpers.queryAllByAttribute( - 'aria-label', - container, - `Page ${index + 2} of ${barDotsCount}`, - ), - ).toHaveLength(1); - }); + const arrow = container.querySelector('.pc-SliderBlock__arrow'); + expect(arrow).toBeFalsy(); }); }); diff --git a/src/blocks/Slider/i18n/en.json b/src/blocks/Slider/i18n/en.json index c72f11a9e..b4a01d664 100644 --- a/src/blocks/Slider/i18n/en.json +++ b/src/blocks/Slider/i18n/en.json @@ -1,6 +1,6 @@ { "arrow-right": "Next", "arrow-left": "Previous", - "dot-label": "Page {{index}} of {{count}}", + "dot-label": "Page {{index}}", "pagination-label": "Pages" } diff --git a/src/blocks/Slider/i18n/ru.json b/src/blocks/Slider/i18n/ru.json index fb16cd7bc..c9547fa2f 100644 --- a/src/blocks/Slider/i18n/ru.json +++ b/src/blocks/Slider/i18n/ru.json @@ -1,6 +1,6 @@ { "arrow-right": "Дальше", "arrow-left": "Назад", - "dot-label": "Страница {{index}} из {{count}}", + "dot-label": "Страница {{index}}", "pagination-label": "Страницы" } diff --git a/src/blocks/Slider/models.ts b/src/blocks/Slider/models.ts index bcb40599a..f2b47dc0b 100644 --- a/src/blocks/Slider/models.ts +++ b/src/blocks/Slider/models.ts @@ -1,8 +1,8 @@ export enum SliderBreakpointNames { + Xs = 'xs', Sm = 'sm', Md = 'md', Lg = 'lg', - Xl = 'xl', } export type SliderBreakpointParams = Record<SliderBreakpointNames, number>; diff --git a/src/blocks/Slider/schema.ts b/src/blocks/Slider/schema.ts index 27c0b7a00..58251e8ac 100644 --- a/src/blocks/Slider/schema.ts +++ b/src/blocks/Slider/schema.ts @@ -63,6 +63,15 @@ export const SliderProps = { autoplay: { type: 'number', }, + type: { + type: 'string', + }, + adaptive: { + type: 'boolean', + }, + arrowSize: { + type: 'number', + }, animated: AnimatableProps, slidesToShow: sliderSizesObject, disclaimer: DisclaimerProps, diff --git a/src/blocks/SliderNew/useSlider.tsx b/src/blocks/Slider/useSlider.tsx similarity index 88% rename from src/blocks/SliderNew/useSlider.tsx rename to src/blocks/Slider/useSlider.tsx index d919a29b2..010bff883 100644 --- a/src/blocks/SliderNew/useSlider.tsx +++ b/src/blocks/Slider/useSlider.tsx @@ -1,4 +1,4 @@ -import React, {PropsWithChildren, useEffect, useMemo, useState} from 'react'; +import React, {PropsWithChildren, useCallback, useEffect, useMemo, useState} from 'react'; import type {Swiper} from 'swiper'; @@ -57,6 +57,10 @@ export const useSlider = ({children, autoplayMs, type, ...props}: UseSliderProps slider.slidePrev(); }; + const handleImagesReady = useCallback((localSlider: Swiper) => { + setTimeout(() => localSlider.update(), 100); + }, []); + useEffect(() => { if (!slider) { return; @@ -74,6 +78,7 @@ export const useSlider = ({children, autoplayMs, type, ...props}: UseSliderProps onSwiper: setSlider, onNext: handleNext, onPrev: handlePrev, + onImagesReady: handleImagesReady, breakpoints, childrenCount, isLocked, diff --git a/src/blocks/SliderNew/useSliderPagination.ts b/src/blocks/Slider/useSliderPagination.ts similarity index 100% rename from src/blocks/SliderNew/useSliderPagination.ts rename to src/blocks/Slider/useSliderPagination.ts diff --git a/src/blocks/Slider/utils.ts b/src/blocks/Slider/utils.ts index 23c0ec8e6..dbeb05261 100644 --- a/src/blocks/Slider/utils.ts +++ b/src/blocks/Slider/utils.ts @@ -1,174 +1,69 @@ -import {useEffect, useRef, useState} from 'react'; +import {useEffect, useState} from 'react'; +import isEqual from 'lodash/isEqual'; import pickBy from 'lodash/pickBy'; +import type {SwiperOptions} from 'swiper/types/swiper-options'; import {BREAKPOINTS} from '../../constants'; import {SliderBreakpointNames, SliderBreakpointParams, SlidesToShow} from './models'; export const DEFAULT_SLIDE_BREAKPOINTS = { - [SliderBreakpointNames.Xl]: 3, - [SliderBreakpointNames.Lg]: 2, + [SliderBreakpointNames.Lg]: 3, [SliderBreakpointNames.Md]: 2, - [SliderBreakpointNames.Sm]: 1.15, + [SliderBreakpointNames.Sm]: 2, + [SliderBreakpointNames.Xs]: 1.15, }; -const BREAKPOINT_NAMES_BY_VALUES = Object.entries(BREAKPOINTS).reduce< - Record<number, SliderBreakpointNames> ->((acc, [key, value]) => ({...acc, [value]: key as SliderBreakpointNames}), {}); - export interface GetSlidesToShowParams { contentLength: number; - breakpoints?: SlidesToShow; + slidesToShow?: SlidesToShow; mobileFullscreen?: boolean; } - -export const isFocusable = (element: Element): boolean => { - if (!(element instanceof HTMLElement)) { - return false; - } - const tabIndexAttr = element.getAttribute('tabindex'); - const hasTabIndex = tabIndexAttr !== null; - const tabIndex = Number(tabIndexAttr); - if (element.ariaHidden === 'true' || (hasTabIndex && tabIndex < 0)) { - return false; - } - if (hasTabIndex && tabIndex >= 0) { - return true; - } - - // without this jest fails here for some reason - let htmlElement: - | HTMLAnchorElement - | HTMLInputElement - | HTMLSelectElement - | HTMLTextAreaElement - | HTMLButtonElement; - switch (true) { - case element instanceof HTMLAnchorElement: - htmlElement = element as HTMLAnchorElement; - return Boolean(htmlElement.href); - case element instanceof HTMLInputElement: - htmlElement = element as HTMLInputElement; - return htmlElement.type !== 'hidden' && !htmlElement.disabled; - case element instanceof HTMLSelectElement: - case element instanceof HTMLTextAreaElement: - case element instanceof HTMLButtonElement: - htmlElement = element as HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement; - return !htmlElement.disabled; - default: - return false; - } -}; - -export function getSlidesToShowWithDefaults({ +export function getSliderResponsiveParams({ contentLength, - breakpoints, + slidesToShow, mobileFullscreen, }: GetSlidesToShowParams) { let result; - if (typeof breakpoints === 'number') { + if (typeof slidesToShow === 'number') { result = Object.keys(DEFAULT_SLIDE_BREAKPOINTS).reduce( - (acc, breakpointName) => ({...acc, [breakpointName]: breakpoints}), + (acc, breakpointName) => ({...acc, [breakpointName]: slidesToShow}), {} as SliderBreakpointParams, ); } else { - result = breakpoints || DEFAULT_SLIDE_BREAKPOINTS; + result = slidesToShow || DEFAULT_SLIDE_BREAKPOINTS; } - return { + const showCount = { ...DEFAULT_SLIDE_BREAKPOINTS, ...pickBy(result, (value) => !isNaN(value)), - sm: !mobileFullscreen && contentLength > 1 ? DEFAULT_SLIDE_BREAKPOINTS.sm : 1, + xs: !mobileFullscreen && contentLength > 1 ? DEFAULT_SLIDE_BREAKPOINTS.xs : 1, }; -} - -export function getSliderResponsiveParams(breakpoints: SliderBreakpointParams) { - return Object.entries(breakpoints).map(([breakpointName, slidesToShow]) => ({ - breakpoint: BREAKPOINTS[breakpointName as SliderBreakpointNames], - settings: {slidesToShow}, - })); -} - -export function getSlidesCountByBreakpoint( - breakpoint: number, - breakpoints: SliderBreakpointParams, -) { - const breakpointName = BREAKPOINT_NAMES_BY_VALUES[breakpoint]; - - return Math.floor(breakpoints[breakpointName]); -} -export function getSlidesToShowCount(breakpoints: SliderBreakpointParams) { - return Math.floor(Math.max(...Object.values(breakpoints))); + return Object.entries(showCount).reduce((res, [breakpointName, value]) => { + // eslint-disable-next-line no-param-reassign + res[BREAKPOINTS[breakpointName as SliderBreakpointNames] + 1] = { + slidesPerView: value, + }; + return res; + }, {} as Record<number, SwiperOptions>); } -const getRovingListItemId = (uniqId: string, index: number) => - `${uniqId}-roving-tabindex-item-${index}`; -export function useRovingTabIndex(props: { - itemCount: number; - activeIndex: number; - firstIndex?: number; - uniqId: string; -}) { - const {itemCount, activeIndex, firstIndex = 0, uniqId} = props; - const [currentIndex, setCurrentIndex] = useState(firstIndex); - const hasFocusRef = useRef(false); - const lastIndex = itemCount + firstIndex - 1; - - const getRovingItemProps = ( - index: number, - ): Pick<React.HTMLAttributes<HTMLElement>, 'id' | 'tabIndex' | 'onFocus'> => { - return { - id: getRovingListItemId(uniqId, index), - tabIndex: index === activeIndex ? 0 : -1, - onFocus: () => { - setCurrentIndex(index); - hasFocusRef.current = true; - }, - }; - }; +export const useMemoized = <T>(value: T): T => { + const [memoizedValue, setMemoizedValue] = useState(value); useEffect(() => { - if (!hasFocusRef.current) { - return; - } - document.getElementById(getRovingListItemId(uniqId, currentIndex))?.focus(); - }, [activeIndex, currentIndex, uniqId]); - - const setNextIndex = () => - setCurrentIndex((prev) => (prev >= lastIndex ? firstIndex : prev + 1)); - const setPrevIndex = () => - setCurrentIndex((prev) => (prev <= firstIndex ? lastIndex : prev - 1)); - - const onRovingListKeyDown: React.KeyboardEventHandler<HTMLElement> = (e) => { - const key = e.key.toLowerCase(); - - if (key !== 'tab' && key !== 'enter') { - e.preventDefault(); - } - - switch (key) { - case 'arrowleft': - case 'arrowup': - setPrevIndex(); - return; - case 'arrowright': - case 'arrowdown': - setNextIndex(); - return; - } - }; - - const onRovingListBlur: React.FocusEventHandler<HTMLElement> = () => { - hasFocusRef.current = false; - }; + setMemoizedValue((memoized) => + value && typeof value === 'object' && isEqual(memoized, value) ? memoized : value, + ); + }, [value]); - const rovingListProps: React.HTMLAttributes<HTMLElement> = { - onKeyDown: onRovingListKeyDown, - onBlur: onRovingListBlur, - }; + return memoizedValue; +}; - return {getRovingItemProps, rovingListProps}; -} +export const setElementAtrributes = (element: Element, attributes: Record<string, unknown>) => + Object.entries(attributes).forEach(([attribute, value]) => + element.setAttribute(attribute, String(value)), + ); diff --git a/src/blocks/SliderNew/Slider.scss b/src/blocks/SliderNew/Slider.scss deleted file mode 100644 index 2b2970465..000000000 --- a/src/blocks/SliderNew/Slider.scss +++ /dev/null @@ -1,286 +0,0 @@ -@import '../../../styles/mixins'; -@import '../../../styles/variables'; - -$block: '.#{$ns}SliderNewBlock'; - -@mixin fullscreen-card() { - @media (max-width: map-get($gridBreakpoints, 'sm')) { - &:not(&_one-slide) { - margin-left: 0; - padding-left: 0; - width: 100%; - overflow: inherit; - } - } -} - -$dotsCn: '.swiper-container-horizontal .swiper-pagination-bullets'; - -$dotSize: 8px; - -#{$block} { - $root: &; - position: relative; - - &__slider { - @include add-specificity(&) { - padding: $indentSM 0 $indentL; - margin: 0 -#{$gridGutter}; - } - } - - &_without-dots { - #{$root}__slider { - padding-bottom: $indentS; - } - } - - &__slide.swiper-slide { - padding: 0 #{$gridGutter}; - box-sizing: border-box; - height: auto; - - @keyframes safari-fix { - from { - transform: translateX(0.001px); - } - to { - transform: translateX(0); - } - } - - // fix text under video in safari - &.swiper-slide-visible { - animation: safari-fix 300ms; - } - } - - #{$root}__slide-item { - width: 100%; - height: 100%; - } - - &__arrow { - position: absolute; - top: -2px; - right: 0; - z-index: 2; - - &_prev { - right: $sliderArrowSize; - margin-right: $indentXS; - } - } - - &__header { - &_no-description { - position: relative; - top: -3px; - } - } - - &__dot { - width: $dotSize; - height: $dotSize; - - border-radius: 50%; - background-color: var(--g-color-line-generic-accent); - cursor: pointer; - display: inline-block; - - &:hover { - background-color: var(--g-color-line-generic-accent-hover); - } - - & + & { - margin-left: $indentXS; - } - - &_active { - opacity: 1; - background-color: var(--g-color-line-generic-active); - } - } - - &_only-arrows { - padding-top: $sliderArrowSize; - } - - &__footer { - display: flex; - position: relative; - - #{$root}__disclaimer { - position: absolute; - top: 0; - left: 0; - color: var(--g-color-text-secondary); - - &_size { - &_l { - @include text-size(header-1); - } - - &_m { - @include text-size(body-2); - } - - &_s { - @include text-size(body-1); - } - } - } - } - - &__animate-slides { - @include animate-slides(#{$root}__slide); - @include animate-slides(#{$root}__dot); - } - - &_type_media-card { - @include fullscreen-card(); - - padding: 0; - - & #{$dotsCn} { - bottom: $indentSM; - } - - #{$root}__slider { - padding: 0; - } - - &:hover { - #{$root}__arrow { - display: flex; - } - } - - #{$root}__arrow { - display: none; - width: 64px; - top: 50%; - transform: translate(0, -50%); - - &_prev { - left: 0; - margin-right: 0; - } - } - - @media (max-width: map-get($gridBreakpoints, 'md')) { - &:hover { - #{$root}__arrow { - display: none; - } - } - } - } - - &_type_header-card { - @include fullscreen-card(); - $arrowWidth: 68px; - $arrowHeight: 68px; - - padding-top: 0; - - #{$root}__wrapper { - position: relative; - } - - & #{$dotsCn} { - bottom: $indentM; - } - - #{$root}__slider { - padding: 0; - margin: 0; - } - - #{$root}__arrow { - top: 50%; - transform: translateY(-50%); - width: $arrowWidth; - height: $arrowHeight; - - right: 0; - - &_prev { - left: 0; - margin-right: 0; - } - - button { - background-color: transparent; - box-shadow: none; - - &:hover { - box-shadow: none; - } - } - - &:hover { - & button { - box-shadow: none; - } - } - } - - #{$root}__slide { - padding: 0; - } - - @media (max-width: map-get($gridBreakpoints, 'sm')) { - #{$root}__arrow { - display: none; - } - - &#{$root}:not(&_one-slide) { - #{$root}__slider { - margin-left: 0; - } - - #{$root}__slide { - // to remove the indentation between slides - padding-right: 0; - padding-left: 0; - - &:last-child { - padding-right: 0; - } - } - } - } - } - - @media (max-width: map-get($gridBreakpoints, 'md')) { - &__footer { - display: block; - - #{$root}__disclaimer { - position: relative; - width: 100%; - padding-bottom: $indentS; - } - } - } - - @media (max-width: map-get($gridBreakpoints, 'sm')) { - $horizontalPadding: $gridContainerMargin + $gridGutterMobile; - &:not(&_one-slide) { - margin-left: #{-$horizontalPadding}; - padding-left: $horizontalPadding; - width: calc(100% + #{$horizontalPadding * 2}); - overflow-x: auto; - - #{$root}__slider { - padding: $indentSM $horizontalPadding $indentL; - margin: 0 0 0 #{-$horizontalPadding}; - } - - #{$root}__slide { - padding: 0 $indentXXXS; - } - } - } -} diff --git a/src/blocks/SliderNew/Slider.tsx b/src/blocks/SliderNew/Slider.tsx deleted file mode 100644 index fd5798bed..000000000 --- a/src/blocks/SliderNew/Slider.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React, {Fragment, PropsWithChildren} from 'react'; - -import SwiperCore, {A11y, Autoplay, Pagination} from 'swiper'; -import {Swiper, SwiperSlide} from 'swiper/react'; - -import Anchor from '../../components/Anchor/Anchor'; -import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; -import Title from '../../components/Title/Title'; -import {ClassNameProps, Refable, SliderProps as SliderParams} from '../../models'; -import {block} from '../../utils'; - -import Arrow from './Arrow/Arrow'; -import {i18n} from './i18n'; -import {useSlider} from './useSlider'; -import {useSliderPagination} from './useSliderPagination'; - -import './Slider.scss'; -import 'swiper/swiper-bundle.css'; - -const b = block('SliderNewBlock'); - -export interface SliderNewProps - extends Omit<SliderParams, 'children'>, - Partial< - Pick< - Swiper, - | 'onSlideChange' - | 'onSlideChangeTransitionStart' - | 'onSlideChangeTransitionEnd' - | 'onActiveIndexChange' - | 'onBreakpoint' - > - >, - Refable<HTMLDivElement>, - ClassNameProps { - type?: string; - anchorId?: string; - dotsClassName?: string; - blockClassName?: string; - arrowSize?: number; -} - -SwiperCore.use([Autoplay, A11y, Pagination]); - -export const SliderNewBlock = ({ - animated, - title, - description, - type, - anchorId, - arrows = true, - adaptive, - autoplay: autoplayMs, - dots = true, - className, - dotsClassName, - disclaimer, - children, - blockClassName, - arrowSize, - slidesToShow, - onSlideChange, - onSlideChangeTransitionStart, - onSlideChangeTransitionEnd, - onActiveIndexChange, - onBreakpoint, -}: PropsWithChildren<SliderNewProps>) => { - const {autoplay, isLocked, childrenCount, breakpoints, onSwiper, onPrev, onNext, setIsLocked} = - useSlider({ - slidesToShow, - children, - type, - autoplayMs, - }); - - const isA11yControlHidden = Boolean(autoplay); - const controlTabIndex = isA11yControlHidden ? -1 : 0; - - const paginationProps = useSliderPagination({ - enabled: dots, - isA11yControlHidden, - controlTabIndex, - bulletClass: b('dot', dotsClassName), - bulletActiveClass: b('dot_active'), - paginationLabel: i18n('pagination-label'), - }); - - return ( - <div - className={b( - { - 'one-slide': childrenCount === 1, - 'only-arrows': !title?.text && !description && arrows, - 'without-dots': !dots || isLocked, - type, - }, - blockClassName, - )} - > - {anchorId && <Anchor id={anchorId} />} - <Title - title={title} - subtitle={description} - className={b('header', {'no-description': !description})} - /> - <AnimateBlock className={b('animate-slides')} animate={animated}> - <Swiper - className={b('slider', className)} - onSwiper={onSwiper} - speed={1000} - autoplay={autoplay} - autoHeight={adaptive} - initialSlide={0} - noSwiping={false} - breakpoints={breakpoints} - onSlideChange={onSlideChange} - onSlideChangeTransitionStart={onSlideChangeTransitionStart} - onSlideChangeTransitionEnd={onSlideChangeTransitionEnd} - onActiveIndexChange={onActiveIndexChange} - onBreakpoint={onBreakpoint} - onLock={() => setIsLocked(true)} - onUnlock={() => setIsLocked(false)} - watchSlidesVisibility - watchOverflow - a11y={{ - slideLabelMessage: '', - paginationBulletMessage: i18n('dot-label', {index: '{{index}}'}), - }} - {...paginationProps} - > - {React.Children.map(children, (elem, index) => ( - <SwiperSlide className={b('slide')} key={index}> - {({isVisible}) => ( - <div - className={b('slide-item')} - aria-hidden={!isA11yControlHidden && !isVisible} - > - {elem} - </div> - )} - </SwiperSlide> - ))} - </Swiper> - {arrows && !isLocked && ( - <Fragment> - <div aria-hidden={isA11yControlHidden}> - <Arrow - className={b('arrow', {prev: true})} - type="left" - onClick={onPrev} - size={arrowSize} - extraProps={{tabIndex: controlTabIndex}} - /> - <Arrow - className={b('arrow', {next: true})} - type="right" - onClick={onNext} - size={arrowSize} - extraProps={{tabIndex: controlTabIndex}} - /> - </div> - </Fragment> - )} - <div className={b('footer')}> - {disclaimer ? ( - <div className={b('disclaimer', {size: disclaimer?.size || 'm'})}> - {disclaimer?.text} - </div> - ) : null} - </div> - </AnimateBlock> - </div> - ); -}; - -export default SliderNewBlock; diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Default-light-chromium-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Default-light-chromium-linux.png deleted file mode 100644 index cc0fe6574..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Default-light-chromium-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Default-light-webkit-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Default-light-webkit-linux.png deleted file mode 100644 index 056542574..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Default-light-webkit-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-QuoteCards-light-chromium-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-QuoteCards-light-chromium-linux.png deleted file mode 100644 index b60e62625..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-QuoteCards-light-chromium-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-QuoteCards-light-webkit-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-QuoteCards-light-webkit-linux.png deleted file mode 100644 index f3042fbd6..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-QuoteCards-light-webkit-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-SlidesToShow-light-chromium-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-SlidesToShow-light-chromium-linux.png deleted file mode 100644 index ce6b27341..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-SlidesToShow-light-chromium-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-SlidesToShow-light-webkit-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-SlidesToShow-light-webkit-linux.png deleted file mode 100644 index e90d35dd8..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-SlidesToShow-light-webkit-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutArrows-light-chromium-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutArrows-light-chromium-linux.png deleted file mode 100644 index 1b70b66fd..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutArrows-light-chromium-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutArrows-light-webkit-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutArrows-light-webkit-linux.png deleted file mode 100644 index e5fb404f8..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutArrows-light-webkit-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutDots-light-chromium-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutDots-light-chromium-linux.png deleted file mode 100644 index fcbdeba7e..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutDots-light-chromium-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutDots-light-webkit-linux.png b/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutDots-light-webkit-linux.png deleted file mode 100644 index 87d83bb1a..000000000 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-WithoutDots-light-webkit-linux.png and /dev/null differ diff --git a/src/blocks/SliderNew/__tests__/Slider.test.tsx b/src/blocks/SliderNew/__tests__/Slider.test.tsx deleted file mode 100644 index f28135cb1..000000000 --- a/src/blocks/SliderNew/__tests__/Slider.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; - -import {render} from '@testing-library/react'; - -import {BasicCard} from '../../../sub-blocks'; -import Slider from '../Slider'; - -const EXAMPLE_URL = 'https://example.com'; -const SLIDER_TITLE = 'Slider title'; -const CARD_TITLE = 'Card title'; -const CARD_TEXT = 'Lorem ipsum'; -const SLIDES_TO_SHOW = 3; -const CARDS_COUNTS = [1, 3, 5, 10]; - -describe('SliderNew', () => { - test.each(CARDS_COUNTS)('Has correct slider slides', async (cardCount) => { - const {container} = render( - <Slider - title={{text: SLIDER_TITLE, url: EXAMPLE_URL}} - slidesToShow={SLIDES_TO_SHOW} - dots - > - {Array(cardCount) - .fill(null) - .map((_, index) => ( - <BasicCard - url={EXAMPLE_URL} - title={CARD_TITLE} - text={CARD_TEXT} - key={index} - /> - ))} - </Slider>, - ); - - // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access - const slides = container.querySelectorAll('.pc-SliderNewBlock__slide'); - expect(slides.length).toEqual(cardCount); - }); - - test('Slider has no arrows', () => { - const {container} = render( - <Slider - title={{text: SLIDER_TITLE, url: EXAMPLE_URL}} - slidesToShow={SLIDES_TO_SHOW} - dots - arrows={false} - > - {Array(3) - .fill(null) - .map((_, index) => ( - <BasicCard - url={EXAMPLE_URL} - title={CARD_TITLE} - text={CARD_TEXT} - key={index} - /> - ))} - </Slider>, - ); - - // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access - const arrow = container.querySelector('.pc-SliderNewBlock__arrow'); - expect(arrow).toBeFalsy(); - }); -}); diff --git a/src/blocks/SliderNew/utils.ts b/src/blocks/SliderNew/utils.ts deleted file mode 100644 index dbeb05261..000000000 --- a/src/blocks/SliderNew/utils.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {useEffect, useState} from 'react'; - -import isEqual from 'lodash/isEqual'; -import pickBy from 'lodash/pickBy'; -import type {SwiperOptions} from 'swiper/types/swiper-options'; - -import {BREAKPOINTS} from '../../constants'; - -import {SliderBreakpointNames, SliderBreakpointParams, SlidesToShow} from './models'; - -export const DEFAULT_SLIDE_BREAKPOINTS = { - [SliderBreakpointNames.Lg]: 3, - [SliderBreakpointNames.Md]: 2, - [SliderBreakpointNames.Sm]: 2, - [SliderBreakpointNames.Xs]: 1.15, -}; - -export interface GetSlidesToShowParams { - contentLength: number; - slidesToShow?: SlidesToShow; - mobileFullscreen?: boolean; -} -export function getSliderResponsiveParams({ - contentLength, - slidesToShow, - mobileFullscreen, -}: GetSlidesToShowParams) { - let result; - - if (typeof slidesToShow === 'number') { - result = Object.keys(DEFAULT_SLIDE_BREAKPOINTS).reduce( - (acc, breakpointName) => ({...acc, [breakpointName]: slidesToShow}), - {} as SliderBreakpointParams, - ); - } else { - result = slidesToShow || DEFAULT_SLIDE_BREAKPOINTS; - } - - const showCount = { - ...DEFAULT_SLIDE_BREAKPOINTS, - ...pickBy(result, (value) => !isNaN(value)), - xs: !mobileFullscreen && contentLength > 1 ? DEFAULT_SLIDE_BREAKPOINTS.xs : 1, - }; - - return Object.entries(showCount).reduce((res, [breakpointName, value]) => { - // eslint-disable-next-line no-param-reassign - res[BREAKPOINTS[breakpointName as SliderBreakpointNames] + 1] = { - slidesPerView: value, - }; - return res; - }, {} as Record<number, SwiperOptions>); -} - -export const useMemoized = <T>(value: T): T => { - const [memoizedValue, setMemoizedValue] = useState(value); - - useEffect(() => { - setMemoizedValue((memoized) => - value && typeof value === 'object' && isEqual(memoized, value) ? memoized : value, - ); - }, [value]); - - return memoizedValue; -}; - -export const setElementAtrributes = (element: Element, attributes: Record<string, unknown>) => - Object.entries(attributes).forEach(([attribute, value]) => - element.setAttribute(attribute, String(value)), - ); diff --git a/src/blocks/SliderNew/Arrow/Arrow.scss b/src/blocks/SliderOld/Arrow/Arrow.scss similarity index 88% rename from src/blocks/SliderNew/Arrow/Arrow.scss rename to src/blocks/SliderOld/Arrow/Arrow.scss index 4ae72b0b9..92f9b5147 100644 --- a/src/blocks/SliderNew/Arrow/Arrow.scss +++ b/src/blocks/SliderOld/Arrow/Arrow.scss @@ -1,7 +1,7 @@ @import '../../../../styles/mixins.scss'; @import '../../../../styles/variables.scss'; -$block: '.#{$ns}slider-new-block-arrow'; +$block: '.#{$ns}slider-old-block-arrow'; %flex { display: flex; @@ -23,6 +23,7 @@ $block: '.#{$ns}slider-new-block-arrow'; #{$root}__icon-wrapper { transform: rotate(180deg); } + margin-right: 16px; } &__button { @@ -38,7 +39,7 @@ $block: '.#{$ns}slider-new-block-arrow'; background-color: var(--g-color-base-background); box-shadow: 0 4px 24px var(--pc-color-sfx-shadow), 0 2px 8px var(--pc-color-sfx-shadow); - transition: box-shadow 0.3s $ease-out-cubic, color 0.3s $ease-out-cubic; + transition: box-shadow 0.3s $ease-out-cubic, color 1s $ease-out-cubic; @include focusable(); } diff --git a/src/blocks/SliderNew/Arrow/Arrow.tsx b/src/blocks/SliderOld/Arrow/Arrow.tsx similarity index 74% rename from src/blocks/SliderNew/Arrow/Arrow.tsx rename to src/blocks/SliderOld/Arrow/Arrow.tsx index 6d73586ee..e5c3fea91 100644 --- a/src/blocks/SliderNew/Arrow/Arrow.tsx +++ b/src/blocks/SliderOld/Arrow/Arrow.tsx @@ -7,24 +7,22 @@ import {i18n} from '../i18n'; import './Arrow.scss'; -const b = block('slider-new-block-arrow'); +const b = block('slider-old-block-arrow'); export type ArrowType = 'left' | 'right'; export interface ArrowProps { type: ArrowType; - onClick?: () => void; + handleClick?: (direction: ArrowType) => void; size?: number; - extraProps?: React.ButtonHTMLAttributes<HTMLButtonElement>; } -const Arrow = ({type, onClick, className, size = 16, extraProps}: ArrowProps & ClassNameProps) => ( +const Arrow = ({type, handleClick, className, size = 16}: ArrowProps & ClassNameProps) => ( <div className={b({type}, className)}> <button className={b('button')} - onClick={onClick} + onClick={handleClick ? () => handleClick(type) : undefined} aria-label={i18n(`arrow-${type}`)} - {...extraProps} > <span className={b('icon-wrapper')}> <ToggleArrow diff --git a/src/blocks/SliderOld/SliderOld.scss b/src/blocks/SliderOld/SliderOld.scss new file mode 100644 index 000000000..1fd899748 --- /dev/null +++ b/src/blocks/SliderOld/SliderOld.scss @@ -0,0 +1,398 @@ +@import '../../../styles/mixins'; +@import '../../../styles/variables'; +@import './slick.scss'; + +$slideOffset: 8px; + +$block: '.#{$ns}SliderOldBlock'; + +@mixin fullscreen-card() { + .slick-slide { + width: 100%; + } + + @media (max-width: map-get($gridBreakpoints, 'sm')) { + &:not(&_one-slide) { + margin-left: 0; + padding-left: 0; + width: 100%; + overflow: inherit; + + .slick-list { + margin: 0; + } + + .slick-slide { + &:last-child { + padding-right: 10px; + } + } + } + } +} + +#{$block} { + $root: &; + + .slick-list { + padding: 24px 0 $indentS; + margin: 0 -#{$gridGutter}; + } + + .slick-slide { + padding: 0 #{$gridGutter}; + box-sizing: border-box; + flex-shrink: 0; + } + + .slick-track { + display: flex; + min-width: 100%; + + .slick-slide { + height: auto; + } + + .slick-slide > div { + display: flex; + width: 100%; + height: 100%; + } + } + + .slick-arrow { + position: absolute; + top: -#{$sliderArrowSize}; + right: 0; + + z-index: 2; + + &.slick-prev { + right: $sliderArrowSize; + } + } + + &__header { + &_no-description { + position: relative; + top: -3px; + } + } + + &__dots { + display: flex; + justify-content: center; + width: 100%; + } + + &__dots-list { + @include reset-list-style(); + + position: relative; + display: inline-flex; + justify-content: center; + + li#{$root}__bar, + li#{$root}__accessible-bar, + li#{$root}__dot { + margin: calc(#{$indentXXS} / 2) $indentXXXS; + top: 0; + } + } + + &__dot { + margin-top: $indentXXS; + width: 8px; + height: 8px; + + border-radius: 100%; + background-color: var(--g-color-line-generic-accent); + cursor: pointer; + + transition: background-color 1s; + + &:hover { + background-color: var(--g-color-line-generic-accent-hover); + } + + & + & { + margin-left: 16px; + } + + &_active { + background-color: var(--g-color-line-generic-active); + } + } + + &__bar, + &__accessible-bar { + position: absolute; + top: $indentXXS; + left: 0; + width: 24px; + height: 8px; + border-radius: $borderRadius; + } + + &__bar { + transition: left 0.3s; + background-color: var(--pc-color-line-generic-active-solid); + } + + &_align-left { + .slick-track { + /* stylelint-disable-next-line declaration-no-important */ + width: inherit !important; + } + } + + &_only-arrows { + padding-top: $sliderArrowSize; + } + + &__footer { + display: flex; + position: relative; + + #{$root}__disclaimer { + position: absolute; + top: 0; + left: 0; + color: var(--g-color-text-secondary); + + &_size { + &_l { + @include text-size(header-1); + } + + &_m { + @include text-size(body-2); + } + + &_s { + @include text-size(body-1); + } + } + } + } + + &__animate-slides { + @include animate-slides('.slick-slide'); + } + + &_type_media-card { + @include fullscreen-card(); + + padding: 0; + + #{$root}__dots { + position: absolute; + bottom: 24px; + left: 0; + width: 100%; + } + + &:hover { + .slick-arrow { + display: flex; + } + } + + .slick-arrow { + display: none; + width: 64px; + top: 50%; + transform: translate(0, -50%); + } + + .slick-prev { + left: 0; + } + + .slick-next { + right: 0; + } + + .slick-list { + padding: 0; + } + + @media (max-width: map-get($gridBreakpoints, 'md')) { + &:hover { + .slick-arrow { + display: none; + } + } + } + } + + &_type_header-card { + @include fullscreen-card(); + $arrowWidth: 68px; + $arrowHeight: 68px; + + $arrowIndent: 16px; + + padding-top: 0; + + #{$root}__wrapper { + position: relative; + } + + #{$root}__dots { + position: absolute; + bottom: 16px; + left: 50%; + transform: translateX(-50%); + z-index: 100; + } + + .slick-arrow { + top: 50%; + transform: translateY(-50%); + right: $arrowIndent; + + &.slick-prev { + left: $arrowIndent; + } + + button { + background-color: transparent; + box-shadow: none; + + &:hover { + box-shadow: none; + } + } + + &:hover { + & button { + box-shadow: none; + } + } + } + + &:has(.slick-active .#{$ns}header-block_controls-view_light) { + .#{$ns}slider-old-block-arrow__button { + color: var(--g-color-text-dark-primary); + } + + .#{$ns}SliderOldBlock__dot { + background-color: var(--g-color-private-black-150); + + &_active { + background-color: var(--g-color-private-black-300); + } + } + } + + &:has(.slick-active .#{$ns}header-block_controls-view_dark) { + .#{$ns}slider-old-block-arrow__button { + color: var(--g-color-text-light-primary); + } + + .#{$ns}SliderOldBlock__dot { + background-color: var(--g-color-private-white-150); + + &_active { + background-color: var(--g-color-private-white-300); + } + } + } + + .slick-slide { + padding: 0; + + @keyframes safari-fix { + from { + transform: translateX(0.001px); + } + to { + transform: translateX(0); + } + } + + // fix text under video in safari + &[aria-hidden='true'] { + animation: safari-fix 1000ms; + } + } + + .slick-list { + padding: 0; + margin: 0; + } + + .slick-arrow { + width: $arrowWidth; + height: $arrowHeight; + + right: 0; + &.slick-prev { + left: 0; + } + } + + @media (max-width: map-get($gridBreakpoints, 'sm')) { + .slick-arrow { + display: none; + } + + &#{$root}:not(&_one-slide) { + .slick-list { + margin-left: 0; + } + + .slick-track { + padding-left: 0; + } + + .slick-slide { + // to remove the indentation between slides + /* stylelint-disable declaration-no-important */ + padding-right: 0 !important; + padding-left: 0 !important; + /* stylelint-enable declaration-no-important */ + + &:last-child { + padding-right: 0; + } + } + } + } + } + + @media (max-width: map-get($gridBreakpoints, 'md')) { + &__footer { + display: block; + + #{$root}__disclaimer { + position: relative; + width: 100%; + padding-bottom: $indentS; + } + } + } + + @media (max-width: map-get($gridBreakpoints, 'sm')) { + &:not(&_one-slide) { + margin-left: -($gridContainerMargin + $gridGutterMobile); + padding-left: $gridContainerMargin + $gridGutterMobile; + width: calc(100% + #{($gridContainerMargin + $gridGutterMobile) * 2}); + overflow-x: auto; + + .slick-list { + margin-left: -($gridContainerMargin + $gridGutterMobile); + margin-right: 0; + } + + .slick-track { + padding-left: $gridContainerMargin + $gridGutterMobile - $slideOffset; + } + + .slick-slide { + padding: 0 $slideOffset; + } + } + } +} diff --git a/src/blocks/SliderOld/SliderOld.tsx b/src/blocks/SliderOld/SliderOld.tsx new file mode 100644 index 000000000..aa144db06 --- /dev/null +++ b/src/blocks/SliderOld/SliderOld.tsx @@ -0,0 +1,518 @@ +import React, { + Fragment, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; + +import {useUniqId} from '@gravity-ui/uikit'; +import debounce from 'lodash/debounce'; +import get from 'lodash/get'; +import noop from 'lodash/noop'; +import SlickSlider, {Settings} from 'react-slick'; + +import Anchor from '../../components/Anchor/Anchor'; +import AnimateBlock from '../../components/AnimateBlock/AnimateBlock'; +import OutsideClick from '../../components/OutsideClick/OutsideClick'; +import Title from '../../components/Title/Title'; +import {BREAKPOINTS} from '../../constants'; +import {MobileContext} from '../../context/mobileContext'; +import {SSRContext} from '../../context/ssrContext'; +import {StylesContext} from '../../context/stylesContext/StylesContext'; +import useFocus from '../../hooks/useFocus'; +import { + ClassNameProps, + Refable, + SliderOldProps as SliderParams, + SliderType, + Timeout, +} from '../../models'; +import {block} from '../../utils'; + +import Arrow, {ArrowType} from './Arrow/Arrow'; +import {i18n} from './i18n'; +import {SliderBreakpointParams} from './models'; +import { + getSliderResponsiveParams, + getSlidesCountByBreakpoint, + getSlidesToShowCount, + getSlidesToShowWithDefaults, + isFocusable, + useRovingTabIndex, +} from './utils'; + +import './SliderOld.scss'; + +const b = block('SliderOldBlock'); +const slick = block('slick-origin'); + +const DOT_WIDTH = 8; +const DOT_GAP = 16; + +/** @deprecated */ +export interface SliderOldProps + extends Omit<SliderParams, 'children'>, + Refable<HTMLDivElement>, + ClassNameProps, + Pick<Settings, 'lazyLoad'> { + type?: string; + anchorId?: string; + onAfterChange?: (index: number) => void; + onBeforeChange?: (current: number, next: number) => void; + dotsClassName?: string; + blockClassName?: string; + arrowSize?: number; + initialIndex?: number; +} + +// eslint-disable-next-line valid-jsdoc +/** @deprecated */ +export const SliderOldBlock = (props: React.PropsWithChildren<SliderOldProps>) => { + const { + animated, + title, + description, + type, + anchorId, + arrows = true, + adaptive, + autoplay: autoplaySpeed, + dots = true, + dotsClassName, + disclaimer, + children, + className, + blockClassName, + lazyLoad, + arrowSize, + onAfterChange: handleAfterChange, + onBeforeChange: handleBeforeChange, + initialIndex = 0, + } = props; + + const {isServer} = useContext(SSRContext); + const isMobile = useContext(MobileContext); + const [breakpoint, setBreakpoint] = useState<number>(BREAKPOINTS.xl); + const sliderId = useUniqId(); + const disclosedChildren = useMemo<React.ReactElement[]>( + () => discloseAllNestedChildren(children as React.ReactElement[], sliderId), + [children, sliderId], + ); + const childrenCount = disclosedChildren.length; + const isAutoplayEnabled = autoplaySpeed !== undefined && autoplaySpeed > 0; + const isUserInteractionRef = useRef(false); + + const [slidesToShow] = useState<SliderBreakpointParams>( + getSlidesToShowWithDefaults({ + contentLength: childrenCount, + breakpoints: props.slidesToShow, + mobileFullscreen: Boolean( + props.type && Object.values(SliderType).includes(props.type as SliderType), + ), + }), + ); + + const slidesToShowCount = getSlidesToShowCount(slidesToShow); + const slidesCountByBreakpoint = getSlidesCountByBreakpoint(breakpoint, slidesToShow); + + const [currentIndex, setCurrentIndex] = useState<number>(initialIndex); + const [childStyles, setChildStyles] = useState<Object>({}); + const [slider, setSlider] = useState<SlickSlider>(); + const prevIndexRef = useRef<number>(0); + const autoplayTimeId = useRef<Timeout>(); + const {hasFocus, unsetFocus} = useFocus(slider?.innerSlider?.list); + + const asUserInteraction = + <T extends unknown[], R>(fn: (...args: T) => R) => + (...args: T): R => { + isUserInteractionRef.current = true; + return fn(...args); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + const onResize = useCallback( + debounce(() => { + if (!slider) { + return; + } + + const newBreakpoint = get(slider, 'state.breakpoint') || BREAKPOINTS.xl; + + if (newBreakpoint !== breakpoint) { + setBreakpoint(newBreakpoint); + setCurrentIndex(0); + + slider.slickGoTo(0); + } + }, 100), + [slider, breakpoint], + ); + + const scrollLastSlide = useCallback( + (current: number) => { + const lastSlide = childrenCount - slidesToShowCount; + + if (isAutoplayEnabled && lastSlide === current) { + // Slick doesn't support autoplay with no infinity scroll + autoplayTimeId.current = setTimeout(() => { + if (slider) { + slider.slickGoTo(0, false); + slider.slickPause(); + } + setTimeout(() => { + if (slider) { + slider.slickPlay(); + } + }, 500); + }, autoplaySpeed); + } + }, + [autoplaySpeed, childrenCount, isAutoplayEnabled, slider, slidesToShowCount], + ); + + useEffect(() => { + if (hasFocus && autoplayTimeId.current) { + clearTimeout(autoplayTimeId.current); + } else { + scrollLastSlide(currentIndex); + } + }, [currentIndex, hasFocus, scrollLastSlide]); + + useEffect(() => { + onResize(); + + window.addEventListener('resize', onResize, {passive: true}); + + return () => window.removeEventListener('resize', onResize); + }, [onResize]); + + const handleArrowClick = (direction: ArrowType) => { + let nextIndex; + + if (direction === 'right') { + nextIndex = + currentIndex === childrenCount - slidesCountByBreakpoint ? 0 : currentIndex + 1; + } else { + nextIndex = + currentIndex === 0 ? childrenCount - slidesCountByBreakpoint : currentIndex - 1; + } + + if (slider) { + slider.slickGoTo(nextIndex); + } + }; + + const onBeforeChange = useCallback( + (current: number, next: number) => { + if (handleBeforeChange) { + handleBeforeChange(current, next); + } + + prevIndexRef.current = current; + + setCurrentIndex(Math.ceil(next)); + }, + [handleBeforeChange], + ); + + const onAfterChange = useCallback( + (current: number) => { + if (handleAfterChange) { + handleAfterChange(current); + } + + if (autoplayTimeId.current) { + clearTimeout(autoplayTimeId.current); + } + + if (!hasFocus) { + scrollLastSlide(current); + } + + if (isUserInteractionRef.current) { + const focusIndex = + prevIndexRef.current >= current + ? current + : Math.max(current, prevIndexRef.current + slidesCountByBreakpoint); + + const firstNewSlide = document.getElementById(getSlideId(sliderId, focusIndex)); + if (firstNewSlide) { + const focusableChild = Array.from(firstNewSlide.querySelectorAll('*')).find( + isFocusable, + ) as HTMLElement | undefined; + focusableChild?.focus(); + } + } + + isUserInteractionRef.current = false; + }, + [handleAfterChange, hasFocus, scrollLastSlide, sliderId, slidesCountByBreakpoint], + ); + + const handleDotClick = (index: number) => { + const nextIndex = index > currentIndex ? index + 1 - slidesCountByBreakpoint : index; + + if (slider) { + slider.slickGoTo(nextIndex); + } + }; + + const barSlidesCount = childrenCount - slidesCountByBreakpoint + 1; + const barPosition = (DOT_GAP + DOT_WIDTH) * currentIndex; + const barWidth = DOT_WIDTH + (DOT_GAP + DOT_WIDTH) * (slidesCountByBreakpoint - 1); + + const {getRovingItemProps, rovingListProps} = useRovingTabIndex({ + itemCount: barSlidesCount, + activeIndex: currentIndex + 1, + firstIndex: 1, + uniqId: sliderId, + }); + + const renderBar = () => { + return ( + slidesCountByBreakpoint > 1 && ( + <li + className={b('bar')} + style={{ + left: barPosition, + width: barWidth, + }} + /> + ) + ); + }; + + // renders additional bar, not visible in the layout but visible for screenreaders + const renderAccessibleBar = (index: number) => { + return ( + // To have this key differ from keys used in renderDot function, added `-accessible-bar` part + <Fragment key={`${index}-accessible-bar`}> + {slidesCountByBreakpoint > 0 && ( + <li + className={b('accessible-bar')} + role="menuitemradio" + aria-checked + aria-label={i18n('dot-label', { + index: currentIndex + 1, + count: barSlidesCount, + })} + style={{ + left: barPosition, + width: barWidth, + }} + {...getRovingItemProps(currentIndex + 1)} + /> + )} + </Fragment> + ); + }; + + const getCurrentSlideNumber = (index: number) => { + const currentIndexDiff = index - currentIndex; + + let currentSlideNumber; + if (0 <= currentIndexDiff && currentIndexDiff < slidesCountByBreakpoint) { + currentSlideNumber = currentIndex + 1; + } else if (currentIndexDiff >= slidesCountByBreakpoint) { + currentSlideNumber = index - slidesCountByBreakpoint + 2; + } else { + currentSlideNumber = index + 1; + } + return currentSlideNumber; + }; + const isVisibleSlide = (index: number) => { + const currentIndexDiff = index - currentIndex; + + const result = + slidesCountByBreakpoint > 0 && + 0 <= currentIndexDiff && + currentIndexDiff < slidesCountByBreakpoint; + return result; + }; + + const renderDot = (index: number) => { + const isVisible = isVisibleSlide(index); + const currentSlideNumber = getCurrentSlideNumber(index); + const rovingItemProps = isVisible ? undefined : getRovingItemProps(currentSlideNumber); + return ( + <li + key={index} + className={b('dot', {active: index === currentIndex})} + onClick={asUserInteraction(() => handleDotClick(index))} + onKeyDown={(e) => { + const key = e.key.toLowerCase(); + if (key === 'space' || key === 'enter') { + e.currentTarget.click(); + } + }} + role="menuitemradio" + aria-checked={false} + tabIndex={-1} + aria-hidden={isVisible} + aria-label={i18n('dot-label', { + index: currentSlideNumber, + count: barSlidesCount, + })} + {...rovingItemProps} + /> + ); + }; + + const renderNavigation = () => { + if (childrenCount <= slidesCountByBreakpoint || !dots || childrenCount === 1) { + return null; + } + const dotsList = Array(childrenCount) + .fill(null) + .map((_item, index) => renderDot(index)); + dotsList.splice(currentIndex, 0, renderAccessibleBar(currentIndex)); + + return ( + <div className={b('dots', dotsClassName)}> + <ul + className={b('dots-list')} + role="menu" + aria-label={i18n('pagination-label')} + {...rovingListProps} + > + {renderBar()} + {dotsList} + </ul> + </div> + ); + }; + + const renderDisclaimer = () => { + return disclaimer ? ( + <div className={b('disclaimer', {size: disclaimer.size || 'm'})}>{disclaimer.text}</div> + ) : null; + }; + + const renderSlider = () => { + /* Disable adding of width in inline styles when SSR to prevent overriding of default styles */ + /* Calculate appropriate breakpoint for mobile devices with user agent */ + const variableWidth = isServer && isMobile; + + const settings = { + ref: (slickSlider: SlickSlider) => setSlider(slickSlider), + className: slick(null, className), + arrows, + variableWidth, + infinite: false, + speed: 1000, + adaptiveHeight: adaptive, + autoplay: isAutoplayEnabled, + autoplaySpeed, + slidesToShow: slidesToShowCount, + slidesToScroll: 1, + responsive: getSliderResponsiveParams(slidesToShow), + beforeChange: onBeforeChange, + afterChange: onAfterChange, + initialSlide: initialIndex, + nextArrow: ( + <Arrow + type="right" + handleClick={asUserInteraction(handleArrowClick)} + size={arrowSize} + /> + ), + prevArrow: ( + <Arrow + type="left" + handleClick={asUserInteraction(handleArrowClick)} + size={arrowSize} + /> + ), + lazyLoad, + accessibility: false, + }; + + return ( + <OutsideClick onOutsideClick={isMobile ? unsetFocus : noop}> + <SlickSlider {...settings}>{disclosedChildren}</SlickSlider> + <div className={b('footer')}> + {renderDisclaimer()} + {renderNavigation()} + </div> + </OutsideClick> + ); + }; + + return ( + <StylesContext.Provider value={{...childStyles, setStyles: setChildStyles}}> + <div + className={b( + { + 'align-left': childrenCount < slidesCountByBreakpoint, + 'one-slide': childrenCount === 1, + 'only-arrows': !title?.text && !description && arrows, + mobile: isMobile, + type, + }, + blockClassName, + )} + > + {anchorId && <Anchor id={anchorId} />} + <Title + title={title} + subtitle={description} + className={b('header', {'no-description': !description})} + /> + <AnimateBlock className={b('animate-slides')} animate={animated}> + {renderSlider()} + </AnimateBlock> + </div> + </StylesContext.Provider> + ); +}; + +function getSlideId(sliderId: string, index: number) { + return `slider-${sliderId}-child-${index}`; +} + +// TODO remove this and rework PriceDetailed CLOUDFRONT-12230 +function discloseAllNestedChildren( + children: React.ReactElement[], + sliderId: string, +): React.ReactElement[] { + if (!children) { + return []; + } + + let childIndex = 0; + const wrapped = (child: React.ReactElement) => { + const id = getSlideId(sliderId, childIndex++); + + return ( + <div key={id} id={id}> + {child} + </div> + ); + }; + + return React.Children.map(children, (child) => { + if (child) { + // TODO: if child has 'items' then 'items' determinate like nested children for Slider. + const nestedChildren = child.props.data?.items; + + if (nestedChildren) { + return nestedChildren.map((nestedChild: React.ReactElement) => { + return wrapped( + React.cloneElement(child, { + data: { + ...child.props.data, + items: [nestedChild], + }, + }), + ); + }); + } + } + return child && wrapped(child); + }).filter(Boolean); +} + +export default SliderOldBlock; diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Banners-light-chromium-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Banners-light-chromium-linux.png similarity index 84% rename from src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Banners-light-chromium-linux.png rename to src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Banners-light-chromium-linux.png index b5143c5d3..12ed2a538 100644 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Banners-light-chromium-linux.png and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Banners-light-chromium-linux.png differ diff --git a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Banners-light-webkit-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Banners-light-webkit-linux.png similarity index 99% rename from src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Banners-light-webkit-linux.png rename to src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Banners-light-webkit-linux.png index 8273cc452..c453fd40c 100644 Binary files a/src/blocks/SliderNew/__snapshots__/Slider.visual.test.tsx-snapshots/SliderNew-render-stories-Banners-light-webkit-linux.png and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Banners-light-webkit-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Default-light-chromium-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Default-light-chromium-linux.png new file mode 100644 index 000000000..182eaf119 Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Default-light-chromium-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Default-light-webkit-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Default-light-webkit-linux.png new file mode 100644 index 000000000..4700ed062 Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-Default-light-webkit-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-QuoteCards-light-chromium-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-QuoteCards-light-chromium-linux.png new file mode 100644 index 000000000..1d2b70628 Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-QuoteCards-light-chromium-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-QuoteCards-light-webkit-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-QuoteCards-light-webkit-linux.png new file mode 100644 index 000000000..3df57cbf4 Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-QuoteCards-light-webkit-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-SlidesToShow-light-chromium-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-SlidesToShow-light-chromium-linux.png new file mode 100644 index 000000000..225c568c6 Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-SlidesToShow-light-chromium-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-SlidesToShow-light-webkit-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-SlidesToShow-light-webkit-linux.png new file mode 100644 index 000000000..c74c6c479 Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-SlidesToShow-light-webkit-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutArrows-light-chromium-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutArrows-light-chromium-linux.png new file mode 100644 index 000000000..8f22599bc Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutArrows-light-chromium-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutArrows-light-webkit-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutArrows-light-webkit-linux.png new file mode 100644 index 000000000..baadf4ec1 Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutArrows-light-webkit-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutDots-light-chromium-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutDots-light-chromium-linux.png new file mode 100644 index 000000000..bea1e6825 Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutDots-light-chromium-linux.png differ diff --git a/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutDots-light-webkit-linux.png b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutDots-light-webkit-linux.png new file mode 100644 index 000000000..c1d38c7d6 Binary files /dev/null and b/src/blocks/SliderOld/__snapshots__/Slider.visual.test.tsx-snapshots/Slider-Old-render-stories-WithoutDots-light-webkit-linux.png differ diff --git a/src/blocks/SliderNew/__stories__/Slider.mdx b/src/blocks/SliderOld/__stories__/Slider.mdx similarity index 63% rename from src/blocks/SliderNew/__stories__/Slider.mdx rename to src/blocks/SliderOld/__stories__/Slider.mdx index d22709a74..2602c6f46 100644 --- a/src/blocks/SliderNew/__stories__/Slider.mdx +++ b/src/blocks/SliderOld/__stories__/Slider.mdx @@ -6,14 +6,13 @@ import * as SliderStories from './Slider.stories.tsx'; <Meta of={SliderStories} /> <StoryTemplate> -##### Slider new block. based on [swiper](https://v6.swiperjs.com/react) library. -<br/><br/> +For any reason you can still use old Slider block. By default prefer to use new one [Slider](?path=/story/blocks-slider--docs&viewMode=docs). ## Parameters The slider supports two types of content: loadable and from a config. Loadable content is loaded via the `loadable` property and content from a config via the `children` property. -`type: "slider-new-block"` +`type: "slider-old-block"` `animated?: bool` — Enables/disables animation for the block (enabled by default). @@ -21,13 +20,13 @@ The slider supports two types of content: loadable and from a config. Loadable c `arrows? bool` — A flag that indicates whether to show navigation arrows. -`title: Title` — Title. +`title: Title`: Title. -`description?: string` — Description. +`description?: string`: Description. -`randomOrder?: bool` — Enables a random slide order. +`randomOrder?: bool`: Enables a random slide order. -`slidesToShow?: Record<'all' | 'sm' | 'md' | 'lg' | 'xl', number> | number` — How many slides to show on screens of different width. Overrides the default values. They can be overridden for each screen width. You can also set a single numeric value so that the number of slides is always the same (except for mobiles, where the value is always 1). +`slidesToShow?: Record<'all' | 'sm' | 'md' | 'lg' | 'xl', number> | number`: How many slides to show on screens of different width. Overrides the default values. They can be overridden for each screen width. You can also set a single numeric value so that the number of slides is always the same (except for mobiles, where the value is always 1). Default values: @@ -36,14 +35,6 @@ Default values: - `md`: 2 - `sm`: 1 -`type?: string` — Currently supported: `"media-card" | "header-card"`. - -`autoplay?: number` — Autoplay delay between transitions in ms. - -`arrowSize?: number` - Size of arrow icons. Default: `16`. - -`adaptive?: boolean` - Adapt slider height to the height of the active slide. Default: `false`. - `loadable: Loadable` — Loadable content, the following data sources are currently supported: - `events` — Events. @@ -62,5 +53,4 @@ The following blocks are currently supported: - [`MediaCard` — Card with an image](?path=/story/блоки-media--default&viewMode=docs) - [`PriceCard` — Price card](?path=/story/components-cards-pricecard--default&viewMode=docs) -`onSlideChange | onSlideChangeTransitionStart | onSlideChangeTransitionEnd | onActiveIndexChange | onBreakpoint` [events](https://v6.swiperjs.com/swiper-api#events) supported. If you need more please open an [issue](https://github.com/gravity-ui/page-constructor/issues) or make PR. </StoryTemplate> diff --git a/src/blocks/SliderNew/__stories__/Slider.stories.tsx b/src/blocks/SliderOld/__stories__/Slider.stories.tsx similarity index 80% rename from src/blocks/SliderNew/__stories__/Slider.stories.tsx rename to src/blocks/SliderOld/__stories__/Slider.stories.tsx index 51dee4e27..4bc99c659 100644 --- a/src/blocks/SliderNew/__stories__/Slider.stories.tsx +++ b/src/blocks/SliderOld/__stories__/Slider.stories.tsx @@ -3,14 +3,14 @@ import React from 'react'; import {Meta, StoryFn} from '@storybook/react'; import {PageConstructor} from '../../../containers/PageConstructor'; -import {BannerCardModel, BasicCardModel, SliderNewBlockModel} from '../../../models'; -import Slider from '../Slider'; +import {BannerCardModel, BasicCardModel, SliderOldBlockModel} from '../../../models'; +import SliderOld from '../SliderOld'; import data from './data.json'; export default { - title: 'Lab/SliderNew', - component: Slider, + title: 'Blocks/SliderOld (deprecated)', + component: SliderOld, args: { dots: true, disclaimer: undefined, @@ -25,11 +25,11 @@ export default { }, } as Meta; -const DefaultTemplate: StoryFn<SliderNewBlockModel> = (args) => ( +const DefaultTemplate: StoryFn<SliderOldBlockModel> = (args) => ( <PageConstructor content={{blocks: [args]}} /> ); -const SlidesToShowTemplate: StoryFn<SliderNewBlockModel> = (args) => ( +const SlidesToShowTemplate: StoryFn<SliderOldBlockModel> = (args) => ( <PageConstructor content={{ blocks: [ @@ -70,22 +70,22 @@ export const WithoutArrows = DefaultTemplate.bind({}); export const WithoutDots = DefaultTemplate.bind({}); export const SlidesToShow = SlidesToShowTemplate.bind({}); -Default.args = data.default.content as SliderNewBlockModel; -QuoteCards.args = data.quoteCards.content as SliderNewBlockModel; -Banners.args = data.banners.content as SliderNewBlockModel; +Default.args = data.default.content as SliderOldBlockModel; +QuoteCards.args = data.quoteCards.content as SliderOldBlockModel; +Banners.args = data.banners.content as SliderOldBlockModel; AutoPlay.args = { ...data.default.content, ...data.autoPlay.content, -} as SliderNewBlockModel; +} as SliderOldBlockModel; WithoutArrows.args = { ...data.default.content, ...data.withoutArrows.content, -} as SliderNewBlockModel; +} as SliderOldBlockModel; WithoutDots.args = { ...data.default.content, ...data.withoutDots.content, -} as SliderNewBlockModel; +} as SliderOldBlockModel; SlidesToShow.args = { ...data.default.content, -} as SliderNewBlockModel; +} as SliderOldBlockModel; diff --git a/src/blocks/SliderNew/__stories__/data.json b/src/blocks/SliderOld/__stories__/data.json similarity index 96% rename from src/blocks/SliderNew/__stories__/data.json rename to src/blocks/SliderOld/__stories__/data.json index c937c53de..2bef0d27d 100644 --- a/src/blocks/SliderNew/__stories__/data.json +++ b/src/blocks/SliderOld/__stories__/data.json @@ -2,7 +2,7 @@ "quoteCards": { "content": { "dots": true, - "type": "slider-new-block", + "type": "slider-old-block", "title": { "text": "QuoteCards", "url": "https://example.com" @@ -56,7 +56,7 @@ "banners": { "content": { "dots": true, - "type": "slider-new-block", + "type": "slider-old-block", "title": { "text": "Banners", "url": "https://example.com" @@ -105,9 +105,9 @@ }, "default": { "content": { - "type": "slider-new-block", + "type": "slider-old-block", "title": { - "text": "Slider New", + "text": "Slider", "url": "https://example.com" }, "children": [ @@ -151,19 +151,19 @@ }, "autoPlay": { "content": { - "type": "slider-new-block", + "type": "slider-old-block", "autoplay": 1000 } }, "withoutArrows": { "content": { - "type": "slider-new-block", + "type": "slider-old-block", "arrows": false } }, "withoutDots": { "content": { - "type": "slider-new-block", + "type": "slider-old-block", "dots": false } }, diff --git a/src/blocks/SliderOld/__tests__/Slider.test.tsx b/src/blocks/SliderOld/__tests__/Slider.test.tsx new file mode 100644 index 000000000..68c8b6dea --- /dev/null +++ b/src/blocks/SliderOld/__tests__/Slider.test.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import {queryHelpers, render} from '@testing-library/react'; + +import {BasicCard} from '../../../sub-blocks'; +import Slider from '../SliderOld'; + +const EXAMPLE_URL = 'https://example.com'; +const SLIDER_TITLE = 'Slider title'; +const CARD_TITLE = 'Card title'; +const CARD_TEXT = 'Lorem ipsum'; +const CARDS_COUNT = 10; + +const slidesToShowValues = [3, 2, 1]; + +describe('Slider', () => { + test.each(slidesToShowValues)('Has correct slider labels', async (slidesToShow) => { + const {container} = render( + <Slider title={{text: SLIDER_TITLE, url: EXAMPLE_URL}} slidesToShow={slidesToShow}> + {Array(CARDS_COUNT) + .fill(null) + .map((_, index) => ( + <BasicCard + url={EXAMPLE_URL} + title={CARD_TITLE} + text={CARD_TEXT} + key={index} + /> + ))} + </Slider>, + ); + + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const barElement = container.querySelector('.pc-SliderOldBlock__bar'); + if (slidesToShow > 1) { + expect(barElement).toBeTruthy(); + } else { + expect(barElement).toBeFalsy(); + } + + const barDotsCount = CARDS_COUNT - slidesToShow + 1; + + // Checking labels for the first slide + // There we have a bar covering `slidesToShow` dots + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const accessibleBarElement = container.querySelector('.pc-SliderOldBlock__accessible-bar'); + expect(accessibleBarElement?.getAttribute('aria-label')).toBe(`Page 1 of ${barDotsCount}`); + expect( + queryHelpers.queryAllByAttribute('aria-label', container, `Page 1 of ${barDotsCount}`), + ).toHaveLength(slidesToShow + 1); + + // Checking labels for the slides starting from 2 + Array(barDotsCount - 1) + .fill(null) + .forEach((_, index) => { + expect( + queryHelpers.queryAllByAttribute( + 'aria-label', + container, + `Page ${index + 2} of ${barDotsCount}`, + ), + ).toHaveLength(1); + }); + }); +}); diff --git a/src/blocks/SliderNew/__tests__/Slider.visual.test.tsx b/src/blocks/SliderOld/__tests__/Slider.visual.test.tsx similarity index 98% rename from src/blocks/SliderNew/__tests__/Slider.visual.test.tsx rename to src/blocks/SliderOld/__tests__/Slider.visual.test.tsx index 95c877379..4f457f5c6 100644 --- a/src/blocks/SliderNew/__tests__/Slider.visual.test.tsx +++ b/src/blocks/SliderOld/__tests__/Slider.visual.test.tsx @@ -12,7 +12,7 @@ import { WithoutDots, } from './helpers'; -test.describe('SliderNew', () => { +test.describe('Slider Old', () => { test('render stories <Default>', async ({mount, expectScreenshot, defaultDelay}) => { await mount(<Default />); await defaultDelay(); diff --git a/src/blocks/SliderNew/__tests__/helpers.tsx b/src/blocks/SliderOld/__tests__/helpers.tsx similarity index 100% rename from src/blocks/SliderNew/__tests__/helpers.tsx rename to src/blocks/SliderOld/__tests__/helpers.tsx diff --git a/src/blocks/SliderNew/i18n/en.json b/src/blocks/SliderOld/i18n/en.json similarity index 65% rename from src/blocks/SliderNew/i18n/en.json rename to src/blocks/SliderOld/i18n/en.json index b4a01d664..c72f11a9e 100644 --- a/src/blocks/SliderNew/i18n/en.json +++ b/src/blocks/SliderOld/i18n/en.json @@ -1,6 +1,6 @@ { "arrow-right": "Next", "arrow-left": "Previous", - "dot-label": "Page {{index}}", + "dot-label": "Page {{index}} of {{count}}", "pagination-label": "Pages" } diff --git a/src/blocks/SliderNew/i18n/index.ts b/src/blocks/SliderOld/i18n/index.ts similarity index 93% rename from src/blocks/SliderNew/i18n/index.ts rename to src/blocks/SliderOld/i18n/index.ts index f5c4c1c03..7678b1586 100644 --- a/src/blocks/SliderNew/i18n/index.ts +++ b/src/blocks/SliderOld/i18n/index.ts @@ -5,4 +5,4 @@ import {NAMESPACE} from '../../../utils/cn'; import en from './en.json'; import ru from './ru.json'; -export const i18n = addComponentKeysets({en, ru}, `${NAMESPACE}SliderNewBlock`); +export const i18n = addComponentKeysets({en, ru}, `${NAMESPACE}SliderOldBlock`); diff --git a/src/blocks/SliderNew/i18n/ru.json b/src/blocks/SliderOld/i18n/ru.json similarity index 64% rename from src/blocks/SliderNew/i18n/ru.json rename to src/blocks/SliderOld/i18n/ru.json index c9547fa2f..fb16cd7bc 100644 --- a/src/blocks/SliderNew/i18n/ru.json +++ b/src/blocks/SliderOld/i18n/ru.json @@ -1,6 +1,6 @@ { "arrow-right": "Дальше", "arrow-left": "Назад", - "dot-label": "Страница {{index}}", + "dot-label": "Страница {{index}} из {{count}}", "pagination-label": "Страницы" } diff --git a/src/blocks/SliderNew/models.ts b/src/blocks/SliderOld/models.ts similarity index 93% rename from src/blocks/SliderNew/models.ts rename to src/blocks/SliderOld/models.ts index f2b47dc0b..bcb40599a 100644 --- a/src/blocks/SliderNew/models.ts +++ b/src/blocks/SliderOld/models.ts @@ -1,8 +1,8 @@ export enum SliderBreakpointNames { - Xs = 'xs', Sm = 'sm', Md = 'md', Lg = 'lg', + Xl = 'xl', } export type SliderBreakpointParams = Record<SliderBreakpointNames, number>; diff --git a/src/blocks/SliderNew/schema.ts b/src/blocks/SliderOld/schema.ts similarity index 86% rename from src/blocks/SliderNew/schema.ts rename to src/blocks/SliderOld/schema.ts index 3f20233a6..e50340e58 100644 --- a/src/blocks/SliderNew/schema.ts +++ b/src/blocks/SliderOld/schema.ts @@ -50,7 +50,7 @@ const DisclaimerProps = { }, }; -export const SliderNewProps = { +export const SliderOldProps = { dots: { type: 'boolean', }, @@ -63,15 +63,6 @@ export const SliderNewProps = { autoplay: { type: 'number', }, - type: { - type: 'string', - }, - adaptive: { - type: 'boolean', - }, - arrowSize: { - type: 'number', - }, animated: AnimatableProps, slidesToShow: sliderSizesObject, disclaimer: DisclaimerProps, @@ -79,14 +70,15 @@ export const SliderNewProps = { children: ChildrenCardsProps, }; -export const SliderNewBlock = { - 'slider-new-block': { +/** @deprecated */ +export const SliderOldBlock = { + 'slider-old-block': { additionalProperties: false, required: [], properties: { ...BlockBaseProps, ...AnimatableProps, - ...SliderNewProps, + ...SliderOldProps, ...BlockHeaderProps, }, }, diff --git a/src/blocks/Slider/slick.scss b/src/blocks/SliderOld/slick.scss similarity index 100% rename from src/blocks/Slider/slick.scss rename to src/blocks/SliderOld/slick.scss diff --git a/src/blocks/SliderOld/utils.ts b/src/blocks/SliderOld/utils.ts new file mode 100644 index 000000000..23c0ec8e6 --- /dev/null +++ b/src/blocks/SliderOld/utils.ts @@ -0,0 +1,174 @@ +import {useEffect, useRef, useState} from 'react'; + +import pickBy from 'lodash/pickBy'; + +import {BREAKPOINTS} from '../../constants'; + +import {SliderBreakpointNames, SliderBreakpointParams, SlidesToShow} from './models'; + +export const DEFAULT_SLIDE_BREAKPOINTS = { + [SliderBreakpointNames.Xl]: 3, + [SliderBreakpointNames.Lg]: 2, + [SliderBreakpointNames.Md]: 2, + [SliderBreakpointNames.Sm]: 1.15, +}; + +const BREAKPOINT_NAMES_BY_VALUES = Object.entries(BREAKPOINTS).reduce< + Record<number, SliderBreakpointNames> +>((acc, [key, value]) => ({...acc, [value]: key as SliderBreakpointNames}), {}); + +export interface GetSlidesToShowParams { + contentLength: number; + breakpoints?: SlidesToShow; + mobileFullscreen?: boolean; +} + +export const isFocusable = (element: Element): boolean => { + if (!(element instanceof HTMLElement)) { + return false; + } + const tabIndexAttr = element.getAttribute('tabindex'); + const hasTabIndex = tabIndexAttr !== null; + const tabIndex = Number(tabIndexAttr); + if (element.ariaHidden === 'true' || (hasTabIndex && tabIndex < 0)) { + return false; + } + if (hasTabIndex && tabIndex >= 0) { + return true; + } + + // without this jest fails here for some reason + let htmlElement: + | HTMLAnchorElement + | HTMLInputElement + | HTMLSelectElement + | HTMLTextAreaElement + | HTMLButtonElement; + switch (true) { + case element instanceof HTMLAnchorElement: + htmlElement = element as HTMLAnchorElement; + return Boolean(htmlElement.href); + case element instanceof HTMLInputElement: + htmlElement = element as HTMLInputElement; + return htmlElement.type !== 'hidden' && !htmlElement.disabled; + case element instanceof HTMLSelectElement: + case element instanceof HTMLTextAreaElement: + case element instanceof HTMLButtonElement: + htmlElement = element as HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement; + return !htmlElement.disabled; + default: + return false; + } +}; + +export function getSlidesToShowWithDefaults({ + contentLength, + breakpoints, + mobileFullscreen, +}: GetSlidesToShowParams) { + let result; + + if (typeof breakpoints === 'number') { + result = Object.keys(DEFAULT_SLIDE_BREAKPOINTS).reduce( + (acc, breakpointName) => ({...acc, [breakpointName]: breakpoints}), + {} as SliderBreakpointParams, + ); + } else { + result = breakpoints || DEFAULT_SLIDE_BREAKPOINTS; + } + + return { + ...DEFAULT_SLIDE_BREAKPOINTS, + ...pickBy(result, (value) => !isNaN(value)), + sm: !mobileFullscreen && contentLength > 1 ? DEFAULT_SLIDE_BREAKPOINTS.sm : 1, + }; +} + +export function getSliderResponsiveParams(breakpoints: SliderBreakpointParams) { + return Object.entries(breakpoints).map(([breakpointName, slidesToShow]) => ({ + breakpoint: BREAKPOINTS[breakpointName as SliderBreakpointNames], + settings: {slidesToShow}, + })); +} + +export function getSlidesCountByBreakpoint( + breakpoint: number, + breakpoints: SliderBreakpointParams, +) { + const breakpointName = BREAKPOINT_NAMES_BY_VALUES[breakpoint]; + + return Math.floor(breakpoints[breakpointName]); +} + +export function getSlidesToShowCount(breakpoints: SliderBreakpointParams) { + return Math.floor(Math.max(...Object.values(breakpoints))); +} + +const getRovingListItemId = (uniqId: string, index: number) => + `${uniqId}-roving-tabindex-item-${index}`; +export function useRovingTabIndex(props: { + itemCount: number; + activeIndex: number; + firstIndex?: number; + uniqId: string; +}) { + const {itemCount, activeIndex, firstIndex = 0, uniqId} = props; + const [currentIndex, setCurrentIndex] = useState(firstIndex); + const hasFocusRef = useRef(false); + const lastIndex = itemCount + firstIndex - 1; + + const getRovingItemProps = ( + index: number, + ): Pick<React.HTMLAttributes<HTMLElement>, 'id' | 'tabIndex' | 'onFocus'> => { + return { + id: getRovingListItemId(uniqId, index), + tabIndex: index === activeIndex ? 0 : -1, + onFocus: () => { + setCurrentIndex(index); + hasFocusRef.current = true; + }, + }; + }; + + useEffect(() => { + if (!hasFocusRef.current) { + return; + } + document.getElementById(getRovingListItemId(uniqId, currentIndex))?.focus(); + }, [activeIndex, currentIndex, uniqId]); + + const setNextIndex = () => + setCurrentIndex((prev) => (prev >= lastIndex ? firstIndex : prev + 1)); + const setPrevIndex = () => + setCurrentIndex((prev) => (prev <= firstIndex ? lastIndex : prev - 1)); + + const onRovingListKeyDown: React.KeyboardEventHandler<HTMLElement> = (e) => { + const key = e.key.toLowerCase(); + + if (key !== 'tab' && key !== 'enter') { + e.preventDefault(); + } + + switch (key) { + case 'arrowleft': + case 'arrowup': + setPrevIndex(); + return; + case 'arrowright': + case 'arrowdown': + setNextIndex(); + return; + } + }; + + const onRovingListBlur: React.FocusEventHandler<HTMLElement> = () => { + hasFocusRef.current = false; + }; + + const rovingListProps: React.HTMLAttributes<HTMLElement> = { + onKeyDown: onRovingListKeyDown, + onBlur: onRovingListBlur, + }; + + return {getRovingItemProps, rovingListProps}; +} diff --git a/src/blocks/index.ts b/src/blocks/index.ts index 6ce472095..709a08b46 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -3,6 +3,7 @@ export {default as CompaniesBlock} from './Companies/Companies'; export {default as InfoBlock} from './Info/Info'; export {default as MediaBlock} from './Media/Media'; export {default as MapBlock} from './Map/Map'; +export {default as SliderOldBlock} from './SliderOld/SliderOld'; export {default as SliderBlock} from './Slider/Slider'; export {default as ExtendedFeaturesBlock} from './ExtendedFeatures/ExtendedFeatures'; export {default as PromoFeaturesBlock} from './PromoFeaturesBlock/PromoFeaturesBlock'; diff --git a/src/blocks/unstable.ts b/src/blocks/unstable.ts deleted file mode 100644 index 2fbd03729..000000000 --- a/src/blocks/unstable.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as SliderNewBlock} from './SliderNew/Slider'; diff --git a/src/blocks/validators.ts b/src/blocks/validators.ts index a1a536817..df42de841 100644 --- a/src/blocks/validators.ts +++ b/src/blocks/validators.ts @@ -10,6 +10,7 @@ export * from './Info/schema'; export * from './Media/schema'; export * from './PromoFeaturesBlock/schema'; export * from './Questions/schema'; +export * from './SliderOld/schema'; export * from './Slider/schema'; export * from './Table/schema'; export * from './Share/schema'; diff --git a/src/components/FullscreenImage/FullscreenImage.scss b/src/components/FullscreenImage/FullscreenImage.scss index 3eb32470c..8a84508c0 100644 --- a/src/components/FullscreenImage/FullscreenImage.scss +++ b/src/components/FullscreenImage/FullscreenImage.scss @@ -4,6 +4,32 @@ $block: '.#{$ns}fullscreen-image'; $closeButtonSize: 36px; +@mixin iconWrapper { + @include reset-button-style(); + @include focusable(); + + display: flex; + align-items: center; + justify-content: center; + width: $closeButtonSize; + height: $closeButtonSize; + border-radius: 8px; + background-color: var(--g-color-base-simple-hover-solid); + cursor: pointer; + opacity: 0; + transition: 0.3s; + + &:focus { + opacity: 1; + } + + @media (max-width: map-get($gridBreakpoints, 'md')) { + & { + opacity: 1; + } + } +} + #{$block} { &__image { cursor: pointer; @@ -12,7 +38,7 @@ $closeButtonSize: 36px; position: relative; &:hover { - #{$block}__icon-wrapper { + & #{$block}__expand-icon-wrapper { opacity: 1; } } @@ -22,6 +48,36 @@ $closeButtonSize: 36px; &__modal-content { position: relative; border-radius: $borderRadius; + + width: 100%; + + &-wrapper { + width: 100%; + } + + &_loaded { + max-width: fit-content; + } + } + + &__modal_with-slider { + & .g-modal__content-wrapper { + width: 100%; + height: 100vh; + margin: 0; + justify-content: center; + } + + #{$block}__modal-content { + background-color: transparent; + + &:hover { + & #{$block}__expand-icon-wrapper, + & #{$block}__close-icon-wrapper { + opacity: 1; + } + } + } } &__modal-image { @@ -32,32 +88,49 @@ $closeButtonSize: 36px; overflow: hidden; } + &__modal-slider { + max-width: 100vw; + width: 100%; + height: 100vh; + + &_item { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + + &-image { + display: block; + margin: auto; + border-radius: $borderRadius; + overflow: hidden; + margin-top: 40px; + max-height: calc(100vh - 120px); + max-width: 100%; + object-fit: contain; + object-position: center; + } + } + } + &__modal .g-modal__content, &__modal-image { border-radius: $borderRadius; } - &__icon-wrapper { - @include reset-button-style(); - @include focusable(); - - display: flex; - align-items: center; - justify-content: center; + &__expand-icon-wrapper { + @include iconWrapper(); position: absolute; - right: $indentXS; - top: $indentXS; - width: $closeButtonSize; - height: $closeButtonSize; - border-radius: 8px; - background-color: var(--g-color-base-simple-hover-solid); - cursor: pointer; - opacity: 0; - transition: 0.3s; + right: 16px; + top: 16px; + } - &:focus { - opacity: 1; - } + &__close-icon-wrapper { + @include iconWrapper(); + z-index: 1001; + position: absolute; + right: 24px; + top: 24px; } &__icon { @@ -68,23 +141,13 @@ $closeButtonSize: 36px; } } - @media (max-width: map-get($gridBreakpoints, 'xl')) { - &__modal-image { - width: 100%; - } - } - - @media (max-width: map-get($gridBreakpoints, 'lg')) { - &__image { - pointer-events: none; - } - - &__icon-wrapper { - display: none; + @media (max-width: map-get($gridBreakpoints, 'md')) { + &__modal .g-modal__content-wrapper { + margin: $fullscreenImageMobilePadding; } - &__modal { - display: none !important; /* stylelint-disable-line declaration-no-important */ + &__modal_with-slider .g-modal__content-wrapper { + margin: 0; } } } diff --git a/src/components/FullscreenImage/FullscreenImage.tsx b/src/components/FullscreenImage/FullscreenImage.tsx index 41442f5c4..65e53c49a 100644 --- a/src/components/FullscreenImage/FullscreenImage.tsx +++ b/src/components/FullscreenImage/FullscreenImage.tsx @@ -1,10 +1,13 @@ -import React, {CSSProperties, HTMLProps, useState} from 'react'; +import React, {CSSProperties, HTMLProps, useEffect, useState} from 'react'; import {ChevronsExpandUpRight, Xmark} from '@gravity-ui/icons'; import {Icon, Modal} from '@gravity-ui/uikit'; +import {SliderBlock} from '../../blocks'; +import {ImageProps as ModelImageProps, SliderType} from '../../models'; import {block} from '../../utils'; import Image, {ImageProps} from '../Image/Image'; +import {getMediaImage} from '../Media/Image/utils'; import {i18n} from './i18n'; @@ -15,6 +18,7 @@ export interface FullscreenImageProps extends ImageProps { modalImageClass?: string; imageStyle?: CSSProperties; extraProps?: HTMLProps<HTMLDivElement>; + sliderData?: {items: ModelImageProps[]; initialIndex: number}; } const b = block('fullscreen-image'); @@ -22,12 +26,30 @@ const FULL_SCREEN_ICON_SIZE = 18; const CLOSE_ICON_SIZE = 24; const FullscreenImage = (props: FullscreenImageProps) => { - const {imageClassName, modalImageClass, imageStyle, alt = i18n('img-alt'), extraProps} = props; + const { + imageClassName, + sliderData, + modalImageClass, + imageStyle, + alt = i18n('img-alt'), + extraProps, + } = props; const [isOpened, setIsOpened] = useState(false); + const [sliderLoaded, setSliderLoaded] = useState(false); const openModal = () => setIsOpened(true); const closeModal = () => setIsOpened(false); + useEffect(() => { + if (sliderData && !isOpened) { + setSliderLoaded(false); + } + }, [isOpened, sliderData]); + + const handleSliderImageLoad = () => { + setSliderLoaded(true); + }; + return ( <div className={b()} {...extraProps}> <div className={b('image-wrapper')}> @@ -38,7 +60,7 @@ const FullscreenImage = (props: FullscreenImageProps) => { onClick={openModal} style={imageStyle} /> - <button className={b('icon-wrapper')} onClick={openModal}> + <button className={b('expand-icon-wrapper')} onClick={openModal}> <Icon data={ChevronsExpandUpRight} width={FULL_SCREEN_ICON_SIZE} @@ -51,11 +73,11 @@ const FullscreenImage = (props: FullscreenImageProps) => { <Modal open={isOpened} onClose={closeModal} - className={b('modal')} - contentClassName={b('modal-content')} + className={b('modal', {'with-slider': Boolean(sliderData)})} + contentClassName={b('modal-content', {loaded: sliderLoaded})} > <button - className={b('icon-wrapper', {visible: true})} + className={b('close-icon-wrapper', {visible: true})} onClick={closeModal} aria-label={i18n('close')} > @@ -66,7 +88,33 @@ const FullscreenImage = (props: FullscreenImageProps) => { className={b('icon', {hover: true})} /> </button> - <Image {...props} className={b('modal-image', modalImageClass)} /> + {sliderData ? ( + <div className={b('modal-slider')}> + <SliderBlock + initialSlide={sliderData.initialIndex} + slidesToShow={1} + type={SliderType.FullscreenCard} + > + {sliderData.items.map((item, index) => ( + <div key={index} className={b('modal-slider_item')}> + <Image + onLoad={handleSliderImageLoad} + className={b( + 'modal-slider_item-image', + modalImageClass, + )} + containerClassName={b( + 'modal-slider_item-image-wrapper', + )} + {...getMediaImage(item)} + /> + </div> + ))} + </SliderBlock> + </div> + ) : ( + <Image {...props} className={b('modal-image', modalImageClass)} /> + )} </Modal> )} </div> diff --git a/src/components/Media/Image/Image.tsx b/src/components/Media/Image/Image.tsx index 1ecce074f..95c89ed56 100644 --- a/src/components/Media/Image/Image.tsx +++ b/src/components/Media/Image/Image.tsx @@ -3,11 +3,11 @@ import React, {Fragment, useEffect, useState} from 'react'; import {Interpolation, animated, config, useSpring} from '@react-spring/web'; import debounce from 'lodash/debounce'; -import SliderBlock from '../../../blocks/Slider/Slider'; +import {SliderBlock} from '../../../blocks'; import {ImageProps, MediaComponentImageProps, QAProps, SliderType} from '../../../models'; import {block, getQaAttrubutes} from '../../../utils'; import BackgroundImage from '../../BackgroundImage/BackgroundImage'; -import FullscreenImage from '../../FullscreenImage/FullscreenImage'; +import FullscreenImage, {FullscreenImageProps} from '../../FullscreenImage/FullscreenImage'; import ImageView from '../../Image/Image'; import {getMediaImage} from './utils'; @@ -20,6 +20,7 @@ export interface ImageAdditionProps { imageClassName?: string; isBackground?: boolean; fullscreen?: boolean; + fullscreenClassName?: string; onLoad?: () => void; } @@ -36,6 +37,7 @@ const Image = (props: ImageAllProps) => { parallax, height, imageClassName, + fullscreenClassName, isBackground, hasVideoFallback, video, @@ -87,7 +89,10 @@ const Image = (props: ImageAllProps) => { const imageClass = b('item', {withVideo: Boolean(video) && !hasVideoFallback}, imageClassName); - const renderFullscreenImage = (item: ImageProps) => { + const renderFullscreenImage = ( + item: ImageProps, + sliderData?: FullscreenImageProps['sliderData'], + ) => { const itemData = getMediaImage(item); return ( @@ -95,8 +100,10 @@ const Image = (props: ImageAllProps) => { key={itemData.alt} {...itemData} imageClassName={imageClass} + modalImageClass={fullscreenClassName} imageStyle={{height}} qa={qaAttributes.fullscreenImage} + sliderData={sliderData} /> ); }; @@ -135,7 +142,9 @@ const Image = (props: ImageAllProps) => { <SliderBlock slidesToShow={1} type={SliderType.MediaCard}> {imageArray.map((item, index) => ( <Fragment key={index}> - {fullscreenItem ? renderFullscreenImage(item) : imageOnly(item)} + {fullscreenItem + ? renderFullscreenImage(item, {items: imageArray, initialIndex: index}) + : imageOnly(item)} </Fragment> ))} </SliderBlock> diff --git a/src/components/Media/Media.scss b/src/components/Media/Media.scss index af4b8fec0..5d71bc38b 100644 --- a/src/components/Media/Media.scss +++ b/src/components/Media/Media.scss @@ -13,4 +13,9 @@ $block: '.#{$ns}Media'; display: flex; align-items: center; } + + &__fullscreen-image-cover { + object-fit: cover; + object-position: top; + } } diff --git a/src/components/Media/Media.tsx b/src/components/Media/Media.tsx index 1a3fa02e4..9dd31affa 100644 --- a/src/components/Media/Media.tsx +++ b/src/components/Media/Media.tsx @@ -18,6 +18,7 @@ const b = block('Media'); export interface MediaAllProps extends MediaProps, VideoAdditionProps, ImageAdditionProps, QAProps { className?: string; + isFullscreenImageCover?: boolean; youtubeClassName?: string; autoplay?: boolean; onImageLoad?: () => void; @@ -35,6 +36,7 @@ export const Media = (props: MediaAllProps) => { previewImg, parallax = false, fullscreen, + isFullscreenImageCover, analyticsEvents, className, imageClassName, @@ -71,6 +73,9 @@ export const Media = (props: MediaAllProps) => { disableImageSliderForArrayInput={disableImageSliderForArrayInput} height={height} imageClassName={imageClassName} + fullscreenClassName={ + isFullscreenImageCover ? b('fullscreen-image-cover') : undefined + } isBackground={isBackground} video={video} hasVideoFallback={hasVideoFallback} @@ -144,6 +149,7 @@ export const Media = (props: MediaAllProps) => { isBackground, hasVideoFallback, fullscreen, + isFullscreenImageCover, qaAttributes.image, qaAttributes.video, onImageLoad, diff --git a/src/constructor-items.ts b/src/constructor-items.ts index 3bb5c4a7d..4def4f18f 100644 --- a/src/constructor-items.ts +++ b/src/constructor-items.ts @@ -16,10 +16,10 @@ import { QuestionsBlock, ShareBlock, SliderBlock, + SliderOldBlock, TableBlock, TabsBlock, } from './blocks'; -import {SliderNewBlock} from './blocks/unstable'; import {BlockType, NavigationItemType, SubBlockType} from './models'; import { GithubButton, @@ -43,7 +43,7 @@ import { } from './sub-blocks'; export const blockMap = { - [BlockType.SliderBlock]: SliderBlock, + [BlockType.SliderOldBlock]: SliderOldBlock, [BlockType.ExtendedFeaturesBlock]: ExtendedFeaturesBlock, [BlockType.PromoFeaturesBlock]: PromoFeaturesBlock, [BlockType.QuestionsBlock]: QuestionsBlock, @@ -62,8 +62,7 @@ export const blockMap = { [BlockType.MapBlock]: MapBlock, [BlockType.FilterBlock]: FilterBlock, [BlockType.FormBlock]: FormBlock, - // unstable - [BlockType.SliderNewBlock]: SliderNewBlock, + [BlockType.SliderBlock]: SliderBlock, }; export const subBlockMap = { diff --git a/src/editor/data/templates/slider-new-block.json b/src/editor/data/templates/slider-old-block.json similarity index 98% rename from src/editor/data/templates/slider-new-block.json rename to src/editor/data/templates/slider-old-block.json index d3670d16f..f337b8a5e 100644 --- a/src/editor/data/templates/slider-new-block.json +++ b/src/editor/data/templates/slider-old-block.json @@ -1,8 +1,8 @@ { "template": { - "type": "slider-new-block", + "type": "slider-old-block", "title": { - "text": "Slider new", + "text": "Slider", "url": "https://example.com" }, "children": [ diff --git a/src/models/constructor-items/blocks.ts b/src/models/constructor-items/blocks.ts index 715d0f235..e4fc200bc 100644 --- a/src/models/constructor-items/blocks.ts +++ b/src/models/constructor-items/blocks.ts @@ -44,6 +44,8 @@ import {BannerCardProps, HubspotFormProps, SubBlock, SubBlockModels} from './sub export enum BlockType { PromoFeaturesBlock = 'promo-features-block', ExtendedFeaturesBlock = 'extended-features-block', + /** @deprecated */ + SliderOldBlock = 'slider-old-block', SliderBlock = 'slider-block', QuestionsBlock = 'questions-block', BannerBlock = 'banner-block', @@ -61,8 +63,6 @@ export enum BlockType { MapBlock = 'map-block', FilterBlock = 'filter-block', FormBlock = 'form-block', - // unstable - SliderNewBlock = 'slider-new-block', } export const BlockTypes = Object.values(BlockType); @@ -108,12 +108,13 @@ export enum SliderBreakpointNames { export enum SliderType { MediaCard = 'media-card', HeaderCard = 'header-card', + FullscreenCard = 'fullscreen-card', } export type SliderBreakpointParams = Record<SliderBreakpointNames, number>; export type SlidesToShow = Partial<SliderBreakpointParams> | number; -export interface SliderProps extends Childable, Animatable, LoadableChildren { +export interface SliderOldProps extends Childable, Animatable, LoadableChildren { dots?: boolean; arrows?: boolean; slidesToShow?: SlidesToShow; @@ -129,7 +130,7 @@ export interface SliderProps extends Childable, Animatable, LoadableChildren { adaptive?: boolean; } -export interface SliderNewProps extends Childable, Animatable, LoadableChildren { +export interface SliderProps extends Childable, Animatable, LoadableChildren { dots?: boolean; arrows?: boolean; slidesToShow?: SlidesToShow; @@ -142,9 +143,10 @@ export interface SliderNewProps extends Childable, Animatable, LoadableChildren autoplay?: number; //for server transforms randomOrder?: boolean; + adaptive?: boolean; } -export interface HeaderSliderBlockProps extends Omit<SliderProps, 'title' | 'description'> { +export interface HeaderSliderBlockProps extends Omit<SliderOldProps, 'title' | 'description'> { items: HeaderBlockProps[]; } @@ -457,9 +459,9 @@ export type HeaderBlockModel = { type: BlockType.HeaderBlock; } & HeaderBlockProps; -export type SliderBlockModel = { - type: BlockType.SliderBlock; -} & SliderProps; +export type SliderOldBlockModel = { + type: BlockType.SliderOldBlock; +} & SliderOldProps; export type ExtendedFeaturesBlockModel = { type: BlockType.ExtendedFeaturesBlock; @@ -529,12 +531,12 @@ export type FormBlockModel = { type: BlockType.FormBlock; } & FormBlockProps; -// unstable block models -export type SliderNewBlockModel = { - type: BlockType.SliderNewBlock; -} & SliderNewProps; +export type SliderBlockModel = { + type: BlockType.SliderBlock; +} & SliderProps; type BlockModels = + | SliderOldBlockModel | SliderBlockModel | ExtendedFeaturesBlockModel | PromoFeaturesBlockModel @@ -555,6 +557,4 @@ type BlockModels = | FilterBlockModel | FormBlockModel; -type UnstableBlockModels = SliderNewBlockModel; - -export type Block = (BlockModels | UnstableBlockModels) & BlockBaseProps; +export type Block = BlockModels & BlockBaseProps; diff --git a/src/schema/constants.ts b/src/schema/constants.ts index 63d4b62a5..94450cabf 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -19,7 +19,7 @@ import { QuestionsBlock, ShareBlock, SliderBlock, - SliderNewBlock, + SliderOldBlock, TableBlock, TabsBlock, } from './validators/blocks'; @@ -38,7 +38,7 @@ export const blockSchemas: Record<BlockType, object> = { ...Divider, ...ExtendedFeaturesBlock, ...PromoFeaturesBlock, - ...SliderBlock, + ...SliderOldBlock, ...QuestionsBlock, ...HeaderBlock, ...BannerBlock, @@ -55,7 +55,7 @@ export const blockSchemas: Record<BlockType, object> = { ...ShareBlock, ...FilterBlock, ...FormBlock, - ...SliderNewBlock, + ...SliderBlock, }; export const cardSchemas = { @@ -76,6 +76,8 @@ export const constructorBlockSchemaNames = [ 'post', 'extended-features-block', 'promo-features-block', + /** @deprecated */ + 'slider-old-block', 'slider-block', 'questions-block', 'header-block', diff --git a/src/schema/validators/blocks.ts b/src/schema/validators/blocks.ts index ca30f4929..f48b1cd11 100644 --- a/src/schema/validators/blocks.ts +++ b/src/schema/validators/blocks.ts @@ -7,7 +7,7 @@ export * from '../../blocks/Info/schema'; export * from '../../blocks/Media/schema'; export * from '../../blocks/Map/schema'; export * from '../../blocks/Questions/schema'; -export * from '../../blocks/Slider/schema'; +export * from '../../blocks/SliderOld/schema'; export * from '../../blocks/Table/schema'; export * from '../../blocks/Tabs/schema'; export * from '../../blocks/HeaderSlider/schema'; @@ -17,4 +17,4 @@ export * from '../../blocks/ContentLayout/schema'; export * from '../../blocks/Share/schema'; export * from '../../blocks/FilterBlock/schema'; export * from '../../blocks/Form/schema'; -export * from '../../blocks/SliderNew/schema'; +export * from '../../blocks/Slider/schema'; diff --git a/src/sub-blocks/MediaCard/__tests__/MediaCard.visual.test.tsx b/src/sub-blocks/MediaCard/__tests__/MediaCard.visual.test.tsx index 467e49123..3fa6e08db 100644 --- a/src/sub-blocks/MediaCard/__tests__/MediaCard.visual.test.tsx +++ b/src/sub-blocks/MediaCard/__tests__/MediaCard.visual.test.tsx @@ -21,7 +21,7 @@ test.describe('MediaCard', () => { await expectScreenshot({skipTheme: 'dark'}); }); - test('render stories <ImageSlider>', async ({mount, expectScreenshot, delay}) => { + test.skip('render stories <ImageSlider>', async ({mount, expectScreenshot, delay}) => { await mount(<ImageSlider />); await delay(DEFAULT_MEDIACARD_DELAY); await expectScreenshot({skipTheme: 'dark'}); diff --git a/src/text-transform/config.ts b/src/text-transform/config.ts index 3b9e7e720..bbb69a8a3 100644 --- a/src/text-transform/config.ts +++ b/src/text-transform/config.ts @@ -229,7 +229,7 @@ export const config: BlocksConfig = { parser: parsePromoFeatures, }, ], - [BlockType.SliderBlock]: blockHeaderTransformer, + [BlockType.SliderOldBlock]: blockHeaderTransformer, [BlockType.CompaniesBlock]: blockHeaderTransformer, [BlockType.QuestionsBlock]: [ { diff --git a/src/widget/styles.scss b/src/widget/styles.scss index 7a17f9ab7..7745dd8f9 100644 --- a/src/widget/styles.scss +++ b/src/widget/styles.scss @@ -1 +1,2 @@ @import '../../styles/styles.scss'; +@import '../../styles/fonts.scss'; diff --git a/styles/fonts.scss b/styles/fonts.scss new file mode 100644 index 000000000..637a6e1cd --- /dev/null +++ b/styles/fonts.scss @@ -0,0 +1 @@ +@import '@gravity-ui/uikit/styles/fonts.css'; diff --git a/styles/styles.scss b/styles/styles.scss index 5c2a8d3e0..2b3d63c5b 100644 --- a/styles/styles.scss +++ b/styles/styles.scss @@ -1,4 +1,3 @@ -@import '@gravity-ui/uikit/styles/fonts.css'; @import '~@gravity-ui/uikit/styles/styles.css'; @import './mixins.scss'; @import './variables.scss'; diff --git a/styles/variables.scss b/styles/variables.scss index c8682cf53..d4882bc14 100644 --- a/styles/variables.scss +++ b/styles/variables.scss @@ -57,3 +57,4 @@ $gridGutterMobile: 8px; $contentWidth: calc(#{$newContentWidth} + #{$gridGutter} * 2 + #{$gridContainerPadding} * 2); $gridContainerMargin: 16px; +$fullscreenImageMobilePadding: 8px;