@doyourjob/gravity-ui-page-constructor-addons 2.1.46 → 2.1.48

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 (48) hide show
  1. package/README.md +12 -0
  2. package/build/cjs/components/HeaderStripe/HeaderStripe.css +10 -0
  3. package/build/cjs/components/HeaderStripe/HeaderStripe.js +18 -7
  4. package/build/cjs/components/Navigation/containers/Footer/GroupLinks/GroupLinks.js +3 -4
  5. package/build/cjs/components/NewHeader/NewHeader.css +5 -0
  6. package/build/cjs/components/NewHeader/NewHeader.js +3 -2
  7. package/build/cjs/components/NewHeader/components/NHLoginButton/NHLoginButton.css +18 -0
  8. package/build/cjs/components/NewHeader/components/NHLoginButton/NHLoginButton.js +18 -6
  9. package/build/cjs/components/NewHeader/components/NHMobileNavigation/NHMobileNavigation.css +10 -0
  10. package/build/cjs/components/NewHeader/components/NHMobileNavigation/NHMobileNavigation.js +8 -3
  11. package/build/cjs/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.css +5 -0
  12. package/build/cjs/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.js +2 -1
  13. package/build/cjs/components/NewHeader/components/NHMobileNavigationPopup/NHMobileNavigationPopup.d.ts +2 -1
  14. package/build/cjs/components/NewHeader/components/NHMobileNavigationPopup/NHMobileNavigationPopup.js +2 -2
  15. package/build/cjs/components/NewHeader/components/NHNavigation/NHNavigation.d.ts +4 -1
  16. package/build/cjs/components/NewHeader/components/NHNavigation/NHNavigation.js +72 -9
  17. package/build/cjs/components/NewHeader/components/NHNavigationItem/NHNavigationItem.css +5 -0
  18. package/build/cjs/components/NewHeader/components/NHNavigationItem/NHNavigationItem.d.ts +3 -0
  19. package/build/cjs/components/NewHeader/components/NHNavigationItem/NHNavigationItem.js +12 -3
  20. package/build/cjs/components/NewHeader/components/NHNavigationPopup/NHNavigationPopup.css +13 -2
  21. package/build/cjs/components/NewHeader/components/NHNavigationPopup/NHNavigationPopup.js +58 -6
  22. package/build/cjs/components/NewHeader/components/NHNavigationPopup/keyboardNavigation.d.ts +4 -0
  23. package/build/cjs/components/NewHeader/components/NHNavigationPopup/keyboardNavigation.js +43 -0
  24. package/build/cjs/components/NewHeader/components/NHPopupItem/NHPopupItem.css +5 -0
  25. package/build/esm/components/HeaderStripe/HeaderStripe.css +10 -0
  26. package/build/esm/components/HeaderStripe/HeaderStripe.js +18 -7
  27. package/build/esm/components/Navigation/containers/Footer/GroupLinks/GroupLinks.js +3 -4
  28. package/build/esm/components/NewHeader/NewHeader.css +5 -0
  29. package/build/esm/components/NewHeader/NewHeader.js +3 -2
  30. package/build/esm/components/NewHeader/components/NHLoginButton/NHLoginButton.css +18 -0
  31. package/build/esm/components/NewHeader/components/NHLoginButton/NHLoginButton.js +19 -7
  32. package/build/esm/components/NewHeader/components/NHMobileNavigation/NHMobileNavigation.css +10 -0
  33. package/build/esm/components/NewHeader/components/NHMobileNavigation/NHMobileNavigation.js +9 -4
  34. package/build/esm/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.css +5 -0
  35. package/build/esm/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.js +2 -1
  36. package/build/esm/components/NewHeader/components/NHMobileNavigationPopup/NHMobileNavigationPopup.d.ts +2 -1
  37. package/build/esm/components/NewHeader/components/NHMobileNavigationPopup/NHMobileNavigationPopup.js +2 -2
  38. package/build/esm/components/NewHeader/components/NHNavigation/NHNavigation.d.ts +4 -1
  39. package/build/esm/components/NewHeader/components/NHNavigation/NHNavigation.js +72 -9
  40. package/build/esm/components/NewHeader/components/NHNavigationItem/NHNavigationItem.css +5 -0
  41. package/build/esm/components/NewHeader/components/NHNavigationItem/NHNavigationItem.d.ts +3 -0
  42. package/build/esm/components/NewHeader/components/NHNavigationItem/NHNavigationItem.js +12 -3
  43. package/build/esm/components/NewHeader/components/NHNavigationPopup/NHNavigationPopup.css +13 -2
  44. package/build/esm/components/NewHeader/components/NHNavigationPopup/NHNavigationPopup.js +58 -6
  45. package/build/esm/components/NewHeader/components/NHNavigationPopup/keyboardNavigation.d.ts +4 -0
  46. package/build/esm/components/NewHeader/components/NHNavigationPopup/keyboardNavigation.js +38 -0
  47. package/build/esm/components/NewHeader/components/NHPopupItem/NHPopupItem.css +5 -0
  48. package/package.json +2 -1
@@ -2,17 +2,69 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NHNavigationPopup = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const react_1 = tslib_1.__importDefault(require("react"));
5
+ const react_1 = tslib_1.__importStar(require("react"));
6
6
  const react_dom_1 = tslib_1.__importDefault(require("react-dom"));
7
- const FocusTrap_1 = require("../../../../utils/FocusTrap");
8
7
  const cn_1 = require("../../../../utils/cn");
