@doyourjob/gravity-ui-page-constructor 5.31.283 → 5.31.285

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.
@@ -14,7 +14,7 @@ const Image_1 = tslib_1.__importDefault(require("./Image/Image"));
14
14
  const Video_1 = tslib_1.__importDefault(require("./Video/Video"));
15
15
  const b = (0, utils_1.block)('Media');
16
16
  const Media = (props) => {
17
- const { animated, image, video, youtube, videoIframe, dataLens, color, height, previewImg, previewVideo, parallax = false, fullscreen, analyticsEvents, className, imageClassName, videoClassName, youtubeClassName, disableImageSliderForArrayInput, playVideo = true, isBackground, playButton, playButtonCorner, customBarControlsClassName, qa, ratio, autoplay, onImageLoad, iframe, margins, videoMicrodata, } = props;
17
+ const { animated, image, video, youtube, videoIframe, dataLens, color, height, previewImg, previewVideo, parallax = false, fullscreen, analyticsEvents, className, imageClassName, videoClassName, youtubeClassName, disableImageSliderForArrayInput, playVideo = true, isBackground, playButton, playButtonCorner, playButtonText, customBarControlsClassName, qa, ratio, autoplay, onImageLoad, iframe, margins, videoMicrodata, } = props;
18
18
  const [hasVideoFallback, setHasVideoFallback] = (0, react_1.useState)(false);
19
19
  const { microdata } = (0, react_1.useContext)(innerContext_1.InnerContext);
20
20
  const qaAttributes = (0, utils_1.getQaAttrubutes)(qa, 'video');
@@ -46,7 +46,7 @@ const Media = (props) => {
46
46
  }
47
47
  }
48
48
  if (youtube || videoIframe) {
49
- result = (react_1.default.createElement(VideoBlock_1.default, { className: b('youtube', youtubeClassName), record: youtube, videoIframe: videoIframe, attributes: { color: 'white', rel: '0' }, previewImg: previewImg, previewVideo: previewVideo, playButtonCorner: playButtonCorner, height: height, ratio: ratio, fullscreen: fullscreen, analyticsEvents: analyticsEvents, autoplay: autoplay, onImageLoad: onImageLoad }));
49
+ result = (react_1.default.createElement(VideoBlock_1.default, { className: b('youtube', youtubeClassName), record: youtube, videoIframe: videoIframe, attributes: { color: 'white', rel: '0' }, previewImg: previewImg, previewVideo: previewVideo, playButtonCorner: playButtonCorner, playButtonText: playButtonText, height: height, ratio: ratio, fullscreen: fullscreen, analyticsEvents: analyticsEvents, autoplay: autoplay, onImageLoad: onImageLoad }));
50
50
  }
51
51
  if (dataLens) {
52
52
  result = react_1.default.createElement(DataLens_1.default, { dataLens: dataLens });
@@ -83,6 +83,7 @@ const Media = (props) => {
83
83
  ratio,
84
84
  youtubeClassName,
85
85
  playButtonCorner,
86
+ playButtonText,
86
87
  autoplay,
87
88
  margins,
88
89
  ]);
@@ -4,6 +4,16 @@ unpredictable css rules order in build */
4
4
  position: relative;
5
5
  }
6
6
  .pc-VideoBlock__preview {
7
+ display: inline-block;
8
+ margin: 0;
9
+ padding: 0;
10
+ font: inherit;
11
+ border: none;
12
+ outline: none;
13
+ color: inherit;
14
+ background: none;
15
+ cursor: pointer;
16
+ border-radius: inherit;
7
17
  display: flex;
8
18
  justify-content: center;
9
19
  align-items: center;
@@ -13,6 +23,13 @@ unpredictable css rules order in build */
13
23
  width: 100%;
14
24
  height: 100%;
15
25
  }
