@gravity-ui/page-constructor 5.27.0 → 5.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/build/cjs/blocks/Slider/Slider.css +1 -1
  2. package/build/cjs/blocks/Slider/Slider.js +83 -29
  3. package/build/cjs/blocks/Slider/i18n/en.json +3 -1
  4. package/build/cjs/blocks/Slider/i18n/index.d.ts +1 -1
  5. package/build/cjs/blocks/Slider/i18n/ru.json +3 -1
  6. package/build/cjs/blocks/Slider/utils.d.ts +10 -0
  7. package/build/cjs/blocks/Slider/utils.js +85 -1
  8. package/build/cjs/blocks/SliderNew/Arrow/Arrow.d.ts +3 -1
  9. package/build/cjs/blocks/SliderNew/Arrow/Arrow.js +2 -2
  10. package/build/cjs/blocks/SliderNew/Slider.js +20 -8
  11. package/build/cjs/blocks/SliderNew/i18n/en.json +3 -1
  12. package/build/cjs/blocks/SliderNew/i18n/index.d.ts +1 -1
  13. package/build/cjs/blocks/SliderNew/i18n/ru.json +3 -1
  14. package/build/cjs/blocks/SliderNew/useSlider.d.ts +8 -6
  15. package/build/cjs/blocks/SliderNew/useSlider.js +4 -2
  16. package/build/cjs/blocks/SliderNew/useSliderPagination.d.ts +9 -0
  17. package/build/cjs/blocks/SliderNew/useSliderPagination.js +36 -0
  18. package/build/cjs/blocks/SliderNew/utils.d.ts +2 -0
  19. package/build/cjs/blocks/SliderNew/utils.js +13 -1
  20. package/build/esm/blocks/Slider/Slider.css +1 -1
  21. package/build/esm/blocks/Slider/Slider.js +84 -30
  22. package/build/esm/blocks/Slider/i18n/en.json +3 -1
  23. package/build/esm/blocks/Slider/i18n/index.d.ts +1 -1
  24. package/build/esm/blocks/Slider/i18n/ru.json +3 -1
  25. package/build/esm/blocks/Slider/utils.d.ts +10 -0
  26. package/build/esm/blocks/Slider/utils.js +82 -0
  27. package/build/esm/blocks/SliderNew/Arrow/Arrow.d.ts +3 -1
  28. package/build/esm/blocks/SliderNew/Arrow/Arrow.js +2 -2
  29. package/build/esm/blocks/SliderNew/Slider.js +20 -8
  30. package/build/esm/blocks/SliderNew/i18n/en.json +3 -1
  31. package/build/esm/blocks/SliderNew/i18n/index.d.ts +1 -1
  32. package/build/esm/blocks/SliderNew/i18n/ru.json +3 -1
  33. package/build/esm/blocks/SliderNew/useSlider.d.ts +8 -6
  34. package/build/esm/blocks/SliderNew/useSlider.js +6 -3
  35. package/build/esm/blocks/SliderNew/useSliderPagination.d.ts +9 -0
  36. package/build/esm/blocks/SliderNew/useSliderPagination.js +32 -0
  37. package/build/esm/blocks/SliderNew/utils.d.ts +2 -0
  38. package/build/esm/blocks/SliderNew/utils.js +10 -0
  39. package/package.json +2 -1
  40. package/widget/index.js +1 -1
@@ -105,7 +105,7 @@ unpredictable css rules order in build */
105
105
  height: auto;
106
106
  }
107
107
  .pc-SliderBlock .slick-track .slick-slide > div {
108
- height: 100%;
108
+ display: flex;
109
109
  }