8
+ const keyboardNavigation_1 = require("./keyboardNavigation");
9
9
  const b = (0, cn_1.block)('nh-navigation-popup');
10
+ const nextKeys = new Set(['ArrowDown', 'ArrowRight']);
11
+ const previousKeys = new Set(['ArrowUp', 'ArrowLeft']);
12
+ const getNextIndex = (currentIndex, offset, elementsCount) => {
13
+ if (currentIndex === -1) {
14
+ return offset > 0 ? 0 : elementsCount - 1;
15
+ }
16
+ return (currentIndex + offset + elementsCount) % elementsCount;
17
+ };
10
18
  const NHNavigationPopup = ({ headerRef, children, id }) => {
19
+ const popupRef = (0, react_1.useRef)(null);
20
+ const focusRelativeElement = (0, react_1.useCallback)((offset) => {
21
+ const popup = popupRef.current;
22
+ const focusableElements = (0, keyboardNavigation_1.getFocusableElements)(popup);
23
+ if (!popup || !focusableElements.length) {
24
+ return;
25
+ }
26
+ const activeElement = popup.ownerDocument.activeElement;
27
+ const currentIndex = activeElement ? focusableElements.indexOf(activeElement) : -1;
28
+ const nextIndex = getNextIndex(currentIndex, offset, focusableElements.length);
29
+ (0, keyboardNavigation_1.focusFromKeyboard)(focusableElements[nextIndex], popup);
30
+ }, []);
31
+ (0, react_1.useEffect)(() => {
32
+ const popup = popupRef.current;
33
+ if (!popup) {
34
+ return undefined;
35
+ }
36
+ const handleKeyDown = (event) => {
37
+ if (event.key === 'Tab') {
38
+ event.preventDefault();
39
+ focusRelativeElement(event.shiftKey ? -1 : 1);
40
+ }
41
+ else if (nextKeys.has(event.key)) {
42
+ event.preventDefault();
43
+ focusRelativeElement(1);
44
+ }
45
+ else if (previousKeys.has(event.key)) {
46
+ event.preventDefault();
47
+ focusRelativeElement(-1);
48
+ }
49
+ else if (event.key === 'Home') {
50
+ event.preventDefault();
51
+ const popupElement = popupRef.current;
52
+ (0, keyboardNavigation_1.focusFromKeyboard)((0, keyboardNavigation_1.getFocusableElements)(popupElement)[0], popupElement);
53
+ }
54
+ else if (event.key === 'End') {
55
+ event.preventDefault();
56
+ const popupElement = popupRef.current;
57
+ const focusableElements = (0, keyboardNavigation_1.getFocusableElements)(popupElement);
58
+ (0, keyboardNavigation_1.focusFromKeyboard)(focusableElements[focusableElements.length - 1], popupElement);
59
+ }
60
+ };
61
+ popup.addEventListener('keydown', handleKeyDown);
62
+ return () => {
63
+ popup.removeEventListener('keydown', handleKeyDown);
64
+ };
65
+ }, [focusRelativeElement]);
11
66
  return (headerRef === null || headerRef === void 0 ? void 0 : headerRef.current)
12
- ? react_dom_1.default.createPortal(react_1.default.createElement(FocusTrap_1.FocusTrap, { enabled: true },
13
- react_1.default.createElement("div", {
14
- // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
15
- tabIndex: 0, className: b(), id: id }, children)), headerRef.current)
67
+ ? react_dom_1.default.createPortal(react_1.default.createElement("div", { className: b(), id: id, ref: popupRef }, children), headerRef.current)
16
68
  : null;
17
69
  };
18
70
  exports.NHNavigationPopup = NHNavigationPopup;
@@ -0,0 +1,4 @@
1
+ export declare const keyboardFocusAttribute = "data-keyboard-focus";
2
+ export declare const focusableElementSelector: string;
3
+ export declare const getFocusableElements: (root?: HTMLElement | null) => HTMLElement[];
4
+ export declare const focusFromKeyboard: (element?: HTMLElement | null, root?: ParentNode | null) => void;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.focusFromKeyboard = exports.getFocusableElements = exports.focusableElementSelector = exports.keyboardFocusAttribute = void 0;
4
+ exports.keyboardFocusAttribute = 'data-keyboard-focus';
5
+ exports.focusableElementSelector = [
6
+ 'a[href]',
7
+ 'button:not([disabled])',
8
+ 'input:not([disabled])',
9
+ 'select:not([disabled])',
10
+ 'textarea:not([disabled])',
11
+ '[tabindex]:not([tabindex="-1"])',
12
+ ].join(',');
13
+ const keyboardFocusSelector = `[${exports.keyboardFocusAttribute}='true']`;
14
+ const blurHandlers = new WeakMap();
15
+ const clearKeyboardFocus = (root) => {
16
+ root.querySelectorAll(keyboardFocusSelector).forEach((element) => {
17
+ const blurHandler = blurHandlers.get(element);
18
+ if (blurHandler) {
19
+ element.removeEventListener('blur', blurHandler);
20
+ blurHandlers.delete(element);
21
+ }
22
+ element.removeAttribute(exports.keyboardFocusAttribute);
23
+ });
24
+ };
25
+ const getFocusableElements = (root) => Array.from((root === null || root === void 0 ? void 0 : root.querySelectorAll(exports.focusableElementSelector)) || []);
26
+ exports.getFocusableElements = getFocusableElements;
27
+ const focusFromKeyboard = (element, root) => {
28
+ if (!element) {
29
+ return;
30
+ }
31
+ const focusRoot = root || element.ownerDocument;
32
+ const handleBlur = () => {
33
+ element.removeAttribute(exports.keyboardFocusAttribute);
34
+ element.removeEventListener('blur', handleBlur);
35
+ blurHandlers.delete(element);
36
+ };
37
+ clearKeyboardFocus(focusRoot);
38
+ element.setAttribute(exports.keyboardFocusAttribute, 'true');
39
+ element.addEventListener('blur', handleBlur);
40
+ blurHandlers.set(element, handleBlur);
41
+ element.focus({ preventScroll: true });
42
+ };
43
+ exports.focusFromKeyboard = focusFromKeyboard;
@@ -57,6 +57,11 @@ unpredictable css rules order in build */
57
57
  opacity: 1;