26
+ .pc-VideoBlock__preview:focus {
27
+ outline: 2px solid var(--g-color-line-focus);
28
+ outline-offset: -1px;
29
+ }
30
+ .pc-VideoBlock__preview:focus:not(:focus-visible) {
31
+ outline: 0;
32
+ }
16
33
  .pc-VideoBlock__image, .pc-VideoBlock__video {
17
34
  width: 100%;
18
35
  height: 100%;
@@ -24,15 +41,6 @@ unpredictable css rules order in build */
24
41
  width: 100%;
25
42
  }
26
43
  .pc-VideoBlock__button {
27
- display: inline-block;
28
- margin: 0;
29
- padding: 0;
30
- font: inherit;
31
- border: none;
32
- outline: none;
33
- color: inherit;
34
- background: none;
35
- cursor: pointer;
36
44
  display: flex;
37
45
  justify-content: center;
38
46
  align-items: center;
@@ -44,19 +52,20 @@ unpredictable css rules order in build */
44
52
  background-color: rgba(215, 215, 215, 0.32);
45
53
  border-radius: 50%;
46
54
  }
47
- .pc-VideoBlock__button:focus {
48
- outline: 2px solid var(--g-color-line-focus);
49
- outline-offset: 0;
50
- }
51
- .pc-VideoBlock__button:focus:not(:focus-visible) {
52
- outline: 0;
53
- }
54
55
  .pc-VideoBlock__button_corner {
55
56
  right: 16px;
56
57
  bottom: 16px;
57
58
  width: 44px;
58
59
  height: 44px;
59
60
  }
61
+ .pc-VideoBlock__button_text {
62
+ height: 61px;
63
+ width: auto;
64
+ border-radius: 16px;
65
+ font-size: 24px;
66
+ line-height: 35px;
67
+ padding: 0 28px;
68
+ }
60
69
  .pc-VideoBlock__icon {
61
70
  margin-left: 1px;
62
71
  }
@@ -16,12 +16,15 @@ export interface VideoBlockProps extends AnalyticsEventsBase {
16
16
  previewVideo?: string;
17
17
  playButton?: React.ReactNode;
18
18
  playButtonCorner?: boolean;
19
+ playButtonText?: string;
19
20
  playButtonId?: string;
20
21
  height?: number;
21
22
  ratio?: number | 'auto';
22
23
  fullscreen?: boolean;
23
24
  autoplay?: boolean;
24
25
  onImageLoad?: () => void;
26
+ /** Название видео — используется как title для iframe и aria-label для кнопки воспроизведения */
27
+ title?: string;
25
28
  }
26
29
  declare const VideoBlock: (props: VideoBlockProps) => JSX.Element | null;
27
30
  export default VideoBlock;
@@ -49,14 +49,15 @@ function getHeight(width, ratio) {
49
49
  }
50
50
  exports.getHeight = getHeight;