110
110
  .pc-SliderBlock .slick-arrow {
111
111
  position: absolute;
@@ -1,4 +1,5 @@
1
1
  import React, { Fragment, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
2
+ import { useUniqId } from '@gravity-ui/uikit';
2
3
  import debounce from 'lodash/debounce';
3
4
  import get from 'lodash/get';
4
5
  import noop from 'lodash/noop';
@@ -15,7 +16,8 @@ import useFocus from '../../hooks/useFocus';
15
16
  import { SliderType, } from '../../models';
16
17
  import { block } from '../../utils';
17
18
  import Arrow from './Arrow/Arrow';
18
- import { getSliderResponsiveParams, getSlidesCountByBreakpoint, getSlidesToShowCount, getSlidesToShowWithDefaults, } from './utils';
19
+ import { i18n } from './i18n';
20
+ import { getSliderResponsiveParams, getSlidesCountByBreakpoint, getSlidesToShowCount, getSlidesToShowWithDefaults, isFocusable, useRovingTabIndex, } from './utils';
19
21
  import './Slider.css';
20
22
  const b = block('SliderBlock');
21
23
  const slick = block('slick-origin');
@@ -23,12 +25,15 @@ const DOT_WIDTH = 8;
23
25
  const DOT_GAP = 16;
24
26
  export const SliderBlock = (props) => {
25
27
  var _a;
26
- const { animated, title, description, type, anchorId, arrows = true, adaptive, autoplay = undefined, dots = true, dotsClassName, disclaimer, children, className, blockClassName, lazyLoad, arrowSize, onAfterChange: handleAfterChange, onBeforeChange: handleBeforeChange, } = props;
28
+ const { animated, title, description, type, anchorId, arrows = true, adaptive, autoplay: autoplaySpeed, dots = true, dotsClassName, disclaimer, children, className, blockClassName, lazyLoad, arrowSize, onAfterChange: handleAfterChange, onBeforeChange: handleBeforeChange, } = props;
27
29
  const { isServer } = useContext(SSRContext);
28
30
  const isMobile = useContext(MobileContext);
29
31
  const [breakpoint, setBreakpoint] = useState(BREAKPOINTS.xl);
30
- const disclosedChildren = useMemo(() => discloseAllNestedChildren(children), [children]);
32
+ const sliderId = useUniqId();
33
+ const disclosedChildren = useMemo(() => discloseAllNestedChildren(children, sliderId), [children, sliderId]);
31
34
  const childrenCount = disclosedChildren.length;
35
+ const isAutoplayEnabled = autoplaySpeed !== undefined && autoplaySpeed > 0;
36
+ const isUserInteractionRef = useRef(false);
32
37
  const [slidesToShow] = useState(getSlidesToShowWithDefaults({
33
38
  contentLength: childrenCount,
34
39
  breakpoints: props.slidesToShow,
@@ -39,8 +44,13 @@ export const SliderBlock = (props) => {
39
44
  const [currentIndex, setCurrentIndex] = useState(0);
40
45
  const [childStyles, setChildStyles] = useState({});
41
46
  const [slider, setSlider] = useState();
47
+ const prevIndexRef = useRef(0);
42
48
  const autoplayTimeId = useRef();
43
49
  const { hasFocus, unsetFocus } = useFocus((_a = slider === null || slider === void 0 ? void 0 : slider.innerSlider) === null || _a === void 0 ? void 0 : _a.list);
50
+ const asUserInteraction = (fn) => (...args) => {
51
+ isUserInteractionRef.current = true;
52
+ return fn(...args);
53
+ };
44
54
  // eslint-disable-next-line react-hooks/exhaustive-deps
45
55
  const onResize = useCallback(debounce(() => {
46
56
  if (!slider) {
@@ -55,7 +65,7 @@ export const SliderBlock = (props) => {
55
65
  }, 100), [slider, breakpoint]);
56
66
  const scrollLastSlide = useCallback((current) => {
57
67
  const lastSlide = childrenCount - slidesToShowCount;
58
- if (autoplay && lastSlide === current) {
68
+ if (isAutoplayEnabled && lastSlide === current) {
59
69
  // Slick doesn't support autoplay with no infinity scroll
60
70
  autoplayTimeId.current = setTimeout(() => {
61
71
  if (slider) {
@@ -67,9 +77,9 @@ export const SliderBlock = (props) => {
67
77
  slider.slickPlay();
68
78
  }
69
79
  }, 500);
70
- }, autoplay);
80
+ }, autoplaySpeed);
71
81
  }
72
- }, [autoplay, childrenCount, slider, slidesToShowCount]);
82
+ }, [autoplaySpeed, childrenCount, isAutoplayEnabled, slider, slidesToShowCount]);
73
83
  useEffect(() => {
74
84
  if (hasFocus && autoplayTimeId.current) {
75
85
  clearTimeout(autoplayTimeId.current);
@@ -83,7 +93,7 @@ export const SliderBlock = (props) => {
83
93
  window.addEventListener('resize', onResize, { passive: true });
84
94
  return () => window.removeEventListener('resize', onResize);
85
95
  }, [onResize]);
86
- const handleArrowClick = useCallback((direction) => {
96
+ const handleArrowClick = (direction) => {
87
97
  let nextIndex;
88
98
  if (direction === 'right') {
89
99
  nextIndex =
@@ -96,11 +106,12 @@ export const SliderBlock = (props) => {
96
106
  if (slider) {
97
107
  slider.slickGoTo(nextIndex);
98
108
  }
99
- }, [childrenCount, currentIndex, slider, slidesCountByBreakpoint]);
109
+ };
100
110
  const onBeforeChange = useCallback((current, next) => {
101
111
  if (handleBeforeChange) {
102
112
  handleBeforeChange(current, next);
103
113
  }
114
+ prevIndexRef.current = current;
104
115
  setCurrentIndex(Math.ceil(next));
105
116
  }, [handleBeforeChange]);
106
117
  const onAfterChange = useCallback((current) => {
@@ -113,16 +124,33 @@ export const SliderBlock = (props) => {
113
124
  if (!hasFocus) {
114
125
  scrollLastSlide(current);
115
126
  }
116
- }, [handleAfterChange, hasFocus, scrollLastSlide]);
117
- const handleDotClick = useCallback((index) => {
127
+ if (isUserInteractionRef.current) {
128
+ const focusIndex = prevIndexRef.current >= current
129
+ ? current
130
+ : Math.max(current, prevIndexRef.current + slidesCountByBreakpoint);
131
+ const firstNewSlide = document.getElementById(getSlideId(sliderId, focusIndex));
132
+ if (firstNewSlide) {
133
+ const focusableChild = Array.from(firstNewSlide.querySelectorAll('*')).find(isFocusable);
134
+ focusableChild === null || focusableChild === void 0 ? void 0 : focusableChild.focus();
135
+ }
136
+ }
137
+ isUserInteractionRef.current = false;
138
+ }, [handleAfterChange, hasFocus, scrollLastSlide, sliderId, slidesCountByBreakpoint]);
139
+ const handleDotClick = (index) => {
118
140
  const nextIndex = index > currentIndex ? index + 1 - slidesCountByBreakpoint : index;
119
141
  if (slider) {
120
142
  slider.slickGoTo(nextIndex);
121
143
  }
122
- }, [slider, currentIndex, slidesCountByBreakpoint]);
123
- const barSlidesCount = childrenCount - slidesToShowCount + 1;
144
+ };
145
+ const barSlidesCount = childrenCount - slidesCountByBreakpoint + 1;
124
146
  const barPosition = (DOT_GAP + DOT_WIDTH) * currentIndex;
125
147
  const barWidth = DOT_WIDTH + (DOT_GAP + DOT_WIDTH) * (slidesCountByBreakpoint - 1);
148
+ const { getRovingItemProps, rovingListProps } = useRovingTabIndex({
149
+ itemCount: barSlidesCount,
150
+ activeIndex: currentIndex + 1,
151
+ firstIndex: 1,
152
+ uniqId: sliderId,
153
+ });
126
154
  const renderBar = () => {
127
155
  return (slidesCountByBreakpoint > 1 && (React.createElement("li", { className: b('bar'), style: {
128
156
  left: barPosition,
@@ -133,19 +161,22 @@ export const SliderBlock = (props) => {
133
161
  const renderAccessibleBar = (index) => {
134
162
  return (
135
163
  // To have this key differ from keys used in renderDot function, added `-accessible-bar` part
136
- React.createElement(Fragment, { key: `${index}-accessible-bar` }, slidesCountByBreakpoint > 0 && (React.createElement("li", { className: b('accessible-bar'), "aria-current": true, "aria-label": `Slide ${currentIndex + 1} of ${barSlidesCount}`, style: {
164
+ React.createElement(Fragment, { key: `${index}-accessible-bar` }, slidesCountByBreakpoint > 0 && (React.createElement("li", Object.assign({ className: b('accessible-bar'), role: "menuitemradio", "aria-checked": true, "aria-label": i18n('dot-label', {
165
+ index: currentIndex + 1,
166
+ count: barSlidesCount,
167
+ }), style: {
137
168
  left: barPosition,
138
169
  width: barWidth,
139
- } }))));
170
+ } }, getRovingItemProps(currentIndex + 1))))));
140
171
  };
141
172
  const getCurrentSlideNumber = (index) => {
142
173
  const currentIndexDiff = index - currentIndex;
143
174
  let currentSlideNumber;
144
- if (0 <= currentIndexDiff && currentIndexDiff < slidesToShowCount) {
175
+ if (0 <= currentIndexDiff && currentIndexDiff < slidesCountByBreakpoint) {
145
176
  currentSlideNumber = currentIndex + 1;
146
177
  }
147
- else if (currentIndexDiff >= slidesToShowCount) {
148
- currentSlideNumber = index - slidesToShowCount + 2;
178
+ else if (currentIndexDiff >= slidesCountByBreakpoint) {
179
+ currentSlideNumber = index - slidesCountByBreakpoint + 2;
149
180
  }
150
181
  else {
151
182
  currentSlideNumber = index + 1;
@@ -154,12 +185,24 @@ export const SliderBlock = (props) => {
154
185
  };
155
186
  const isVisibleSlide = (index) => {
156
187
  const currentIndexDiff = index - currentIndex;
157
- return (slidesCountByBreakpoint > 0 &&
188
+ const result = slidesCountByBreakpoint > 0 &&
158
189
  0 <= currentIndexDiff &&
159
- currentIndexDiff < slidesToShowCount);
190
+ currentIndexDiff < slidesCountByBreakpoint;
191
+ return result;
160
192
  };
161
193
  const renderDot = (index) => {
162
- return (React.createElement("li", { key: index, className: b('dot', { active: index === currentIndex }), onClick: () => handleDotClick(index), "aria-hidden": isVisibleSlide(index) ? true : undefined, "aria-label": `Slide ${getCurrentSlideNumber(index)} of ${barSlidesCount}` }));
194
+ const isVisible = isVisibleSlide(index);
195
+ const currentSlideNumber = getCurrentSlideNumber(index);
196
+ const rovingItemProps = isVisible ? undefined : getRovingItemProps(currentSlideNumber);
197
+ return (React.createElement("li", Object.assign({ key: index, className: b('dot', { active: index === currentIndex }), onClick: asUserInteraction(() => handleDotClick(index)), onKeyDown: (e) => {
198
+ const key = e.key.toLowerCase();
199
+ if (key === 'space' || key === 'enter') {
200
+ e.currentTarget.click();
201
+ }
202
+ }, role: "menuitemradio", "aria-checked": false, tabIndex: -1, "aria-hidden": isVisible, "aria-label": i18n('dot-label', {
203
+ index: currentSlideNumber,
204
+ count: barSlidesCount,
205
+ }) }, rovingItemProps)));
163
206
  };
164
207
  const renderNavigation = () => {
165
208
  if (childrenCount <= slidesCountByBreakpoint || !dots || childrenCount === 1) {
@@ -170,7 +213,7 @@ export const SliderBlock = (props) => {
170
213
  .map((_item, index) => renderDot(index));
171
214
  dotsList.splice(currentIndex, 0, renderAccessibleBar(currentIndex));
172
215
  return (React.createElement("div", { className: b('dots', dotsClassName) },
173
- React.createElement("ul", { className: b('dots-list') },
216
+ React.createElement("ul", Object.assign({ className: b('dots-list'), role: "menu", "aria-label": i18n('pagination-label') }, rovingListProps),
174
217
  renderBar(),
175
218
  dotsList)));
176
219
  };
@@ -189,17 +232,18 @@ export const SliderBlock = (props) => {
189
232
  infinite: false,
190
233
  speed: 1000,
191
234
  adaptiveHeight: adaptive,
192
- autoplay: Boolean(autoplay),
193
- autoplaySpeed: autoplay,
235
+ autoplay: isAutoplayEnabled,
236
+ autoplaySpeed,
194
237
  slidesToShow: slidesToShowCount,
195
238
  slidesToScroll: 1,
196
239
  responsive: getSliderResponsiveParams(slidesToShow),
197
240
  beforeChange: onBeforeChange,
198
241
  afterChange: onAfterChange,
199
242
  initialSlide: 0,
200
- nextArrow: React.createElement(Arrow, { type: "right", handleClick: handleArrowClick, size: arrowSize }),
201
- prevArrow: React.createElement(Arrow, { type: "left", handleClick: handleArrowClick, size: arrowSize }),
243
+ nextArrow: (React.createElement(Arrow, { type: "right", handleClick: asUserInteraction(handleArrowClick), size: arrowSize })),
244
+ prevArrow: (React.createElement(Arrow, { type: "left", handleClick: asUserInteraction(handleArrowClick), size: arrowSize })),
202
245
  lazyLoad,
246
+ accessibility: false,
203
247
  };
204
248
  return (React.createElement(OutsideClick, { onOutsideClick: isMobile ? unsetFocus : noop },
205
249
  React.createElement(SlickSlider, Object.assign({}, settings), disclosedChildren),
@@ -219,23 +263,33 @@ export const SliderBlock = (props) => {
219
263
  React.createElement(Title, { title: title, subtitle: description, className: b('header', { 'no-description': !description }) }),
220
264
  React.createElement(AnimateBlock, { className: b('animate-slides'), animate: animated }, renderSlider()))));
221
265
  };
266
+ function getSlideId(sliderId, index) {
267
+ return `slider-${sliderId}-child-${index}`;
268
+ }
222
269
  // TODO remove this and rework PriceDetailed CLOUDFRONT-12230
223
- function discloseAllNestedChildren(children) {
270
+ function discloseAllNestedChildren(children, sliderId) {
224
271
  if (!children) {
225
272
  return [];
226
273
  }
274
+ let childIndex = 0;
275
+ const wrapped = (child) => {
276
+ const id = getSlideId(sliderId, childIndex++);
277
+ return (React.createElement("div", { key: id, id: id }, child));
278
+ };
227
279
  return React.Children.map(children, (child) => {
228
280
  var _a;
229
281
  if (child) {
230
282
  // TODO: if child has 'items' then 'items' determinate like nested children for Slider.
231
283
  const nestedChildren = (_a = child.props.data) === null || _a === void 0 ? void 0 : _a.items;
232
284
  if (nestedChildren) {
233
- return nestedChildren.map((nestedChild) => React.cloneElement(child, {
234
- data: Object.assign(Object.assign({}, child.props.data), { items: [nestedChild] }),
235
- }));
285
+ return nestedChildren.map((nestedChild) => {
286
+ return wrapped(React.cloneElement(child, {
287
+ data: Object.assign(Object.assign({}, child.props.data), { items: [nestedChild] }),
288
+ }));
289
+ });
236
290
  }
237
291
  }
238
- return child;
292
+ return child && wrapped(child);
239
293
  }).filter(Boolean);
240
294
  }
241
295
  export default SliderBlock;
@@ -1,4 +1,6 @@
1
1
  {
2
2
  "arrow-right": "Next",
3
- "arrow-left": "Previous"
3
+ "arrow-left": "Previous",
4
+ "dot-label": "Page {{index}} of {{count}}",
5
+ "pagination-label": "Pages"
4
6
  }
@@ -1 +1 @@
1
- export declare const i18n: (key: "arrow-right" | "arrow-left", params?: import("@gravity-ui/i18n").Params | undefined) => string;
1
+ export declare const i18n: (key: "arrow-right" | "arrow-left" | "dot-label" | "pagination-label", params?: import("@gravity-ui/i18n").Params | undefined) => string;
@@ -1,4 +1,6 @@
1
1
  {
2
2
  "arrow-right": "Дальше",
3
- "arrow-left": "Назад"
3
+ "arrow-left": "Назад",
4
+ "dot-label": "Страница {{index}} из {{count}}",
5
+ "pagination-label": "Страницы"
4
6
  }
@@ -10,6 +10,7 @@ export interface GetSlidesToShowParams {
10
10
  breakpoints?: SlidesToShow;
11
11
  mobileFullscreen?: boolean;
12
12
  }
13
+ export declare const isFocusable: (element: Element) => boolean;
13
14
  export declare function getSlidesToShowWithDefaults({ contentLength, breakpoints, mobileFullscreen, }: GetSlidesToShowParams): {
14
15
  sm: number;
15
16
  xl: number;
@@ -24,3 +25,12 @@ export declare function getSliderResponsiveParams(breakpoints: SliderBreakpointP
24
25
  }[];
25
26
  export declare function getSlidesCountByBreakpoint(breakpoint: number, breakpoints: SliderBreakpointParams): number;
26
27
  export declare function getSlidesToShowCount(breakpoints: SliderBreakpointParams): number;
28
+ export declare function useRovingTabIndex(props: {
29
+ itemCount: number;
30
+ activeIndex: number;
31
+ firstIndex?: number;
32
+ uniqId: string;
33
+ }): {
34
+ getRovingItemProps: (index: number) => Pick<React.HTMLAttributes<HTMLElement>, 'id' | 'tabIndex' | 'onFocus'>;
35
+ rovingListProps: import("react").HTMLAttributes<HTMLElement>;
36
+ };
@@ -1,3 +1,4 @@
1
+ import { useEffect, useRef, useState } from 'react';
1
2
  import pickBy from 'lodash/pickBy';
2
3
  import { BREAKPOINTS } from '../../constants';
3
4
  import { SliderBreakpointNames } from './models';
@@ -8,6 +9,37 @@ export const DEFAULT_SLIDE_BREAKPOINTS = {
8
9
  [SliderBreakpointNames.Sm]: 1.15,
9
10
  };
10
11
  const BREAKPOINT_NAMES_BY_VALUES = Object.entries(BREAKPOINTS).reduce((acc, [key, value]) => (Object.assign(Object.assign({}, acc), { [value]: key })), {});
12
+ export const isFocusable = (element) => {
13
+ if (!(element instanceof HTMLElement)) {
14
+ return false;
15
+ }
16
+ const tabIndexAttr = element.getAttribute('tabindex');
17
+ const hasTabIndex = tabIndexAttr !== null;
18
+ const tabIndex = Number(tabIndexAttr);
19
+ if (element.ariaHidden === 'true' || (hasTabIndex && tabIndex < 0)) {
20
+ return false;
21
+ }
22
+ if (hasTabIndex && tabIndex >= 0) {
23
+ return true;
24
+ }
25
+ // without this jest fails here for some reason
26
+ let htmlElement;
27
+ switch (true) {
28
+ case element instanceof HTMLAnchorElement:
29
+ htmlElement = element;
30
+ return Boolean(htmlElement.href);
31
+ case element instanceof HTMLInputElement:
32
+ htmlElement = element;
33
+ return htmlElement.type !== 'hidden' && !htmlElement.disabled;
34
+ case element instanceof HTMLSelectElement:
35
+ case element instanceof HTMLTextAreaElement:
36
+ case element instanceof HTMLButtonElement:
37
+ htmlElement = element;
38
+ return !htmlElement.disabled;
39
+ default:
40
+ return false;
41
+ }
42
+ };
11
43
  export function getSlidesToShowWithDefaults({ contentLength, breakpoints, mobileFullscreen, }) {
12
44
  let result;
13
45
  if (typeof breakpoints === 'number') {
@@ -31,3 +63,53 @@ export function getSlidesCountByBreakpoint(breakpoint, breakpoints) {
31
63
  export function getSlidesToShowCount(breakpoints) {
32
64
  return Math.floor(Math.max(...Object.values(breakpoints)));
33
65
  }
66
+ const getRovingListItemId = (uniqId, index) => `${uniqId}-roving-tabindex-item-${index}`;
67
+ export function useRovingTabIndex(props) {
68
+ const { itemCount, activeIndex, firstIndex = 0, uniqId } = props;
69
+ const [currentIndex, setCurrentIndex] = useState(firstIndex);
70
+ const hasFocusRef = useRef(false);
71
+ const lastIndex = itemCount + firstIndex - 1;
72
+ const getRovingItemProps = (index) => {
73
+ return {
74
+ id: getRovingListItemId(uniqId, index),
75
+ tabIndex: index === activeIndex ? 0 : -1,
76
+ onFocus: () => {
77
+ setCurrentIndex(index);
78
+ hasFocusRef.current = true;
79
+ },
80
+ };
81
+ };
82
+ useEffect(() => {
83
+ var _a;
84
+ if (!hasFocusRef.current) {
85
+ return;
86
+ }
87
+ (_a = document.getElementById(getRovingListItemId(uniqId, currentIndex))) === null || _a === void 0 ? void 0 : _a.focus();
88
+ }, [activeIndex, currentIndex, uniqId]);
89
+ const setNextIndex = () => setCurrentIndex((prev) => (prev >= lastIndex ? firstIndex : prev + 1));
90
+ const setPrevIndex = () => setCurrentIndex((prev) => (prev <= firstIndex ? lastIndex : prev - 1));
91
+ const onRovingListKeyDown = (e) => {
92
+ const key = e.key.toLowerCase();
93
+ if (key !== 'tab' && key !== 'enter') {
94
+ e.preventDefault();
95
+ }
96
+ switch (key) {
97
+ case 'arrowleft':
98
+ case 'arrowup':
99
+ setPrevIndex();
100
+ return;
101
+ case 'arrowright':
102
+ case 'arrowdown':
103
+ setNextIndex();
104
+ return;
105
+ }
106
+ };
107
+ const onRovingListBlur = () => {
108
+ hasFocusRef.current = false;
109
+ };
110
+ const rovingListProps = {
111
+ onKeyDown: onRovingListKeyDown,
112
+ onBlur: onRovingListBlur,
113
+ };
114
+ return { getRovingItemProps, rovingListProps };
115
+ }
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { ClassNameProps } from '../../../models';
2
3
  import './Arrow.css';
3
4
  export type ArrowType = 'left' | 'right';
@@ -5,6 +6,7 @@ export interface ArrowProps {
5
6
  type: ArrowType;
6
7
  onClick?: () => void;
7
8
  size?: number;
9
+ extraProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
8
10
  }
9
- declare const Arrow: ({ type, onClick, className, size }: ArrowProps & ClassNameProps) => JSX.Element;
11
+ declare const Arrow: ({ type, onClick, className, size, extraProps }: ArrowProps & ClassNameProps) => JSX.Element;
10
12
  export default Arrow;
@@ -4,8 +4,8 @@ import { block } from '../../../utils';
4
4
  import { i18n } from '../i18n';
5
5
  import './Arrow.css';
6
6
  const b = block('slider-new-block-arrow');
7
- const Arrow = ({ type, onClick, className, size = 16 }) => (React.createElement("div", { className: b({ type }, className) },
8
- React.createElement("button", { className: b('button'), onClick: onClick, "aria-label": i18n(`arrow-${type}`) },
7
+ const Arrow = ({ type, onClick, className, size = 16, extraProps }) => (React.createElement("div", { className: b({ type }, className) },
8
+ React.createElement("button", Object.assign({ className: b('button'), onClick: onClick, "aria-label": i18n(`arrow-${type}`) }, extraProps),
9
9
  React.createElement("span", { className: b('icon-wrapper') },
10
10
  React.createElement(ToggleArrow, { size: size, type: 'horizontal', iconType: "navigation", className: b('icon') })))));
11
11
  export default Arrow;
@@ -6,18 +6,30 @@ import AnimateBlock from '../../components/AnimateBlock/AnimateBlock';
6
6
  import Title from '../../components/Title/Title';
7
7
  import { block } from '../../utils';
8
8
  import Arrow from './Arrow/Arrow';
9
+ import { i18n } from './i18n';
9
10
  import { useSlider } from './useSlider';
11
+ import { useSliderPagination } from './useSliderPagination';
10
12
  import './Slider.css';
11
13
  import 'swiper/swiper-bundle.css';
12
14
  const b = block('SliderNewBlock');
13
15
  SwiperCore.use([Autoplay, A11y, Pagination]);
14
16
  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, }) => {
15
- const { childrenCount, breakpoints, autoplay, onSwiper, onPrev, onNext, isLocked, setIsLocked } = useSlider({
17
+ const { autoplay, isLocked, childrenCount, breakpoints, onSwiper, onPrev, onNext, setIsLocked } = useSlider({
16
18
  slidesToShow,
17
19
  children,
18
20
  type,
19
21
  autoplayMs,
20
22
  });
23
+ const isA11yControlHidden = Boolean(autoplay);
24
+ const controlTabIndex = isA11yControlHidden ? -1 : 0;
25
+ const paginationProps = useSliderPagination({
26
+ enabled: dots,
27
+ isA11yControlHidden,
28
+ controlTabIndex,
29
+ bulletClass: b('dot', dotsClassName),
30
+ bulletActiveClass: b('dot_active'),
31
+ paginationLabel: i18n('pagination-label'),
32
+ });
21
33
  return (React.createElement("div", { className: b({
22
34
  'one-slide': childrenCount === 1,
23
35
  'only-arrows': !(title === null || title === void 0 ? void 0 : title.text) && !description && arrows,
@@ -27,14 +39,14 @@ export const SliderNewBlock = ({ animated, title, description, type, anchorId, a
27
39
  anchorId && React.createElement(Anchor, { id: anchorId }),
28
40
  React.createElement(Title, { title: title, subtitle: description, className: b('header', { 'no-description': !description }) }),
29
41
  React.createElement(AnimateBlock, { className: b('animate-slides'), animate: animated },
30
- React.createElement(Swiper, { className: b('slider', className), onSwiper: onSwiper, pagination: dots && {
31
- clickable: true,
32
- bulletClass: b('dot', dotsClassName),
33
- bulletActiveClass: b('dot_active'),
34
- }, 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: true, watchOverflow: true }, React.Children.map(children, (elem, index) => (React.createElement(SwiperSlide, { className: b('slide'), key: index }, elem)))),
42
+ React.createElement(Swiper, Object.assign({ 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: true, watchOverflow: true, a11y: {
43
+ slideLabelMessage: '',
44
+ paginationBulletMessage: i18n('dot-label', { index: '{{index}}' }),
45
+ } }, paginationProps), React.Children.map(children, (elem, index) => (React.createElement(SwiperSlide, { className: b('slide'), key: index }, ({ isVisible }) => (React.createElement("div", { "aria-hidden": !isA11yControlHidden && !isVisible }, elem)))))),
35
46
  arrows && !isLocked && (React.createElement(Fragment, null,
36
- React.createElement(Arrow, { className: b('arrow', { prev: true }), type: "left", onClick: onPrev, size: arrowSize }),
37
- React.createElement(Arrow, { className: b('arrow', { next: true }), type: "right", onClick: onNext, size: arrowSize }))),
47
+ React.createElement("div", { "aria-hidden": isA11yControlHidden },
48
+ React.createElement(Arrow, { className: b('arrow', { prev: true }), type: "left", onClick: onPrev, size: arrowSize, extraProps: { tabIndex: controlTabIndex } }),
49
+ React.createElement(Arrow, { className: b('arrow', { next: true }), type: "right", onClick: onNext, size: arrowSize, extraProps: { tabIndex: controlTabIndex } })))),
38
50
  React.createElement("div", { className: b('footer') }, disclaimer ? (React.createElement("div", { className: b('disclaimer', { size: (disclaimer === null || disclaimer === void 0 ? void 0 : disclaimer.size) || 'm' }) }, disclaimer === null || disclaimer === void 0 ? void 0 : disclaimer.text)) : null))));
39
51
  };
40
52
  export default SliderNewBlock;
@@ -1,4 +1,6 @@
1
1
  {
2
2
  "arrow-right": "Next",
3
- "arrow-left": "Previous"
3
+ "arrow-left": "Previous",
4
+ "dot-label": "Page {{index}}",
5
+ "pagination-label": "Pages"
4
6
  }
@@ -1 +1 @@
1
- export declare const i18n: (key: "arrow-right" | "arrow-left", params?: import("@gravity-ui/i18n").Params | undefined) => string;
1
+ export declare const i18n: (key: "arrow-right" | "arrow-left" | "dot-label" | "pagination-label", params?: import("@gravity-ui/i18n").Params | undefined) => string;
@@ -1,4 +1,6 @@
1
1
  {
2
2
  "arrow-right": "Дальше",
3
- "arrow-left": "Назад"
3
+ "arrow-left": "Назад",
4
+ "dot-label": "Страница {{index}}",
5
+ "pagination-label": "Страницы"
4
6
  }
@@ -1,11 +1,12 @@
1
1
  import React, { PropsWithChildren } from 'react';
2
2
  import type { Swiper } from 'swiper';
3
3
  import { SlidesToShow } from '../../models';
4
- export declare const useSlider: ({ children, autoplayMs, type, slidesToShow, }: React.PropsWithChildren<{
5
- autoplayMs?: number | undefined;
6
- type?: string | undefined;
7
- slidesToShow?: SlidesToShow | undefined;
8
- }>) => {
4
+ type UseSliderProps = PropsWithChildren<{
5
+ autoplayMs?: number;
6
+ type?: string;
7
+ slidesToShow?: SlidesToShow;
8
+ }>;
9
+ export declare const useSlider: ({ children, autoplayMs, type, ...props }: UseSliderProps) => {
9
10
  slider: Swiper | undefined;
10
11
  onSwiper: React.Dispatch<React.SetStateAction<Swiper | undefined>>;
11
12
  onNext: () => void;
@@ -15,7 +16,8 @@ export declare const useSlider: ({ children, autoplayMs, type, slidesToShow, }:
15
16
  isLocked: boolean;
16
17
  setIsLocked: React.Dispatch<React.SetStateAction<boolean>>;
17
18
  autoplay: false | {
18
- delay: number | undefined;
19
+ delay: number;
19
20
  disableOnInteraction: boolean;
20
21
  };
21
22
  };
23
+ export {};
@@ -1,11 +1,14 @@
1
+ import { __rest } from "tslib";
1
2
  import React, { useEffect, useMemo, useState } from 'react';
2
3
  import { SliderType } from '../../models';
3
- import { getSliderResponsiveParams } from './utils';
4
- export const useSlider = ({ children, autoplayMs, type, slidesToShow, }) => {
4
+ import { getSliderResponsiveParams, useMemoized } from './utils';
5
+ export const useSlider = (_a) => {
6
+ var { children, autoplayMs, type } = _a, props = __rest(_a, ["children", "autoplayMs", "type"]);
5
7
  const [slider, setSlider] = useState();
6
8
  const [isLocked, setIsLocked] = useState(false);
9
+ const slidesToShow = useMemoized(props.slidesToShow);
7
10
  const childrenCount = React.Children.count(children);
8
- const autoplayEnabled = useMemo(() => Boolean(autoplayMs), [autoplayMs]);
11
+ const autoplayEnabled = autoplayMs !== undefined && autoplayMs > 0;
9
12
  const breakpoints = useMemo(() => {
10
13
  return getSliderResponsiveParams({
11
14
  contentLength: childrenCount,
@@ -0,0 +1,9 @@
1
+ import { Swiper as SwiperProps } from 'swiper/swiper-react';
2
+ export declare const useSliderPagination: (props: {
3
+ enabled: boolean;
4
+ isA11yControlHidden: boolean;
5
+ controlTabIndex: number;
6
+ bulletClass: string;
7
+ bulletActiveClass: string;
8
+ paginationLabel: string;
9
+ }) => Pick<SwiperProps, 'pagination' | 'onPaginationUpdate'> | undefined;
@@ -0,0 +1,32 @@
1
+ import { setElementAtrributes } from './utils';
2
+ export const useSliderPagination = (props) => {
3
+ if (!props.enabled) {
4
+ return undefined;
5
+ }
6
+ const { isA11yControlHidden, controlTabIndex, bulletClass, bulletActiveClass, paginationLabel } = props;
7
+ return {
8
+ pagination: {
9
+ clickable: true,
10
+ bulletClass,
11
+ bulletActiveClass,
12
+ },
13
+ onPaginationUpdate: (slider) => {
14
+ const pagination = slider.pagination.el;
15
+ setElementAtrributes(pagination, {
16
+ role: 'menu',
17
+ 'aria-hidden': isA11yControlHidden,
18
+ 'aria-label': paginationLabel,
19
+ });
20
+ const bullets = pagination.querySelectorAll(`.${bulletClass}`);
21
+ bullets.forEach((bullet) => {
22
+ const isActive = bullet.classList.contains(bulletActiveClass);
23
+ setElementAtrributes(bullet, {
24
+ role: 'menuitemradio',
25
+ 'aria-hidden': isA11yControlHidden,
26
+ 'aria-checked': isActive,
27
+ tabindex: controlTabIndex,
28
+ });
29
+ });
30
+ },
31
+ };
32
+ };
@@ -12,3 +12,5 @@ export interface GetSlidesToShowParams {
12
12
  mobileFullscreen?: boolean;
13
13
  }
14
14
  export declare function getSliderResponsiveParams({ contentLength, slidesToShow, mobileFullscreen, }: GetSlidesToShowParams): Record<number, SwiperOptions>;
15
+ export declare const useMemoized: <T>(value: T) => T;
16
+ export declare const setElementAtrributes: (element: Element, attributes: Record<string, unknown>) => void;
@@ -1,3 +1,5 @@
1
+ import { useEffect, useState } from 'react';
2
+ import isEqual from 'lodash/isEqual';
1
3
  import pickBy from 'lodash/pickBy';
2
4
  import { BREAKPOINTS } from '../../constants';
3
5
  import { SliderBreakpointNames } from './models';
@@ -24,3 +26,11 @@ export function getSliderResponsiveParams({ contentLength, slidesToShow, mobileF
24
26
  return res;
25
27
  }, {});
26
28
  }
29
+ export const useMemoized = (value) => {
30
+ const [memoizedValue, setMemoizedValue] = useState(value);
31
+ useEffect(() => {
32
+ setMemoizedValue((memoized) => value && typeof value === 'object' && isEqual(memoized, value) ? memoized : value);
33
+ }, [value]);
34
+ return memoizedValue;
35
+ };
36
+ export const setElementAtrributes = (element, attributes) => Object.entries(attributes).forEach(([attribute, value]) => element.setAttribute(attribute, String(value)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/page-constructor",
3
- "version": "5.27.0",
3
+ "version": "5.27.1",
4
4
  "description": "Gravity UI Page Constructor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -80,6 +80,7 @@
80
80
  "test:watch": "jest --watchAll",
81
81
  "playwright": "playwright test --config=playwright/playwright.config.ts",
82
82
  "playwright:update": "npm run playwright -- -u",
83
+ "playwright:clear-cache": "rm -rf ./playwright/.cache",
83
84
  "playwright:docker": "./scripts/playwright-docker.sh 'npm run playwright'",
84
85
  "playwright:docker:update": "./scripts/playwright-docker.sh 'npm run playwright:update'",
85
86
  "playwright:docker:clear-cache": "./scripts/playwright-docker.sh clear-cache",