58
58
  }
59
59
 
60
+ .pc-addons-nh-navigation-popup-item:focus, .pc-addons-nh-navigation-popup-item:focus-visible, .pc-addons-nh-navigation-popup-item[data-keyboard-focus=true] {
61
+ outline: 2px solid var(--g-color-line-focus);
62
+ outline-offset: 4px;
63
+ }
64
+
60
65
  .pc-addons-nh-navigation-popup-item_disable {
61
66
  pointer-events: none;
62
67
  }
@@ -122,6 +122,11 @@ unpredictable css rules order in build */
122
122
  text-decoration: none;
123
123
  }
124
124
 
125
+ .pc-addons-header-stripe__item-link:focus-visible {
126
+ outline: 2px solid var(--g-color-line-focus);
127
+ outline-offset: -2px;
128
+ }
129
+
125
130
  @media (min-width: 769px) {
126
131
  .pc-addons-header-stripe__item-link {
127
132
  overflow: hidden;
@@ -147,6 +152,11 @@ unpredictable css rules order in build */
147
152
  opacity: 0.6;
148
153
  }
149
154
 
155
+ .pc-addons-header-stripe__close:focus-visible {
156
+ outline: 2px solid var(--g-color-line-focus);
157
+ outline-offset: -2px;
158
+ }
159
+
150
160
  @keyframes expand-mobile {
151
161
  from {
152
162
  max-height: 0;
@@ -7,12 +7,12 @@ import { CloseIcon } from './CloseIcon';
7
7
  import i18n from './i18n';
8
8
  import './HeaderStripe.css';
9
9
  const b = block('header-stripe');
10
- const renderItemContent = (item) => {
10
+ const renderItemContent = (item, isActive) => {
11
11
  if (typeof item === 'string') {
12
12
  return item;
13
13
  }
14
14
  else if (item.link) {
15
- return (React.createElement("a", { className: b('item-link'), href: item.link, target: item.target }, item.text));
15
+ return (React.createElement("a", { className: b('item-link'), href: item.link, target: item.target, tabIndex: isActive ? undefined : -1 }, item.text));
16
16
  }
17
17
  else {
18
18
  return item.text;
@@ -21,6 +21,7 @@ const renderItemContent = (item) => {
21
21
  export const HeaderStripe = ({ duration = 8000, items, textColor, background, backgroundImage, onlyDesktop, canClose, onClose, newDesign, }) => {
22
22
  const [activeIndex, setActiveIndex] = useState(0);
23
23
  const [isClosing, setIsClosing] = useState(false);
24
+ const [isFocusInside, setIsFocusInside] = useState(false);
24
25
  const isMobile = useMobile();
25
26
  const filteredItems = useMemo(() => isMobile
26
27
  ? items.filter((item) => (typeof item === 'object' ? !item.onlyDesktop : true))
@@ -29,13 +30,23 @@ export const HeaderStripe = ({ duration = 8000, items, textColor, background, ba
29
30
  setIsClosing(true);
30
31
  onClose === null || onClose === void 0 ? void 0 : onClose();
31
32
  }, [onClose]);
33
+ const handleFocus = useCallback(() => {
34
+ setIsFocusInside(true);
35
+ }, []);
36
+ const handleBlur = useCallback((event) => {
37
+ const nextFocusedElement = event.relatedTarget;
38
+ if (!(nextFocusedElement instanceof Node) ||
39
+ !event.currentTarget.contains(nextFocusedElement)) {
40
+ setIsFocusInside(false);
41
+ }
42
+ }, []);
32
43
  useEffect(() => {
33
44
  if (filteredItems.length > 0) {
34
45
  setActiveIndex(0);
35
46
  }
36
47
  }, [filteredItems.length]);
37
48
  useEffect(() => {
38
- if (filteredItems.length < 1)
49
+ if (filteredItems.length < 2 || isFocusInside)
39
50
  return undefined;
40
51
  const interval = setInterval(() => {
41
52
  setActiveIndex((prevIndex) => (prevIndex + 1) % filteredItems.length);
@@ -43,7 +54,7 @@ export const HeaderStripe = ({ duration = 8000, items, textColor, background, ba
43
54
  return () => {
44
55
  clearInterval(interval);
45
56
  };
46
- }, [duration, filteredItems.length]);
57
+ }, [duration, filteredItems.length, isFocusInside]);
47
58
  const rootStyle = useMemo(() => {
48
59
  const properties = {};
49
60
  if (background) {
@@ -78,7 +89,7 @@ export const HeaderStripe = ({ duration = 8000, items, textColor, background, ba
78
89
  }), style: rootStyle },
79
90
  React.createElement(Grid, null,
80
91
  React.createElement(Col, null,
81
- React.createElement("div", { className: b('content', { 'with-close': Boolean(onClose) }), style: contentStyle },
92
+ React.createElement("div", { className: b('content', { 'with-close': Boolean(onClose) }), style: contentStyle, onFocus: handleFocus, onBlur: handleBlur },
82
93
  filteredItems.map((item, index) => {
83
94
  const isActive = index === activeIndex;
84
95
  const isPrev = filteredItems.length > 1 &&
@@ -89,8 +100,8 @@ export const HeaderStripe = ({ duration = 8000, items, textColor, background, ba
89
100
  return (React.createElement("div", { key: String(index), className: b('item', {
90
101
  active: isActive,
91
102
  prev: isPrev,
92
- }) },
93
- React.createElement("div", { className: b('item-content') }, renderItemContent(item))));
103
+ }), "aria-hidden": !isActive },
104
+ React.createElement("div", { className: b('item-content') }, renderItemContent(item, isActive))));
94
105
  }),
95
106
  canClose && (React.createElement("button", { "aria-label": i18n('close'), className: b('close'), onClick: handleClose },
96
107
  React.createElement(Icon, { data: newDesign ? CloseIcon : Xmark, className: b('close-icon'), size: 16 }))))))));
@@ -5,9 +5,8 @@ import './GroupLinks.css';
5
5
  const b = block('group-links');
6
6
  const GroupLinks = ({ columnGroup: { title, items }, className }) => {
7
7
  return (React.createElement("div", { className: b(null, className), key: title },
8
- React.createElement("ul", { className: b('group') },
9
- title && React.createElement("h5", { className: b('group-title') }, title),
10
- items.map((item) => (React.createElement("li", { key: item.title, className: b('item-wrapper') },
11
- React.createElement(EnrichedLink, Object.assign({ className: b('item') }, item))))))));
8
+ title && React.createElement("h5", { className: b('group-title') }, title),
9
+ React.createElement("ul", { className: b('group'), "aria-label": title }, items.map((item) => (React.createElement("li", { key: item.title, className: b('item-wrapper') },
10
+ React.createElement(EnrichedLink, Object.assign({ className: b('item') }, item))))))));
12
11
  };
13
12
  export default GroupLinks;
@@ -172,6 +172,11 @@ unpredictable css rules order in build */
172
172
  border-color: rgba(5, 43, 66, 0.25);
173
173
  }
174
174
 
175
+ .pc-addons-new-header__button:focus-visible {
176
+ outline: 2px solid var(--g-color-line-focus);
177
+ outline-offset: 2px;
178
+ }
179
+
175
180
  @media (max-width: 1081px) {
176
181
  .pc-addons-new-header__left {
177
182
  margin-right: 0;
@@ -16,6 +16,7 @@ export const NewHeader = ({ data, setupRouteChangeHandler, renderSearch, classNa
16
16
  const [isSearchMode, setIsSearchMode] = useState(false);
17
17
  const [isMobileNavigationOpen, setIsMobileNavigationOpen] = useState(false);
18
18
  const [pageHasScroll, setPageHasScroll] = useState(false);
19
+ const [activeNavigationId, setActiveNavigationId] = useState(null);
19
20
  const showButtonsContainer = Boolean(buttons);
20
21
  const toggleSearch = useCallback((isActive) => setIsSearchMode(isActive), []);
21
22
  const toggleMobileNavigationPopup = useCallback((isOpened) => {
@@ -55,10 +56,10 @@ export const NewHeader = ({ data, setupRouteChangeHandler, renderSearch, classNa
55
56
  React.createElement("div", { className: b('left') },
56
57
  logo && React.createElement(NHLogo, { data: logo }),
57
58
  left ? (React.createElement("div", { className: b('navigation') },
58
- React.createElement(NHNavigation, { data: left, headerRef: headerRef }))) : null),
59
+ React.createElement(NHNavigation, { activeNavigationId: activeNavigationId, data: left, headerRef: headerRef, navigationId: "left", setActiveNavigationId: setActiveNavigationId }))) : null),
59
60
  React.createElement("div", { className: b('right') },
60
61
  right ? (React.createElement("div", { className: b('navigation') },
61
- React.createElement(NHNavigation, { data: right, headerRef: headerRef }))) : null,
62
+ React.createElement(NHNavigation, { activeNavigationId: activeNavigationId, data: right, headerRef: headerRef, navigationId: "right", setActiveNavigationId: setActiveNavigationId }))) : null,
62
63
  React.createElement("div", { className: b('wrap') },
63
64
  renderSearch && (React.createElement("div", { className: b('icons-container') }, renderSearch({ onActiveToggle: toggleSearch }))),
64
65
  React.createElement("div", { className: b('buttons') },
@@ -1,6 +1,19 @@
1
1
  /* use this for style redefinitions to awoid problems with
2
2
  unpredictable css rules order in build */
3
+ .pc-addons-nh-login-button__wrapper {
4
+ display: flex;
5
+ }
6
+
3
7
  .pc-addons-nh-login-button {
8
+ display: inline-block;
9
+ margin: 0;
10
+ padding: 0;
11
+ font: inherit;
12
+ border: none;
13
+ outline: none;
14
+ color: inherit;
15
+ background: none;
16
+ cursor: pointer;
4
17
  display: flex;
5
18
  align-items: center;
6
19
  height: 36px;
@@ -17,4 +30,9 @@ unpredictable css rules order in build */
17
30
 
18
31
  .pc-addons-nh-login-button:hover {
19
32
  background: var(--g-color-base-brand-hover);
33
+ }
34
+
35
+ .pc-addons-nh-login-button:focus-visible {
36
+ outline: 2px solid var(--g-color-line-focus);
37
+ outline-offset: 2px;
20
38
  }
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useEffect, useState } from 'react';
2
- import { Icon } from '@gravity-ui/uikit';
2
+ import { Icon, useUniqId } from '@gravity-ui/uikit';
3
3
  import { block } from '../../../../utils/cn';
4
4
  import { SWITCH_MENU_TAB_TIMEOUT } from '../../constants';
5
5
  import { ChevronDown } from '../ChevronDown';
@@ -12,19 +12,30 @@ export const NHLoginButton = ({ data, headerRef, setupRouteChangeHandler }) => {
12
12
  const [isActive, setIsActive] = useState(false);
13
13
  const [pretendentActiveTab, setPretendentAciveTab] = useState(false);
14
14
  const [previouslyFocusedElement, setPreviouslyFocusedElement] = useState(null);
15
+ const popupId = useUniqId();
15
16
  const handleActiveTab = useCallback((val) => {
16
17
  setPreviouslyFocusedElement(document.activeElement);
17
18
  setPretendentAciveTab(val);
18
19
  }, []);
19
20
  const onEscapeKeyDown = useCallback((event) => {
20
- if (event.key === 'Escape') {
21
+ if (event.key === 'Escape' && isActive) {
21
22
  setIsActive(false);
22
23
  setPretendentAciveTab(false);
23
24
  previouslyFocusedElement === null || previouslyFocusedElement === void 0 ? void 0 : previouslyFocusedElement.focus({ preventScroll: true });
24
25
  }
25
- }, [previouslyFocusedElement]);
26
+ }, [isActive, previouslyFocusedElement]);
26
27
  const handleMouseEnter = useCallback(() => handleActiveTab(true), [handleActiveTab]);
27
28
  const handleMouseLeave = useCallback(() => handleActiveTab(false), [handleActiveTab]);
29
+ const handleToggle = useCallback(() => {
30
+ const focusedElement = document.activeElement;
31
+ const nextIsActive = !isActive;
32
+ setPreviouslyFocusedElement(focusedElement);
33
+ setPretendentAciveTab(nextIsActive);
34
+ setIsActive(nextIsActive);
35
+ if (!nextIsActive) {
36
+ focusedElement === null || focusedElement === void 0 ? void 0 : focusedElement.focus({ preventScroll: true });
37
+ }
38
+ }, [isActive]);
28
39
  useEffect(() => {
29
40
  const timerId = setTimeout(() => {
30
41
  setIsActive(pretendentActiveTab);
@@ -40,9 +51,10 @@ export const NHLoginButton = ({ data, headerRef, setupRouteChangeHandler }) => {
40
51
  useEffect(() => setupRouteChangeHandler === null || setupRouteChangeHandler === void 0 ? void 0 : setupRouteChangeHandler(() => {
41
52
  handleMouseLeave();
42
53
  }), [handleMouseLeave, setupRouteChangeHandler]);
43
- return (React.createElement("div", { className: b(), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
44
- data.text,
45
- React.createElement(Icon, { data: isActive ? ChevronUp : ChevronDown, size: 16 }),
46
- isActive && (React.createElement(NHNavigationPopup, { headerRef: headerRef },
54
+ return (React.createElement("div", { className: b('wrapper'), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
55
+ React.createElement("button", { className: b(), type: "button", onClick: handleToggle, "aria-expanded": isActive, "aria-controls": popupId },
56
+ data.text,
57
+ React.createElement(Icon, { data: isActive ? ChevronUp : ChevronDown, size: 16 })),
58
+ isActive && (React.createElement(NHNavigationPopup, { headerRef: headerRef, id: popupId },
47
59
  React.createElement(NHLoginPopup, Object.assign({}, data))))));
48
60
  };
@@ -29,6 +29,11 @@ unpredictable css rules order in build */
29
29
  display: none;
30
30
  }
31
31
 
32
+ .pc-addons-nh-mobile-navigation__icon:focus-visible {
33
+ outline: 2px solid var(--g-color-line-focus);
34
+ outline-offset: 2px;
35
+ }
36
+
32
37
  .pc-addons-nh-mobile-navigation__header {
33
38
  position: sticky;
34
39
  top: 0;
@@ -48,6 +53,11 @@ unpredictable css rules order in build */
48
53
  gap: 8px;
49
54
  }
50
55
 
56
+ .pc-addons-nh-mobile-navigation__close-button:focus-visible {
57
+ outline: 2px solid var(--g-color-line-focus);
58
+ outline-offset: 2px;
59
+ }
60
+
51
61
  .pc-addons-nh-mobile-navigation__search-container {
52
62
  display: flex;
53
63
  align-items: center;
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Button as PCButton } from '@doyourjob/gravity-ui-page-constructor';
3
3
  import { Bars, Xmark } from '@gravity-ui/icons';
4
- import { Button, Icon } from '@gravity-ui/uikit';
4
+ import { Button, Icon, useUniqId } from '@gravity-ui/uikit';
5
5
  import { block } from '../../../../utils/cn';
6
6
  import { NHLogo } from '../NHLogo/NHLogo';
7
7
  import { NHMobileNavigationItem } from '../NHMobileNavigationItem/NHMobileNavigationItem';
@@ -11,17 +11,22 @@ import './NHMobileNavigation.css';
11
11
  const b = block('nh-mobile-navigation');
12
12
  export const NHMobileNavigation = ({ isOpened, toogleOpen, isSearchOpen, data, onMenuScroll, }) => {
13
13
  var _a, _b;
14
+ const popupId = useUniqId();
14
15
  return (React.createElement("div", { className: b() },
15
- React.createElement(Button, { view: "flat", size: "l", className: b('icon', { hidden: isSearchOpen }), onClick: (e) => {
16
+ React.createElement(Button, { view: "flat", size: "l", className: b('icon', { hidden: isSearchOpen }), extraProps: {
17
+ 'aria-label': isOpened ? 'Close navigation menu' : 'Open navigation menu',
18
+ 'aria-expanded': isOpened,
19
+ 'aria-controls': popupId,
20
+ }, onClick: (e) => {
16
21
  e.stopPropagation();
17
22
  toogleOpen(!isOpened);
18
23
  } },
19
24
  React.createElement(Icon, { data: isOpened ? Xmark : Bars, size: 24 })),
20
- React.createElement(NHMobileNavigationPopup, { isOpened: isOpened, onClose: () => toogleOpen(false), onMenuScroll: onMenuScroll },
25
+ React.createElement(NHMobileNavigationPopup, { id: popupId, isOpened: isOpened, onClose: () => toogleOpen(false), onMenuScroll: onMenuScroll },
21
26
  React.createElement("div", { className: b('header') },
22
27
  (data === null || data === void 0 ? void 0 : data.logo) && React.createElement(NHLogo, { data: data.logo }),
23
28
  React.createElement("div", { className: b('header-right') },
24
- React.createElement(Button, { view: "flat", size: "l", className: b('close-button'), onClick: () => toogleOpen(false) },
29
+ React.createElement(Button, { view: "flat", size: "l", className: b('close-button'), extraProps: { 'aria-label': 'Close navigation menu' }, onClick: () => toogleOpen(false) },
25
30
  React.createElement(Icon, { data: Xmark, size: 24 })))),
26
31
  React.createElement("nav", { className: b('nav') },
27
32
  React.createElement("ul", { className: b('list') }, (_a = data === null || data === void 0 ? void 0 : data.left) === null || _a === void 0 ? void 0 : _a.map((item, index) => (React.createElement("li", { className: b('item'), key: index },
@@ -8,6 +8,11 @@ unpredictable css rules order in build */
8
8
  align-items: center;
9
9
  }
10
10
 
11
+ .pc-addons-nh-mobile-navigation-item:focus-visible {
12
+ outline: 2px solid var(--g-color-line-focus);
13
+ outline-offset: 2px;
14
+ }
15
+
11
16
  .pc-addons-nh-mobile-navigation-item_type_link {
12
17
  color: inherit;
13
18
  text-decoration: none;
@@ -12,6 +12,7 @@ export const NHMobileNavigationItem = ({ item }) => {
12
12
  }, [isOpened]);
13
13
  const handleKeyDown = useCallback((e) => {
14
14
  if (e.key === 'Enter' || e.key === ' ') {
15
+ e.preventDefault();
15
16
  toggleOpen();
16
17
  }
17
18
  }, [toggleOpen]);
@@ -20,7 +21,7 @@ export const NHMobileNavigationItem = ({ item }) => {
20
21
  return (React.createElement("a", Object.assign({ href: item.data.url, className: b({ type: 'link' }) }, getLinkProps(item.data.url || '', undefined, item.data.target)), item.title));
21
22
  }
22
23
  return (React.createElement(Fragment, null,
23
- React.createElement("div", { className: b(), onClick: toggleOpen, role: "button", tabIndex: 0, onKeyDown: handleKeyDown },
24
+ React.createElement("div", { className: b(), onClick: toggleOpen, role: "button", tabIndex: 0, "aria-expanded": isOpened, onKeyDown: handleKeyDown },
24
25
  React.createElement("div", { className: b('text') }, item.title),
25
26
  React.createElement("div", { className: b('arrow') },
26
27
  React.createElement(ToggleArrow, { size: 12, type: "vertical", open: isOpened, iconType: "navigation" }))),
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
2
  import './NHMobileNavigationPopup.css';
3
3
  interface MobileNavigationPopupProps {
4
+ id?: string;
4
5
  isOpened: boolean;
5
6
  onClose: () => void;
6
7
  children: React.ReactNode;
7
8
  onMenuScroll: (scrollTop: number) => void;
8
9
  }
9
- export declare const NHMobileNavigationPopup: ({ isOpened, onClose, children, onMenuScroll, }: MobileNavigationPopupProps) => React.ReactPortal | null;
10
+ export declare const NHMobileNavigationPopup: ({ id, isOpened, onClose, children, onMenuScroll, }: MobileNavigationPopupProps) => React.ReactPortal | null;
10
11
  export {};
@@ -7,7 +7,7 @@ import { block } from '../../../../utils/cn';
7
7
  import './NHMobileNavigationPopup.css';
8
8
  const b = block('nh-mobile-navigation-popup');
9
9
  const TRANSITION_TIME = 400;
10
- export const NHMobileNavigationPopup = ({ isOpened, onClose, children, onMenuScroll, }) => {
10
+ export const NHMobileNavigationPopup = ({ id, isOpened, onClose, children, onMenuScroll, }) => {
11
11
  const [body, setBody] = useState();
12
12
  const ref = useRef(null);
13
13
  const handleScroll = useCallback(() => {
@@ -35,5 +35,5 @@ export const NHMobileNavigationPopup = ({ isOpened, onClose, children, onMenuScr
35
35
  }
36
36
  return ReactDOM.createPortal(React.createElement(CSSTransition, { in: isOpened, classNames: b('transition'), unmountOnExit: true, timeout: TRANSITION_TIME },
37
37
  React.createElement(OutsideClick, { className: b(), onOutsideClick: onClose },
38
- React.createElement("div", { ref: ref, className: b('container') }, children))), body);
38
+ React.createElement("div", { ref: ref, className: b('container'), id: id }, children))), body);
39
39
  };
@@ -3,9 +3,12 @@ import React from 'react';
3
3
  import { NHNavigationItemData, SetupRouteChangeHandler } from '../../models';
4
4
  import './NHNavigation.css';
5
5
  interface NavigationProps {
6
+ activeNavigationId: string | null;
6
7
  data: NHNavigationItemData[];
7
8
  headerRef?: RefObject<HTMLDivElement>;
9
+ navigationId: string;
8
10
  setupRouteChangeHandler?: SetupRouteChangeHandler;
11
+ setActiveNavigationId: (navigationId: string | null) => void;
9
12
  }
10
- export declare const NHNavigation: ({ data, headerRef, setupRouteChangeHandler }: NavigationProps) => React.JSX.Element;
13
+ export declare const NHNavigation: ({ activeNavigationId, data, headerRef, navigationId, setupRouteChangeHandler, setActiveNavigationId, }: NavigationProps) => React.JSX.Element;
11
14
  export {};
@@ -5,6 +5,7 @@ import { NHNavigationItemType } from '../../models';
5
5
  import { NHDefaultPopup } from '../NHDefaultPopup/NHDefaultPopup';
6
6
  import { NHNavigationItem } from '../NHNavigationItem/NHNavigationItem';
7
7
  import { NHNavigationPopup } from '../NHNavigationPopup/NHNavigationPopup';
8
+ import { focusFromKeyboard, getFocusableElements } from '../NHNavigationPopup/keyboardNavigation';
8
9
  import './NHNavigation.css';
9
10
  const b = block('nh-navigation');
10
11
  const tooltipPrefixId = 'navigation-item-key';
@@ -16,36 +17,98 @@ const getPopupContent = (sectionData) => {
16
17
  return null;
17
18
  }
18
19
  };
19
- export const NHNavigation = ({ data, headerRef, setupRouteChangeHandler }) => {
20
+ export const NHNavigation = ({ activeNavigationId, data, headerRef, navigationId, setupRouteChangeHandler, setActiveNavigationId, }) => {
20
21
  const [activeTab, setActiveTab] = useState(NO_MENU_TAB_SELECTED);
21
22
  const [pretendentActiveTab, setPretendentAciveTab] = useState(NO_MENU_TAB_SELECTED);
22
23
  const [previouslyFocusedElement, setPreviouslyFocusedElement] = useState(null);
24
+ const [popupTabToFocus, setPopupTabToFocus] = useState(null);
25
+ const setNavigationTab = useCallback((currentIndex) => {
26
+ setPretendentAciveTab(currentIndex);
27
+ setActiveTab(currentIndex);
28
+ setPopupTabToFocus(null);
29
+ setActiveNavigationId(currentIndex === NO_MENU_TAB_SELECTED ? null : navigationId);
30
+ }, [navigationId, setActiveNavigationId]);
23
31
  const handleActiveTab = useCallback((currentIndex) => {
24
32
  setPreviouslyFocusedElement(document.activeElement);
25
33
  setPretendentAciveTab(currentIndex);
26
34
  }, []);
35
+ const handleToggleTab = useCallback((currentIndex) => {
36
+ const focusedElement = document.activeElement;
37
+ const nextActiveTab = activeTab === currentIndex ? NO_MENU_TAB_SELECTED : currentIndex;
38
+ setPreviouslyFocusedElement(focusedElement);
39
+ setNavigationTab(nextActiveTab);
40
+ if (nextActiveTab === NO_MENU_TAB_SELECTED) {
41
+ focusedElement === null || focusedElement === void 0 ? void 0 : focusedElement.focus({ preventScroll: true });
42
+ }
43
+ }, [activeTab, setNavigationTab]);
44
+ const handleFocusTabPopup = useCallback((currentIndex) => {
45
+ setPreviouslyFocusedElement(document.activeElement);
46
+ setPretendentAciveTab(currentIndex);
47
+ setActiveTab(currentIndex);
48
+ setPopupTabToFocus(currentIndex);
49
+ setActiveNavigationId(navigationId);
50
+ }, [navigationId, setActiveNavigationId]);
51
+ const handleFocusTab = useCallback((currentIndex) => {
52
+ if (activeNavigationId === null) {
53
+ return;
54
+ }
55
+ setPreviouslyFocusedElement(document.activeElement);
56
+ setNavigationTab(currentIndex);
57
+ }, [activeNavigationId, setNavigationTab]);
58
+ const handleCloseTab = useCallback(() => {
59
+ setNavigationTab(NO_MENU_TAB_SELECTED);
60
+ previouslyFocusedElement === null || previouslyFocusedElement === void 0 ? void 0 : previouslyFocusedElement.focus({ preventScroll: true });
61
+ }, [previouslyFocusedElement, setNavigationTab]);
62
+ const handleBlur = useCallback((event) => {
63
+ if (activeTab === NO_MENU_TAB_SELECTED) {
64
+ return;
65
+ }
66
+ const nextFocusedElement = event.relatedTarget;
67
+ const popup = document.getElementById(`${tooltipPrefixId}-${activeTab}`);
68
+ if (nextFocusedElement &&
69
+ (event.currentTarget.contains(nextFocusedElement) ||
70
+ (popup === null || popup === void 0 ? void 0 : popup.contains(nextFocusedElement)))) {
71
+ return;
72
+ }
73
+ setNavigationTab(NO_MENU_TAB_SELECTED);
74
+ }, [activeTab, setNavigationTab]);
27
75
  const onEscapeKeyDown = useCallback((event) => {
28
- if (event.key === 'Escape') {
29
- setActiveTab(NO_MENU_TAB_SELECTED);
30
- setPretendentAciveTab(NO_MENU_TAB_SELECTED);
31
- previouslyFocusedElement === null || previouslyFocusedElement === void 0 ? void 0 : previouslyFocusedElement.focus({ preventScroll: true });
76
+ if (event.key === 'Escape' && activeTab !== NO_MENU_TAB_SELECTED) {
77
+ handleCloseTab();
32
78
  }
33
- }, [previouslyFocusedElement]);
79
+ }, [activeTab, handleCloseTab]);
34
80
  useEffect(() => setupRouteChangeHandler === null || setupRouteChangeHandler === void 0 ? void 0 : setupRouteChangeHandler(() => {
35
81
  handleActiveTab(NO_MENU_TAB_SELECTED);
36
82
  }), [setupRouteChangeHandler, handleActiveTab]);
37
83
  useEffect(() => {
38
84
  const timerId = setTimeout(() => {
39
85
  setActiveTab(pretendentActiveTab);
86
+ setActiveNavigationId(pretendentActiveTab === NO_MENU_TAB_SELECTED ? null : navigationId);
40
87
  }, SWITCH_MENU_TAB_TIMEOUT);
41
88
  return () => clearTimeout(timerId);
42
- }, [activeTab, pretendentActiveTab]);
89
+ }, [activeTab, navigationId, pretendentActiveTab, setActiveNavigationId]);
90
+ useEffect(() => {
91
+ if (activeNavigationId !== navigationId && activeTab !== NO_MENU_TAB_SELECTED) {
92
+ setActiveTab(NO_MENU_TAB_SELECTED);
93
+ setPretendentAciveTab(NO_MENU_TAB_SELECTED);
94
+ setPopupTabToFocus(null);
95
+ }
96
+ }, [activeNavigationId, activeTab, navigationId]);
97
+ useEffect(() => {
98
+ if (popupTabToFocus === null || activeTab !== popupTabToFocus) {
99
+ return;
100
+ }
101
+ const popup = document.getElementById(`${tooltipPrefixId}-${popupTabToFocus}`);
102
+ const [firstFocusableElement] = getFocusableElements(popup);
103
+ focusFromKeyboard(firstFocusableElement, popup);
104
+ setPopupTabToFocus(null);
105
+ }, [activeTab, popupTabToFocus]);
43
106
  useEffect(() => {
44
107
  document.addEventListener('keydown', onEscapeKeyDown);
45
108
  return () => {
46
109
  document.removeEventListener('keydown', onEscapeKeyDown);
47
110
  };
48
111
  }, [onEscapeKeyDown]);
49
- return (React.createElement("nav", null,
50
- React.createElement("ul", { className: b() }, data.map((item, i) => (React.createElement(NHNavigationItem, { key: i, item: item, handleActiveTab: handleActiveTab, index: i, isActive: activeTab === i, tooltipId: `${tooltipPrefixId}-${i}` }, activeTab === i && item.type !== NHNavigationItemType.Link && (React.createElement(NHNavigationPopup, { headerRef: headerRef, id: `${tooltipPrefixId}-${i}` }, getPopupContent(item)))))))));
112
+ return (React.createElement("nav", { onBlur: handleBlur },
113
+ React.createElement("ul", { className: b() }, data.map((item, i) => (React.createElement(NHNavigationItem, { key: i, item: item, handleActiveTab: handleActiveTab, handleFocusTabPopup: handleFocusTabPopup, handleFocusTab: handleFocusTab, handleToggleTab: handleToggleTab, index: i, isActive: activeTab === i, tooltipId: `${tooltipPrefixId}-${i}` }, activeTab === i && item.type !== NHNavigationItemType.Link && (React.createElement(NHNavigationPopup, { headerRef: headerRef, id: `${tooltipPrefixId}-${i}` }, getPopupContent(item)))))))));
51
114
  };
@@ -55,6 +55,11 @@ unpredictable css rules order in build */
55
55
  cursor: default;
56
56
  }
57
57
 
58
+ .pc-addons-nh-navigation-item__text:focus-visible {
59
+ outline: 2px solid var(--g-color-line-focus);
60
+ outline-offset: 2px;
61
+ }
62
+
58
63
  .pc-addons-nh-navigation-item:first-child .pc-addons-nh-navigation-item__text {
59
64
  margin-left: 0;
60
65
  }