51
51
  const VideoBlock = (props) => {
52
- const { stream, record, videoIframe, attributes, className, id, previewImg, previewVideo, playButton, playButtonCorner, playButtonId, height, ratio, fullscreen, analyticsEvents, autoplay, onImageLoad, } = props;
52
+ const { stream, record, videoIframe, attributes, className, id, previewImg, previewVideo, playButton, playButtonCorner, playButtonText, playButtonId, height, ratio, fullscreen, analyticsEvents, autoplay, onImageLoad, title, } = props;
53
53
  const handleAnalytics = (0, useAnalytics_1.useAnalytics)(common_1.DefaultEventNames.VideoPreview);
54
54
  const src = videoIframe ? videoIframe : getYoutubeVideoSrc(stream, record);
55
- const ref = (0, react_1.useRef)(null);
55
+ const containerRef = (0, react_1.useRef)(null);
56
+ // Отдельный ref для iframe чтобы управлять фокусом
57
+ const iframeRef = (0, react_1.useRef)(null);
56
58
  const [hidePreview, setHidePreview] = (0, react_1.useState)(false);
57
59
  const [currentHeight, setCurrentHeight] = (0, react_1.useState)(height || undefined);
58
60
  const fullId = (0, uikit_1.useUniqId)();
59
- const buttonId = (0, uikit_1.useUniqId)();
60
61
  const [isPlaying, setIsPlaying] = (0, react_1.useState)(!previewImg);
61
62
  const [isHovered, setIsHovered] = (0, react_1.useState)(false);
62
63
  const iframeSrc = (0, react_1.useMemo)(() => {
@@ -78,7 +79,13 @@ const VideoBlock = (props) => {
78
79
  const onPreviewClick = (0, react_1.useCallback)(() => {
79
80
  handleAnalytics(analyticsEvents);
80
81
  setIsPlaying(true);
81
- setTimeout(() => setHidePreview(true), AUTOPLAY_DELAY);
82
+ setTimeout(() => {
83
+ var _a;
84
+ setHidePreview(true);
85
+ // Перемещаем фокус на iframe после скрытия превью,
86
+ // чтобы пользователь клавиатуры/скринридера не терял ориентацию
87
+ (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.focus();
88
+ }, AUTOPLAY_DELAY);
82
89
  }, [handleAnalytics, analyticsEvents]);
83
90
  const onMouseEnter = (0, react_1.useCallback)(() => {
84
91
  setIsHovered(true);
@@ -86,10 +93,11 @@ const VideoBlock = (props) => {
86
93
  const onMouseLeave = (0, react_1.useCallback)(() => {
87
94
  setIsHovered(false);
88
95
  }, []);
89
- const { onKeyDown: onPreviewKeyDown } = (0, uikit_1.useActionHandlers)(onPreviewClick);
90
96
  (0, react_1.useEffect)(() => {
91
97
  const updateSize = (0, debounce_1.default)(() => {
92
- setCurrentHeight(ref.current ? Math.round(getHeight(ref.current.offsetWidth, ratio)) : undefined);
98
+ setCurrentHeight(containerRef.current
99
+ ? Math.round(getHeight(containerRef.current.offsetWidth, ratio))
100
+ : undefined);
93
101
  }, 100);
94
102
  updateSize();
95
103
  window.addEventListener('resize', updateSize, { passive: true });
@@ -97,20 +105,47 @@ const VideoBlock = (props) => {
97
105
  window.removeEventListener('resize', updateSize);
98
106
  };
99
107
  }, [height, ratio]);
108
+ // Осмысленный title для iframe: если передан title пропс — используем его,
109
+ // иначе fallback на i18n-строку
110
+ const iframeTitle = title ? `${(0, i18n_1.i18n)('iframe-title')}: ${title}` : (0, i18n_1.i18n)('iframe-title');
111
+ // Пока показывается превью — iframe скрыт от вспомогательных технологий
112
+ // и не попадает в порядок фокуса
113
+ const isPreviewVisible = Boolean(previewImg) && !hidePreview && !fullscreen;
100
114
  const iframeContent = (0, react_1.useMemo)(() => {
101
- return (react_1.default.createElement("iframe", { id: id || fullId, src: iframeSrc, width: "100%", height: "100%", title: (0, i18n_1.i18n)('iframe-title'), frameBorder: "0", allow: "autoplay; fullscreen; encrypted-media; accelerometer; gyroscope; picture-in-picture; clipboard-write; web-share; screen-wake-lock", loading: "lazy" }));
102
- }, [fullId, id, iframeSrc]);
115
+ return (react_1.default.createElement("iframe", { ref: iframeRef, id: id || fullId, src: iframeSrc, width: "100%", height: "100%", title: iframeTitle, frameBorder: "0", allow: "autoplay; fullscreen; encrypted-media; accelerometer; gyroscope; picture-in-picture; clipboard-write; web-share; screen-wake-lock", loading: "lazy", "aria-hidden": isPreviewVisible || undefined, tabIndex: isPreviewVisible ? -1 : 0 }));
116
+ // eslint-disable-next-line react-hooks/exhaustive-deps
117
+ }, [fullId, id, iframeSrc, iframeTitle, isPreviewVisible]);
103
118
  (0, react_1.useEffect)(() => {
104
119
  setHidePreview(false);
105
120
  }, [src]);
106
121
  if (!src) {
107
122
  return null;
108
123
  }
109
- return (react_1.default.createElement("div", { className: b(null, className), style: { height: currentHeight }, ref: ref },
124
+ // aria-label для кнопки воспроизведения:
125
+ // приоритет — playButtonText, затем title видео, затем дефолтная i18n-строка
126
+ const playButtonAriaLabel = playButtonText || title
127
+ ? [playButtonText || 'Play', title].filter(Boolean).join(': ')
128
+ : 'Play';
129
+ return (react_1.default.createElement("div", { className: b(null, className), style: { height: currentHeight }, ref: containerRef },
110
130
  iframeContent,
111
- previewImg && !hidePreview && !fullscreen && (react_1.default.createElement("div", { className: b('preview'), onClick: onPreviewClick, onKeyDown: onPreviewKeyDown, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, role: "button", tabIndex: 0, "aria-labelledby": playButton ? playButtonId : buttonId },
112
- isHovered && previewVideo ? (react_1.default.createElement("video", { src: previewVideo, className: b('video'), autoPlay: true, muted: true, loop: true, playsInline: true })) : (react_1.default.createElement(Image_1.default, { src: previewImg, className: b('image'), containerClassName: b('image-wrapper'), onLoad: onImageLoad })),
113
- playButton || (react_1.default.createElement("button", { title: "Play", id: buttonId, className: b('button', { corner: playButtonCorner }) },
114
- react_1.default.createElement(uikit_1.Icon, { className: b('icon'), data: icons_1.PlayFill, size: playButtonCorner ? 16 : 24 })))))));
131
+ previewImg && !hidePreview && !fullscreen && (
132
+ // Используем настоящий <button> вместо <div role="button">:
133
+ // нативно фокусируется по Tab
134
+ // активируется Enter/Space без дополнительного onKeyDown
135
+ // — корректно объявляется скринридерами
136
+ react_1.default.createElement("button", { className: b('preview'), onClick: onPreviewClick, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, "aria-label": playButtonAriaLabel,
137
+ // Если передан внешний playButton с собственным id — используем его,
138
+ // иначе aria-label на самой кнопке достаточно
139
+ id: playButton ? playButtonId : undefined, type: "button" },
140
+ isHovered && previewVideo ? (
141
+ // Декоративное превью-видео скрыто от AT
142
+ react_1.default.createElement("video", { src: previewVideo, className: b('video'), autoPlay: true, muted: true, loop: true, playsInline: true, "aria-hidden": "true" })) : (react_1.default.createElement(Image_1.default, { src: previewImg, className: b('image'), containerClassName: b('image-wrapper'), onLoad: onImageLoad })),
143
+ playButton || (
144
+ // Декоративный элемент кнопки скрыт от AT —
145
+ // вся доступная информация уже есть в aria-label родительской кнопки
146
+ react_1.default.createElement("span", { className: b('button', {
147
+ corner: playButtonCorner,
148
+ text: Boolean(playButtonText),
149
+ }), "aria-hidden": "true" }, playButtonText ? (react_1.default.createElement("span", { className: b('button-text') }, playButtonText)) : (react_1.default.createElement(uikit_1.Icon, { className: b('icon'), data: icons_1.PlayFill, size: playButtonCorner ? 16 : 24 }))))))));
115
150
  };
116
151
  exports.default = VideoBlock;
@@ -196,6 +196,7 @@ export interface MediaComponentYoutubeProps {
196
196
  previewImg?: string;
197
197
  previewVideo?: string;
198
198
  playButtonCorner?: boolean;
199
+ playButtonText?: string;
199
200
  fullscreen?: boolean;
200
201
  }
201
202
  export interface MediaComponentImageProps {
@@ -11,7 +11,7 @@ import Video from './Video/Video';
11
11
  import './Media.css';
12
12
  const b = block('Media');
13
13
  export const Media = (props) => {
14
- const { animated, image, video, youtube, videoIframe, dataLens, color, height, previewImg, previewVideo, parallax = false, fullscreen, analyticsEvents, className, imageClassName, videoClassName, youtubeClassName, disableImageSliderForArrayInput, playVideo = true, isBackground, playButton, playButtonCorner, customBarControlsClassName, qa, ratio, autoplay, onImageLoad, iframe, margins, videoMicrodata, } = props;
14
+ const { animated, image, video, youtube, videoIframe, dataLens, color, height, previewImg, previewVideo, parallax = false, fullscreen, analyticsEvents, className, imageClassName, videoClassName, youtubeClassName, disableImageSliderForArrayInput, playVideo = true, isBackground, playButton, playButtonCorner, playButtonText, customBarControlsClassName, qa, ratio, autoplay, onImageLoad, iframe, margins, videoMicrodata, } = props;
15
15
  const [hasVideoFallback, setHasVideoFallback] = useState(false);
16
16
  const { microdata } = useContext(InnerContext);
17
17
  const qaAttributes = getQaAttrubutes(qa, 'video');
@@ -43,7 +43,7 @@ export const Media = (props) => {
43
43
  }
44
44
  }
45
45
  if (youtube || videoIframe) {
46
- result = (React.createElement(IframeVideoBlock, { className: b('youtube', youtubeClassName), record: youtube, videoIframe: videoIframe, attributes: { color: 'white', rel: '0' }, previewImg: previewImg, previewVideo: previewVideo, playButtonCorner: playButtonCorner, height: height, ratio: ratio, fullscreen: fullscreen, analyticsEvents: analyticsEvents, autoplay: autoplay, onImageLoad: onImageLoad }));
46
+ result = (React.createElement(IframeVideoBlock, { className: b('youtube', youtubeClassName), record: youtube, videoIframe: videoIframe, attributes: { color: 'white', rel: '0' }, previewImg: previewImg, previewVideo: previewVideo, playButtonCorner: playButtonCorner, playButtonText: playButtonText, height: height, ratio: ratio, fullscreen: fullscreen, analyticsEvents: analyticsEvents, autoplay: autoplay, onImageLoad: onImageLoad }));
47
47
  }
48
48
  if (dataLens) {
49
49
  result = React.createElement(DataLens, { dataLens: dataLens });
@@ -80,6 +80,7 @@ export const Media = (props) => {
80
80
  ratio,
81
81
  youtubeClassName,
82
82
  playButtonCorner,
83
+ playButtonText,
83
84
  autoplay,
84
85
  margins,
85
86
  ]);
@@ -4,6 +4,16 @@ unpredictable css rules order in build */
4
4
  position: relative;
5
5
  }
6
6
  .pc-VideoBlock__preview {
7
+ display: inline-block;
8
+ margin: 0;
9
+ padding: 0;
10
+ font: inherit;
11
+ border: none;
12
+ outline: none;
13
+ color: inherit;
14
+ background: none;
15
+ cursor: pointer;
16
+ border-radius: inherit;
7
17
  display: flex;
8
18
  justify-content: center;
9
19
  align-items: center;
@@ -13,6 +23,13 @@ unpredictable css rules order in build */
13
23
  width: 100%;
14
24
  height: 100%;
15
25
  }
26
+ .pc-VideoBlock__preview:focus {
27
+ outline: 2px solid var(--g-color-line-focus);
28
+ outline-offset: -1px;
29
+ }
30
+ .pc-VideoBlock__preview:focus:not(:focus-visible) {
31
+ outline: 0;
32
+ }
16
33
  .pc-VideoBlock__image, .pc-VideoBlock__video {
17
34
  width: 100%;
18
35
  height: 100%;
@@ -24,15 +41,6 @@ unpredictable css rules order in build */
24
41
  width: 100%;
25
42
  }
26
43
  .pc-VideoBlock__button {
27
- display: inline-block;
28
- margin: 0;
29
- padding: 0;
30
- font: inherit;
31
- border: none;
32
- outline: none;
33
- color: inherit;
34
- background: none;
35
- cursor: pointer;
36
44
  display: flex;
37
45
  justify-content: center;
38
46
  align-items: center;
@@ -44,19 +52,20 @@ unpredictable css rules order in build */
44
52
  background-color: rgba(215, 215, 215, 0.32);
45
53
  border-radius: 50%;
46
54
  }
47
- .pc-VideoBlock__button:focus {
48
- outline: 2px solid var(--g-color-line-focus);
49
- outline-offset: 0;
50
- }
51
- .pc-VideoBlock__button:focus:not(:focus-visible) {
52
- outline: 0;
53
- }
54
55
  .pc-VideoBlock__button_corner {
55
56
  right: 16px;
56
57
  bottom: 16px;
57
58
  width: 44px;
58
59
  height: 44px;
59
60
  }
61
+ .pc-VideoBlock__button_text {
62
+ height: 61px;
63
+ width: auto;
64
+ border-radius: 16px;
65
+ font-size: 24px;
66
+ line-height: 35px;
67
+ padding: 0 28px;
68
+ }
60
69
  .pc-VideoBlock__icon {
61
70
  margin-left: 1px;
62
71
  }
@@ -17,12 +17,15 @@ export interface VideoBlockProps extends AnalyticsEventsBase {
17
17
  previewVideo?: string;
18
18
  playButton?: React.ReactNode;
19
19
  playButtonCorner?: boolean;
20
+ playButtonText?: string;
20
21
  playButtonId?: string;
21
22
  height?: number;
22
23
  ratio?: number | 'auto';
23
24
  fullscreen?: boolean;
24
25
  autoplay?: boolean;
25
26
  onImageLoad?: () => void;
27
+ /** Название видео — используется как title для iframe и aria-label для кнопки воспроизведения */
28
+ title?: string;
26
29
  }
27
30
  declare const VideoBlock: (props: VideoBlockProps) => JSX.Element | null;
28
31
  export default VideoBlock;
@@ -1,6 +1,6 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { PlayFill } from '@gravity-ui/icons';
3
- import { Icon, useActionHandlers, useUniqId } from '@gravity-ui/uikit';
3
+ import { Icon, useUniqId } from '@gravity-ui/uikit';
4
4
  import debounce from 'lodash/debounce';
5
5
  import { useAnalytics } from '../../hooks/useAnalytics';
6
6
  import { DefaultEventNames } from '../../models/common';
@@ -45,14 +45,15 @@ export function getHeight(width, ratio) {
45
45
  return (width / 16) * 9;
46
46
  }
47
47
  const VideoBlock = (props) => {
48
- const { stream, record, videoIframe, attributes, className, id, previewImg, previewVideo, playButton, playButtonCorner, playButtonId, height, ratio, fullscreen, analyticsEvents, autoplay, onImageLoad, } = props;
48
+ const { stream, record, videoIframe, attributes, className, id, previewImg, previewVideo, playButton, playButtonCorner, playButtonText, playButtonId, height, ratio, fullscreen, analyticsEvents, autoplay, onImageLoad, title, } = props;
49
49
  const handleAnalytics = useAnalytics(DefaultEventNames.VideoPreview);
50
50
  const src = videoIframe ? videoIframe : getYoutubeVideoSrc(stream, record);
51
- const ref = useRef(null);
51
+ const containerRef = useRef(null);
52
+ // Отдельный ref для iframe чтобы управлять фокусом
53
+ const iframeRef = useRef(null);
52
54
  const [hidePreview, setHidePreview] = useState(false);
53
55
  const [currentHeight, setCurrentHeight] = useState(height || undefined);
54
56
  const fullId = useUniqId();
55
- const buttonId = useUniqId();
56
57
  const [isPlaying, setIsPlaying] = useState(!previewImg);
57
58
  const [isHovered, setIsHovered] = useState(false);
58
59
  const iframeSrc = useMemo(() => {
@@ -74,7 +75,13 @@ const VideoBlock = (props) => {
74
75
  const onPreviewClick = useCallback(() => {
75
76
  handleAnalytics(analyticsEvents);
76
77
  setIsPlaying(true);
77
- setTimeout(() => setHidePreview(true), AUTOPLAY_DELAY);
78
+ setTimeout(() => {
79
+ var _a;
80
+ setHidePreview(true);
81
+ // Перемещаем фокус на iframe после скрытия превью,
82
+ // чтобы пользователь клавиатуры/скринридера не терял ориентацию
83
+ (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.focus();
84
+ }, AUTOPLAY_DELAY);
78
85
  }, [handleAnalytics, analyticsEvents]);
79
86
  const onMouseEnter = useCallback(() => {
80
87
  setIsHovered(true);
@@ -82,10 +89,11 @@ const VideoBlock = (props) => {
82
89
  const onMouseLeave = useCallback(() => {
83
90
  setIsHovered(false);
84
91
  }, []);
85
- const { onKeyDown: onPreviewKeyDown } = useActionHandlers(onPreviewClick);
86
92
  useEffect(() => {
87
93
  const updateSize = debounce(() => {
88
- setCurrentHeight(ref.current ? Math.round(getHeight(ref.current.offsetWidth, ratio)) : undefined);
94
+ setCurrentHeight(containerRef.current
95
+ ? Math.round(getHeight(containerRef.current.offsetWidth, ratio))
96
+ : undefined);
89
97
  }, 100);
90
98
  updateSize();
91
99
  window.addEventListener('resize', updateSize, { passive: true });
@@ -93,20 +101,47 @@ const VideoBlock = (props) => {
93
101
  window.removeEventListener('resize', updateSize);
94
102
  };
95
103
  }, [height, ratio]);
104
+ // Осмысленный title для iframe: если передан title пропс — используем его,
105
+ // иначе fallback на i18n-строку
106
+ const iframeTitle = title ? `${i18n('iframe-title')}: ${title}` : i18n('iframe-title');
107
+ // Пока показывается превью — iframe скрыт от вспомогательных технологий
108
+ // и не попадает в порядок фокуса
109
+ const isPreviewVisible = Boolean(previewImg) && !hidePreview && !fullscreen;
96
110
  const iframeContent = useMemo(() => {
97
- return (React.createElement("iframe", { id: id || fullId, src: iframeSrc, width: "100%", height: "100%", title: i18n('iframe-title'), frameBorder: "0", allow: "autoplay; fullscreen; encrypted-media; accelerometer; gyroscope; picture-in-picture; clipboard-write; web-share; screen-wake-lock", loading: "lazy" }));
98
- }, [fullId, id, iframeSrc]);
111
+ return (React.createElement("iframe", { ref: iframeRef, id: id || fullId, src: iframeSrc, width: "100%", height: "100%", title: iframeTitle, frameBorder: "0", allow: "autoplay; fullscreen; encrypted-media; accelerometer; gyroscope; picture-in-picture; clipboard-write; web-share; screen-wake-lock", loading: "lazy", "aria-hidden": isPreviewVisible || undefined, tabIndex: isPreviewVisible ? -1 : 0 }));
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps
113
+ }, [fullId, id, iframeSrc, iframeTitle, isPreviewVisible]);
99
114
  useEffect(() => {
100
115
  setHidePreview(false);
101
116
  }, [src]);
102
117
  if (!src) {
103
118
  return null;
104
119
  }
105
- return (React.createElement("div", { className: b(null, className), style: { height: currentHeight }, ref: ref },
120
+ // aria-label для кнопки воспроизведения:
121
+ // приоритет — playButtonText, затем title видео, затем дефолтная i18n-строка
122
+ const playButtonAriaLabel = playButtonText || title
123
+ ? [playButtonText || 'Play', title].filter(Boolean).join(': ')
124
+ : 'Play';
125
+ return (React.createElement("div", { className: b(null, className), style: { height: currentHeight }, ref: containerRef },
106
126
  iframeContent,
107
- previewImg && !hidePreview && !fullscreen && (React.createElement("div", { className: b('preview'), onClick: onPreviewClick, onKeyDown: onPreviewKeyDown, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, role: "button", tabIndex: 0, "aria-labelledby": playButton ? playButtonId : buttonId },
108
- isHovered && previewVideo ? (React.createElement("video", { src: previewVideo, className: b('video'), autoPlay: true, muted: true, loop: true, playsInline: true })) : (React.createElement(Image, { src: previewImg, className: b('image'), containerClassName: b('image-wrapper'), onLoad: onImageLoad })),
109
- playButton || (React.createElement("button", { title: "Play", id: buttonId, className: b('button', { corner: playButtonCorner }) },
110
- React.createElement(Icon, { className: b('icon'), data: PlayFill, size: playButtonCorner ? 16 : 24 })))))));
127
+ previewImg && !hidePreview && !fullscreen && (
128
+ // Используем настоящий <button> вместо <div role="button">:
129
+ // нативно фокусируется по Tab
130
+ // активируется Enter/Space без дополнительного onKeyDown
131
+ // — корректно объявляется скринридерами
132
+ React.createElement("button", { className: b('preview'), onClick: onPreviewClick, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, "aria-label": playButtonAriaLabel,
133
+ // Если передан внешний playButton с собственным id — используем его,
134
+ // иначе aria-label на самой кнопке достаточно
135
+ id: playButton ? playButtonId : undefined, type: "button" },
136
+ isHovered && previewVideo ? (
137
+ // Декоративное превью-видео скрыто от AT
138
+ React.createElement("video", { src: previewVideo, className: b('video'), autoPlay: true, muted: true, loop: true, playsInline: true, "aria-hidden": "true" })) : (React.createElement(Image, { src: previewImg, className: b('image'), containerClassName: b('image-wrapper'), onLoad: onImageLoad })),
139
+ playButton || (
140
+ // Декоративный элемент кнопки скрыт от AT —
141
+ // вся доступная информация уже есть в aria-label родительской кнопки
142
+ React.createElement("span", { className: b('button', {
143
+ corner: playButtonCorner,
144
+ text: Boolean(playButtonText),
145
+ }), "aria-hidden": "true" }, playButtonText ? (React.createElement("span", { className: b('button-text') }, playButtonText)) : (React.createElement(Icon, { className: b('icon'), data: PlayFill, size: playButtonCorner ? 16 : 24 }))))))));
111
146
  };
112
147
  export default VideoBlock;
@@ -196,6 +196,7 @@ export interface MediaComponentYoutubeProps {
196
196
  previewImg?: string;
197
197
  previewVideo?: string;
198
198
  playButtonCorner?: boolean;
199
+ playButtonText?: string;
199
200
  fullscreen?: boolean;
200
201
  }
201
202
  export interface MediaComponentImageProps {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doyourjob/gravity-ui-page-constructor",
3
- "version": "5.31.283",
3
+ "version": "5.31.285",
4
4
  "description": "Gravity UI Page Constructor",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -196,6 +196,7 @@ export interface MediaComponentYoutubeProps {
196
196
  previewImg?: string;
197
197
  previewVideo?: string;
198
198
  playButtonCorner?: boolean;
199
+ playButtonText?: string;
199
200
  fullscreen?: boolean;
200
201
  }
201
202
  export interface MediaComponentImageProps {