@doyourjob/gravity-ui-page-constructor 5.31.284 → 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.
@@ -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,13 +52,6 @@ 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;
@@ -23,6 +23,8 @@ export interface VideoBlockProps extends AnalyticsEventsBase {
23
23
  fullscreen?: boolean;
24
24
  autoplay?: boolean;
25
25
  onImageLoad?: () => void;
26
+ /** Название видео — используется как title для iframe и aria-label для кнопки воспроизведения */
27
+ title?: string;
26
28
  }
27
29
  declare const VideoBlock: (props: VideoBlockProps) => JSX.Element | null;
28
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, playButtonText, 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,22 +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', {
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', {
114
147
  corner: playButtonCorner,
115
148
  text: Boolean(playButtonText),
116
- }) }, playButtonText ? (react_1.default.createElement("div", { className: b('button-text') }, playButtonText)) : (react_1.default.createElement(uikit_1.Icon, { className: b('icon'), data: icons_1.PlayFill, size: playButtonCorner ? 16 : 24 }))))))));
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 }))))))));
117
150
  };
118
151
  exports.default = VideoBlock;
@@ -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,13 +52,6 @@ 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;
@@ -24,6 +24,8 @@ export interface VideoBlockProps extends AnalyticsEventsBase {
24
24
  fullscreen?: boolean;
25
25
  autoplay?: boolean;
26
26
  onImageLoad?: () => void;
27
+ /** Название видео — используется как title для iframe и aria-label для кнопки воспроизведения */
28
+ title?: string;
27
29
  }
28
30
  declare const VideoBlock: (props: VideoBlockProps) => JSX.Element | null;
29
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, playButtonText, 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,22 +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', {
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', {
110
143
  corner: playButtonCorner,
111
144
  text: Boolean(playButtonText),
112
- }) }, playButtonText ? (React.createElement("div", { className: b('button-text') }, playButtonText)) : (React.createElement(Icon, { className: b('icon'), data: PlayFill, size: playButtonCorner ? 16 : 24 }))))))));
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 }))))))));
113
146
  };
114
147
  export default VideoBlock;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doyourjob/gravity-ui-page-constructor",
3
- "version": "5.31.284",
3
+ "version": "5.31.285",
4
4
  "description": "Gravity UI Page Constructor",
5
5
  "license": "MIT",
6
6
  "repository": {