@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
package/README.md CHANGED
@@ -10,6 +10,18 @@
10
10
  npm install @gravity-ui/page-constructor-addons
11
11
  ```
12
12
 
13
+ ## Release patch
14
+
15
+ Run from a clean branch with an upstream configured:
16
+
17
+ ```shell
18
+ pnpm bump-patch-release
19
+ ```
20
+
21
+ The script fetches the upstream remote, refuses to continue if upstream has commits missing
22
+ locally, bumps the package patch version, commits `chore: release vX.Y.Z`, creates the
23
+ `vX.Y.Z` tag, then pushes the commit and tag.
24
+
13
25
  ## Development
14
26
 
15
27
  ```bash
@@ -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;
@@ -10,12 +10,12 @@ const cn_1 = require("../../utils/cn");
10
10
  const CloseIcon_1 = require("./CloseIcon");
11
11
  const i18n_1 = tslib_1.__importDefault(require("./i18n"));
12
12
  const b = (0, cn_1.block)('header-stripe');
13
- const renderItemContent = (item) => {
13
+ const renderItemContent = (item, isActive) => {
14
14
  if (typeof item === 'string') {
15
15
  return item;
16
16
  }
17
17
  else if (item.link) {
18
- return (react_1.default.createElement("a", { className: b('item-link'), href: item.link, target: item.target }, item.text));
18
+ return (react_1.default.createElement("a", { className: b('item-link'), href: item.link, target: item.target, tabIndex: isActive ? undefined : -1 }, item.text));
19
19
  }
20
20
  else {
21
21
  return item.text;
@@ -24,6 +24,7 @@ const renderItemContent = (item) => {
24
24
  const HeaderStripe = ({ duration = 8000, items, textColor, background, backgroundImage, onlyDesktop, canClose, onClose, newDesign, }) => {
25
25
  const [activeIndex, setActiveIndex] = (0, react_1.useState)(0);
26
26
  const [isClosing, setIsClosing] = (0, react_1.useState)(false);
27
+ const [isFocusInside, setIsFocusInside] = (0, react_1.useState)(false);
27
28
  const isMobile = (0, uikit_1.useMobile)();
28
29
  const filteredItems = (0, react_1.useMemo)(() => isMobile
29
30
  ? items.filter((item) => (typeof item === 'object' ? !item.onlyDesktop : true))
@@ -32,13 +33,23 @@ const HeaderStripe = ({ duration = 8000, items, textColor, background, backgroun
32
33
  setIsClosing(true);
33
34
  onClose === null || onClose === void 0 ? void 0 : onClose();
34
35
  }, [onClose]);
36
+ const handleFocus = (0, react_1.useCallback)(() => {
37
+ setIsFocusInside(true);
38
+ }, []);
39
+ const handleBlur = (0, react_1.useCallback)((event) => {
40
+ const nextFocusedElement = event.relatedTarget;
41
+ if (!(nextFocusedElement instanceof Node) ||
42
+ !event.currentTarget.contains(nextFocusedElement)) {
43
+ setIsFocusInside(false);
44
+ }
45
+ }, []);
35
46
  (0, react_1.useEffect)(() => {
36
47
  if (filteredItems.length > 0) {
37
48
  setActiveIndex(0);
38
49
  }
39
50
  }, [filteredItems.length]);
40
51
  (0, react_1.useEffect)(() => {
41
- if (filteredItems.length < 1)
52
+ if (filteredItems.length < 2 || isFocusInside)
42
53
  return undefined;
43
54
  const interval = setInterval(() => {
44
55
  setActiveIndex((prevIndex) => (prevIndex + 1) % filteredItems.length);
@@ -46,7 +57,7 @@ const HeaderStripe = ({ duration = 8000, items, textColor, background, backgroun
46
57
  return () => {
47
58
  clearInterval(interval);
48
59
  };
49
- }, [duration, filteredItems.length]);
60
+ }, [duration, filteredItems.length, isFocusInside]);
50
61
  const rootStyle = (0, react_1.useMemo)(() => {
51
62
  const properties = {};
52
63
  if (background) {
@@ -81,7 +92,7 @@ const HeaderStripe = ({ duration = 8000, items, textColor, background, backgroun
81
92
  }), style: rootStyle },
82
93
  react_1.default.createElement(gravity_ui_page_constructor_1.Grid, null,
83
94
  react_1.default.createElement(gravity_ui_page_constructor_1.Col, null,
84
- react_1.default.createElement("div", { className: b('content', { 'with-close': Boolean(onClose) }), style: contentStyle },
95
+ react_1.default.createElement("div", { className: b('content', { 'with-close': Boolean(onClose) }), style: contentStyle, onFocus: handleFocus, onBlur: handleBlur },
85
96
  filteredItems.map((item, index) => {
86
97
  const isActive = index === activeIndex;
87
98
  const isPrev = filteredItems.length > 1 &&
@@ -92,8 +103,8 @@ const HeaderStripe = ({ duration = 8000, items, textColor, background, backgroun
92
103
  return (react_1.default.createElement("div", { key: String(index), className: b('item', {
93
104
  active: isActive,
94
105
  prev: isPrev,
95
- }) },
96
- react_1.default.createElement("div", { className: b('item-content') }, renderItemContent(item))));
106
+ }), "aria-hidden": !isActive },
107
+ react_1.default.createElement("div", { className: b('item-content') }, renderItemContent(item, isActive))));
97
108
  }),
98
109
  canClose && (react_1.default.createElement("button", { "aria-label": (0, i18n_1.default)('close'), className: b('close'), onClick: handleClose },
99
110
  react_1.default.createElement(uikit_1.Icon, { data: newDesign ? CloseIcon_1.CloseIcon : icons_1.Xmark, className: b('close-icon'), size: 16 }))))))));
@@ -7,9 +7,8 @@ const EnrichedLink_1 = require("../../../../EnrichedLink/EnrichedLink");
7
7
  const b = (0, cn_1.block)('group-links');
8
8
  const GroupLinks = ({ columnGroup: { title, items }, className }) => {
9
9
  return (react_1.default.createElement("div", { className: b(null, className), key: title },
10
- react_1.default.createElement("ul", { className: b('group') },
11
- title && react_1.default.createElement("h5", { className: b('group-title') }, title),
12
- items.map((item) => (react_1.default.createElement("li", { key: item.title, className: b('item-wrapper') },
13
- react_1.default.createElement(EnrichedLink_1.EnrichedLink, Object.assign({ className: b('item') }, item))))))));
10
+ title && react_1.default.createElement("h5", { className: b('group-title') }, title),
11
+ react_1.default.createElement("ul", { className: b('group'), "aria-label": title }, items.map((item) => (react_1.default.createElement("li", { key: item.title, className: b('item-wrapper') },
12
+ react_1.default.createElement(EnrichedLink_1.EnrichedLink, Object.assign({ className: b('item') }, item))))))));
14
13
  };
15
14
  exports.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;
@@ -19,6 +19,7 @@ const NewHeader = ({ data, setupRouteChangeHandler, renderSearch, className, scr
19
19
  const [isSearchMode, setIsSearchMode] = (0, react_1.useState)(false);
20
20
  const [isMobileNavigationOpen, setIsMobileNavigationOpen] = (0, react_1.useState)(false);
21
21
  const [pageHasScroll, setPageHasScroll] = (0, react_1.useState)(false);
22
+ const [activeNavigationId, setActiveNavigationId] = (0, react_1.useState)(null);
22
23
  const showButtonsContainer = Boolean(buttons);
23
24
  const toggleSearch = (0, react_1.useCallback)((isActive) => setIsSearchMode(isActive), []);
24
25
  const toggleMobileNavigationPopup = (0, react_1.useCallback)((isOpened) => {
@@ -58,10 +59,10 @@ const NewHeader = ({ data, setupRouteChangeHandler, renderSearch, className, scr
58
59
  react_1.default.createElement("div", { className: b('left') },
59
60
  logo && react_1.default.createElement(NHLogo_1.NHLogo, { data: logo }),
60
61
  left ? (react_1.default.createElement("div", { className: b('navigation') },
61
- react_1.default.createElement(NHNavigation_1.NHNavigation, { data: left, headerRef: headerRef }))) : null),
62
+ react_1.default.createElement(NHNavigation_1.NHNavigation, { activeNavigationId: activeNavigationId, data: left, headerRef: headerRef, navigationId: "left", setActiveNavigationId: setActiveNavigationId }))) : null),
62
63
  react_1.default.createElement("div", { className: b('right') },
63
64
  right ? (react_1.default.createElement("div", { className: b('navigation') },
64
- react_1.default.createElement(NHNavigation_1.NHNavigation, { data: right, headerRef: headerRef }))) : null,
65
+ react_1.default.createElement(NHNavigation_1.NHNavigation, { activeNavigationId: activeNavigationId, data: right, headerRef: headerRef, navigationId: "right", setActiveNavigationId: setActiveNavigationId }))) : null,
65
66
  react_1.default.createElement("div", { className: b('wrap') },
66
67
  renderSearch && (react_1.default.createElement("div", { className: b('icons-container') }, renderSearch({ onActiveToggle: toggleSearch }))),
67
68
  react_1.default.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
  }
@@ -15,19 +15,30 @@ const NHLoginButton = ({ data, headerRef, setupRouteChangeHandler }) => {
15
15
  const [isActive, setIsActive] = (0, react_1.useState)(false);
16
16
  const [pretendentActiveTab, setPretendentAciveTab] = (0, react_1.useState)(false);
17
17
  const [previouslyFocusedElement, setPreviouslyFocusedElement] = (0, react_1.useState)(null);
18
+ const popupId = (0, uikit_1.useUniqId)();
18
19
  const handleActiveTab = (0, react_1.useCallback)((val) => {
19
20
  setPreviouslyFocusedElement(document.activeElement);
20
21
  setPretendentAciveTab(val);
21
22
  }, []);
22
23
  const onEscapeKeyDown = (0, react_1.useCallback)((event) => {
23
- if (event.key === 'Escape') {
24
+ if (event.key === 'Escape' && isActive) {
24
25
  setIsActive(false);
25
26
  setPretendentAciveTab(false);
26
27
  previouslyFocusedElement === null || previouslyFocusedElement === void 0 ? void 0 : previouslyFocusedElement.focus({ preventScroll: true });
27
28
  }
28
- }, [previouslyFocusedElement]);
29
+ }, [isActive, previouslyFocusedElement]);
29
30
  const handleMouseEnter = (0, react_1.useCallback)(() => handleActiveTab(true), [handleActiveTab]);
30
31
  const handleMouseLeave = (0, react_1.useCallback)(() => handleActiveTab(false), [handleActiveTab]);
32
+ const handleToggle = (0, react_1.useCallback)(() => {
33
+ const focusedElement = document.activeElement;
34
+ const nextIsActive = !isActive;
35
+ setPreviouslyFocusedElement(focusedElement);
36
+ setPretendentAciveTab(nextIsActive);
37
+ setIsActive(nextIsActive);
38
+ if (!nextIsActive) {
39
+ focusedElement === null || focusedElement === void 0 ? void 0 : focusedElement.focus({ preventScroll: true });
40
+ }
41
+ }, [isActive]);
31
42
  (0, react_1.useEffect)(() => {
32
43
  const timerId = setTimeout(() => {
33
44
  setIsActive(pretendentActiveTab);
@@ -43,10 +54,11 @@ const NHLoginButton = ({ data, headerRef, setupRouteChangeHandler }) => {
43
54
  (0, react_1.useEffect)(() => setupRouteChangeHandler === null || setupRouteChangeHandler === void 0 ? void 0 : setupRouteChangeHandler(() => {
44
55
  handleMouseLeave();
45
56
  }), [handleMouseLeave, setupRouteChangeHandler]);
46
- return (react_1.default.createElement("div", { className: b(), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
47
- data.text,
48
- react_1.default.createElement(uikit_1.Icon, { data: isActive ? ChevronUp_1.ChevronUp : ChevronDown_1.ChevronDown, size: 16 }),
49
- isActive && (react_1.default.createElement(NHNavigationPopup_1.NHNavigationPopup, { headerRef: headerRef },
57
+ return (react_1.default.createElement("div", { className: b('wrapper'), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
58
+ react_1.default.createElement("button", { className: b(), type: "button", onClick: handleToggle, "aria-expanded": isActive, "aria-controls": popupId },
59
+ data.text,
60
+ react_1.default.createElement(uikit_1.Icon, { data: isActive ? ChevronUp_1.ChevronUp : ChevronDown_1.ChevronDown, size: 16 })),
61
+ isActive && (react_1.default.createElement(NHNavigationPopup_1.NHNavigationPopup, { headerRef: headerRef, id: popupId },
50
62
  react_1.default.createElement(NHLoginPopup_1.NHLoginPopup, Object.assign({}, data))))));
51
63
  };
52
64
  exports.NHLoginButton = NHLoginButton;
@@ -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;
@@ -14,17 +14,22 @@ const NHPopupItem_1 = require("../NHPopupItem/NHPopupItem");
14
14
  const b = (0, cn_1.block)('nh-mobile-navigation');
15
15
  const NHMobileNavigation = ({ isOpened, toogleOpen, isSearchOpen, data, onMenuScroll, }) => {
16
16
  var _a, _b;
17
+ const popupId = (0, uikit_1.useUniqId)();
17
18
  return (react_1.default.createElement("div", { className: b() },
18
- react_1.default.createElement(uikit_1.Button, { view: "flat", size: "l", className: b('icon', { hidden: isSearchOpen }), onClick: (e) => {
19
+ react_1.default.createElement(uikit_1.Button, { view: "flat", size: "l", className: b('icon', { hidden: isSearchOpen }), extraProps: {
20
+ 'aria-label': isOpened ? 'Close navigation menu' : 'Open navigation menu',
21
+ 'aria-expanded': isOpened,
22
+ 'aria-controls': popupId,
23
+ }, onClick: (e) => {
19
24
  e.stopPropagation();
20
25
  toogleOpen(!isOpened);
21
26
  } },
22
27
  react_1.default.createElement(uikit_1.Icon, { data: isOpened ? icons_1.Xmark : icons_1.Bars, size: 24 })),
23
- react_1.default.createElement(NHMobileNavigationPopup_1.NHMobileNavigationPopup, { isOpened: isOpened, onClose: () => toogleOpen(false), onMenuScroll: onMenuScroll },
28
+ react_1.default.createElement(NHMobileNavigationPopup_1.NHMobileNavigationPopup, { id: popupId, isOpened: isOpened, onClose: () => toogleOpen(false), onMenuScroll: onMenuScroll },
24
29
  react_1.default.createElement("div", { className: b('header') },
25
30
  (data === null || data === void 0 ? void 0 : data.logo) && react_1.default.createElement(NHLogo_1.NHLogo, { data: data.logo }),
26
31
  react_1.default.createElement("div", { className: b('header-right') },
27
- react_1.default.createElement(uikit_1.Button, { view: "flat", size: "l", className: b('close-button'), onClick: () => toogleOpen(false) },
32
+ react_1.default.createElement(uikit_1.Button, { view: "flat", size: "l", className: b('close-button'), extraProps: { 'aria-label': 'Close navigation menu' }, onClick: () => toogleOpen(false) },
28
33
  react_1.default.createElement(uikit_1.Icon, { data: icons_1.Xmark, size: 24 })))),
29
34
  react_1.default.createElement("nav", { className: b('nav') },
30
35
  react_1.default.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_1.default.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;
@@ -15,6 +15,7 @@ const NHMobileNavigationItem = ({ item }) => {
15
15
  }, [isOpened]);
16
16
  const handleKeyDown = (0, react_1.useCallback)((e) => {
17
17
  if (e.key === 'Enter' || e.key === ' ') {
18
+ e.preventDefault();
18
19
  toggleOpen();
19
20
  }
20
21
  }, [toggleOpen]);
@@ -23,7 +24,7 @@ const NHMobileNavigationItem = ({ item }) => {
23
24
  return (react_1.default.createElement("a", Object.assign({ href: item.data.url, className: b({ type: 'link' }) }, (0, gravity_ui_page_constructor_1.getLinkProps)(item.data.url || '', undefined, item.data.target)), item.title));
24
25
  }
25
26
  return (react_1.default.createElement(react_1.Fragment, null,
26
- react_1.default.createElement("div", { className: b(), onClick: toggleOpen, role: "button", tabIndex: 0, onKeyDown: handleKeyDown },
27
+ react_1.default.createElement("div", { className: b(), onClick: toggleOpen, role: "button", tabIndex: 0, "aria-expanded": isOpened, onKeyDown: handleKeyDown },
27
28
  react_1.default.createElement("div", { className: b('text') }, item.title),
28
29
  react_1.default.createElement("div", { className: b('arrow') },
29
30
  react_1.default.createElement(gravity_ui_page_constructor_1.ToggleArrow, { size: 12, type: "vertical", open: isOpened, iconType: "navigation" }))),
@@ -1,9 +1,10 @@
1
1
  import React from 'react';
2
2
  interface MobileNavigationPopupProps {
3
+ id?: string;
3
4
  isOpened: boolean;
4
5
  onClose: () => void;
5
6
  children: React.ReactNode;
6
7
  onMenuScroll: (scrollTop: number) => void;
7
8
  }
8
- export declare const NHMobileNavigationPopup: ({ isOpened, onClose, children, onMenuScroll, }: MobileNavigationPopupProps) => React.ReactPortal | null;
9
+ export declare const NHMobileNavigationPopup: ({ id, isOpened, onClose, children, onMenuScroll, }: MobileNavigationPopupProps) => React.ReactPortal | null;
9
10
  export {};
@@ -10,7 +10,7 @@ const react_transition_group_1 = require("react-transition-group");
10
10
  const cn_1 = require("../../../../utils/cn");
11
11
  const b = (0, cn_1.block)('nh-mobile-navigation-popup');
12
12
  const TRANSITION_TIME = 400;
13
- const NHMobileNavigationPopup = ({ isOpened, onClose, children, onMenuScroll, }) => {
13
+ const NHMobileNavigationPopup = ({ id, isOpened, onClose, children, onMenuScroll, }) => {
14
14
  const [body, setBody] = (0, react_1.useState)();
15
15
  const ref = (0, react_1.useRef)(null);
16
16
  const handleScroll = (0, react_1.useCallback)(() => {
@@ -38,6 +38,6 @@ const NHMobileNavigationPopup = ({ isOpened, onClose, children, onMenuScroll, })
38
38
  }
39
39
  return react_dom_1.default.createPortal(react_1.default.createElement(react_transition_group_1.CSSTransition, { in: isOpened, classNames: b('transition'), unmountOnExit: true, timeout: TRANSITION_TIME },
40
40
  react_1.default.createElement(gravity_ui_page_constructor_1.OutsideClick, { className: b(), onOutsideClick: onClose },
41
- react_1.default.createElement("div", { ref: ref, className: b('container') }, children))), body);
41
+ react_1.default.createElement("div", { ref: ref, className: b('container'), id: id }, children))), body);
42
42
  };
43
43
  exports.NHMobileNavigationPopup = NHMobileNavigationPopup;
@@ -2,9 +2,12 @@ import type { RefObject } from 'react';
2
2
  import React from 'react';
3
3
  import { NHNavigationItemData, SetupRouteChangeHandler } from '../../models';
4
4
  interface NavigationProps {
5
+ activeNavigationId: string | null;
5
6
  data: NHNavigationItemData[];
6
7
  headerRef?: RefObject<HTMLDivElement>;
8
+ navigationId: string;
7
9
  setupRouteChangeHandler?: SetupRouteChangeHandler;
10
+ setActiveNavigationId: (navigationId: string | null) => void;
8
11
  }
9
- export declare const NHNavigation: ({ data, headerRef, setupRouteChangeHandler }: NavigationProps) => React.JSX.Element;
12
+ export declare const NHNavigation: ({ activeNavigationId, data, headerRef, navigationId, setupRouteChangeHandler, setActiveNavigationId, }: NavigationProps) => React.JSX.Element;
10
13
  export {};
@@ -9,6 +9,7 @@ const models_1 = require("../../models");
9
9
  const NHDefaultPopup_1 = require("../NHDefaultPopup/NHDefaultPopup");
10
10
  const NHNavigationItem_1 = require("../NHNavigationItem/NHNavigationItem");
11
11
  const NHNavigationPopup_1 = require("../NHNavigationPopup/NHNavigationPopup");
12
+ const keyboardNavigation_1 = require("../NHNavigationPopup/keyboardNavigation");
12
13
  const b = (0, cn_1.block)('nh-navigation');
13
14
  const tooltipPrefixId = 'navigation-item-key';
14
15
  const getPopupContent = (sectionData) => {
@@ -19,37 +20,99 @@ const getPopupContent = (sectionData) => {
19
20
  return null;
20
21
  }
21
22
  };
22
- const NHNavigation = ({ data, headerRef, setupRouteChangeHandler }) => {
23
+ const NHNavigation = ({ activeNavigationId, data, headerRef, navigationId, setupRouteChangeHandler, setActiveNavigationId, }) => {
23
24
  const [activeTab, setActiveTab] = (0, react_1.useState)(constants_1.NO_MENU_TAB_SELECTED);
24
25
  const [pretendentActiveTab, setPretendentAciveTab] = (0, react_1.useState)(constants_1.NO_MENU_TAB_SELECTED);
25
26
  const [previouslyFocusedElement, setPreviouslyFocusedElement] = (0, react_1.useState)(null);
27
+ const [popupTabToFocus, setPopupTabToFocus] = (0, react_1.useState)(null);
28
+ const setNavigationTab = (0, react_1.useCallback)((currentIndex) => {
29
+ setPretendentAciveTab(currentIndex);
30
+ setActiveTab(currentIndex);
31
+ setPopupTabToFocus(null);
32
+ setActiveNavigationId(currentIndex === constants_1.NO_MENU_TAB_SELECTED ? null : navigationId);
33
+ }, [navigationId, setActiveNavigationId]);
26
34
  const handleActiveTab = (0, react_1.useCallback)((currentIndex) => {
27
35
  setPreviouslyFocusedElement(document.activeElement);
28
36
  setPretendentAciveTab(currentIndex);
29
37
  }, []);
38
+ const handleToggleTab = (0, react_1.useCallback)((currentIndex) => {
39
+ const focusedElement = document.activeElement;
40
+ const nextActiveTab = activeTab === currentIndex ? constants_1.NO_MENU_TAB_SELECTED : currentIndex;
41
+ setPreviouslyFocusedElement(focusedElement);
42
+ setNavigationTab(nextActiveTab);
43
+ if (nextActiveTab === constants_1.NO_MENU_TAB_SELECTED) {
44
+ focusedElement === null || focusedElement === void 0 ? void 0 : focusedElement.focus({ preventScroll: true });
45
+ }
46
+ }, [activeTab, setNavigationTab]);
47
+ const handleFocusTabPopup = (0, react_1.useCallback)((currentIndex) => {
48
+ setPreviouslyFocusedElement(document.activeElement);
49
+ setPretendentAciveTab(currentIndex);
50
+ setActiveTab(currentIndex);
51
+ setPopupTabToFocus(currentIndex);
52
+ setActiveNavigationId(navigationId);
53
+ }, [navigationId, setActiveNavigationId]);
54
+ const handleFocusTab = (0, react_1.useCallback)((currentIndex) => {
55
+ if (activeNavigationId === null) {
56
+ return;
57
+ }
58
+ setPreviouslyFocusedElement(document.activeElement);
59
+ setNavigationTab(currentIndex);
60
+ }, [activeNavigationId, setNavigationTab]);
61
+ const handleCloseTab = (0, react_1.useCallback)(() => {
62
+ setNavigationTab(constants_1.NO_MENU_TAB_SELECTED);
63
+ previouslyFocusedElement === null || previouslyFocusedElement === void 0 ? void 0 : previouslyFocusedElement.focus({ preventScroll: true });
64
+ }, [previouslyFocusedElement, setNavigationTab]);
65
+ const handleBlur = (0, react_1.useCallback)((event) => {
66
+ if (activeTab === constants_1.NO_MENU_TAB_SELECTED) {
67
+ return;
68
+ }
69
+ const nextFocusedElement = event.relatedTarget;
70
+ const popup = document.getElementById(`${tooltipPrefixId}-${activeTab}`);
71
+ if (nextFocusedElement &&
72
+ (event.currentTarget.contains(nextFocusedElement) ||
73
+ (popup === null || popup === void 0 ? void 0 : popup.contains(nextFocusedElement)))) {
74
+ return;
75
+ }
76
+ setNavigationTab(constants_1.NO_MENU_TAB_SELECTED);
77
+ }, [activeTab, setNavigationTab]);
30
78
  const onEscapeKeyDown = (0, react_1.useCallback)((event) => {
31
- if (event.key === 'Escape') {
32
- setActiveTab(constants_1.NO_MENU_TAB_SELECTED);
33
- setPretendentAciveTab(constants_1.NO_MENU_TAB_SELECTED);
34
- previouslyFocusedElement === null || previouslyFocusedElement === void 0 ? void 0 : previouslyFocusedElement.focus({ preventScroll: true });
79
+ if (event.key === 'Escape' && activeTab !== constants_1.NO_MENU_TAB_SELECTED) {
80
+ handleCloseTab();
35
81
  }
36
- }, [previouslyFocusedElement]);
82
+ }, [activeTab, handleCloseTab]);
37
83
  (0, react_1.useEffect)(() => setupRouteChangeHandler === null || setupRouteChangeHandler === void 0 ? void 0 : setupRouteChangeHandler(() => {
38
84
  handleActiveTab(constants_1.NO_MENU_TAB_SELECTED);
39
85
  }), [setupRouteChangeHandler, handleActiveTab]);
40
86
  (0, react_1.useEffect)(() => {
41
87
  const timerId = setTimeout(() => {
42
88
  setActiveTab(pretendentActiveTab);
89
+ setActiveNavigationId(pretendentActiveTab === constants_1.NO_MENU_TAB_SELECTED ? null : navigationId);
43
90
  }, constants_1.SWITCH_MENU_TAB_TIMEOUT);
44
91
  return () => clearTimeout(timerId);
45
- }, [activeTab, pretendentActiveTab]);
92
+ }, [activeTab, navigationId, pretendentActiveTab, setActiveNavigationId]);
93
+ (0, react_1.useEffect)(() => {
94
+ if (activeNavigationId !== navigationId && activeTab !== constants_1.NO_MENU_TAB_SELECTED) {
95
+ setActiveTab(constants_1.NO_MENU_TAB_SELECTED);
96
+ setPretendentAciveTab(constants_1.NO_MENU_TAB_SELECTED);
97
+ setPopupTabToFocus(null);
98
+ }
99
+ }, [activeNavigationId, activeTab, navigationId]);
100
+ (0, react_1.useEffect)(() => {
101
+ if (popupTabToFocus === null || activeTab !== popupTabToFocus) {
102
+ return;
103
+ }
104
+ const popup = document.getElementById(`${tooltipPrefixId}-${popupTabToFocus}`);
105
+ const [firstFocusableElement] = (0, keyboardNavigation_1.getFocusableElements)(popup);
106
+ (0, keyboardNavigation_1.focusFromKeyboard)(firstFocusableElement, popup);
107
+ setPopupTabToFocus(null);
108
+ }, [activeTab, popupTabToFocus]);
46
109
  (0, react_1.useEffect)(() => {
47
110
  document.addEventListener('keydown', onEscapeKeyDown);
48
111
  return () => {
49
112
  document.removeEventListener('keydown', onEscapeKeyDown);
50
113
  };
51
114
  }, [onEscapeKeyDown]);
52
- return (react_1.default.createElement("nav", null,
53
- react_1.default.createElement("ul", { className: b() }, data.map((item, i) => (react_1.default.createElement(NHNavigationItem_1.NHNavigationItem, { key: i, item: item, handleActiveTab: handleActiveTab, index: i, isActive: activeTab === i, tooltipId: `${tooltipPrefixId}-${i}` }, activeTab === i && item.type !== models_1.NHNavigationItemType.Link && (react_1.default.createElement(NHNavigationPopup_1.NHNavigationPopup, { headerRef: headerRef, id: `${tooltipPrefixId}-${i}` }, getPopupContent(item)))))))));
115
+ return (react_1.default.createElement("nav", { onBlur: handleBlur },
116
+ react_1.default.createElement("ul", { className: b() }, data.map((item, i) => (react_1.default.createElement(NHNavigationItem_1.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 !== models_1.NHNavigationItemType.Link && (react_1.default.createElement(NHNavigationPopup_1.NHNavigationPopup, { headerRef: headerRef, id: `${tooltipPrefixId}-${i}` }, getPopupContent(item)))))))));
54
117
  };
55
118
  exports.NHNavigation = NHNavigation;
@@ -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
  }
@@ -5,6 +5,9 @@ interface NavigationItemOwnProps {
5
5
  item: NHNavigationItemData;
6
6
  isActive: boolean;
7
7
  handleActiveTab: (currentIndex: number) => void;
8
+ handleFocusTab: (currentIndex: number) => void;
9
+ handleFocusTabPopup: (currentIndex: number) => void;
10
+ handleToggleTab: (currentIndex: number) => void;
8
11
  children?: ReactNode;
9
12
  tooltipId?: string;
10
13
  }
@@ -12,21 +12,30 @@ const models_1 = require("../../models");
12
12
  const ChevronDown_1 = require("../ChevronDown");
13
13
  const ChevronUp_1 = require("../ChevronUp");
14
14
  const b = (0, cn_1.block)('nh-navigation-item');
15
- const NHNavigationItem = ({ item, isActive, handleActiveTab, index, children, tooltipId, }) => {
15
+ const NHNavigationItem = ({ item, isActive, handleActiveTab, handleFocusTab, handleFocusTabPopup, handleToggleTab, index, children, tooltipId, }) => {
16
16
  var _a, _b, _c, _d;
17
17
  const setupRouteChangeHandler = (0, react_1.useContext)(route_change_1.RouteChangeHandlerContext);
18
18
  const handleMouseEnter = (0, react_1.useCallback)(() => handleActiveTab(index), [handleActiveTab, index]);
19
19
  const handleMouseLeave = (0, react_1.useCallback)(() => handleActiveTab(constants_1.NO_MENU_TAB_SELECTED), [handleActiveTab]);
20
+ const handleToggle = (0, react_1.useCallback)(() => handleToggleTab(index), [handleToggleTab, index]);
21
+ const handlePopupFocus = (0, react_1.useCallback)(() => handleFocusTab(index), [handleFocusTab, index]);
22
+ const handleLinkFocus = (0, react_1.useCallback)(() => handleFocusTab(constants_1.NO_MENU_TAB_SELECTED), [handleFocusTab]);
23
+ const handleKeyDown = (0, react_1.useCallback)((event) => {
24
+ if (event.key === 'ArrowDown') {
25
+ event.preventDefault();
26
+ handleFocusTabPopup(index);
27
+ }
28
+ }, [handleFocusTabPopup, index]);
20
29
  (0, react_1.useEffect)(() => setupRouteChangeHandler === null || setupRouteChangeHandler === void 0 ? void 0 : setupRouteChangeHandler(() => {
21
30
  handleMouseLeave();
22
31
  }), [handleMouseLeave, setupRouteChangeHandler]);
23
32
  if (item.type === models_1.NHNavigationItemType.Link) {
24
33
  return (react_1.default.createElement("li", { className: b({ disable: !((_a = item.data) === null || _a === void 0 ? void 0 : _a.url) }), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
25
- react_1.default.createElement("a", Object.assign({ className: b('text', { active: isActive }), href: (_b = item.data) === null || _b === void 0 ? void 0 : _b.url }, (0, gravity_ui_page_constructor_1.getLinkProps)(((_c = item.data) === null || _c === void 0 ? void 0 : _c.url) || '', undefined, (_d = item.data) === null || _d === void 0 ? void 0 : _d.target)), item.title),
34
+ react_1.default.createElement("a", Object.assign({ className: b('text', { active: isActive }), href: (_b = item.data) === null || _b === void 0 ? void 0 : _b.url, onFocus: handleLinkFocus }, (0, gravity_ui_page_constructor_1.getLinkProps)(((_c = item.data) === null || _c === void 0 ? void 0 : _c.url) || '', undefined, (_d = item.data) === null || _d === void 0 ? void 0 : _d.target)), item.title),
26
35
  children));
27
36
  }
28
37
  return (react_1.default.createElement("li", { className: b({}), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
29
- react_1.default.createElement("button", { className: b('text', { active: isActive, cursor: 'default' }), onClick: handleMouseEnter, "aria-expanded": isActive, "aria-controls": tooltipId },
38
+ react_1.default.createElement("button", { className: b('text', { active: isActive, cursor: 'default' }), type: "button", onClick: handleToggle, onFocus: handlePopupFocus, onKeyDown: handleKeyDown, "aria-expanded": isActive, "aria-controls": tooltipId },
30
39
  item.title,
31
40
  react_1.default.createElement(uikit_1.Icon, { className: b('icon'), data: isActive ? ChevronUp_1.ChevronUp : ChevronDown_1.ChevronDown, size: 16 })),
32
41
  children));
@@ -8,6 +8,17 @@
8
8
  z-index: 10;
9
9
  }
10
10
 
11
- .pc-addons-nh-navigation-popup:focus {
12
- outline: 0;
11
+ .pc-addons-nh-navigation-popup a:focus-visible, .pc-addons-nh-navigation-popup a[data-keyboard-focus=true],
12
+ .pc-addons-nh-navigation-popup button:focus-visible,
13
+ .pc-addons-nh-navigation-popup button[data-keyboard-focus=true],
14
+ .pc-addons-nh-navigation-popup input:focus-visible,
15
+ .pc-addons-nh-navigation-popup input[data-keyboard-focus=true],
16
+ .pc-addons-nh-navigation-popup select:focus-visible,
17
+ .pc-addons-nh-navigation-popup select[data-keyboard-focus=true],
18
+ .pc-addons-nh-navigation-popup textarea:focus-visible,
19
+ .pc-addons-nh-navigation-popup textarea[data-keyboard-focus=true],
20
+ .pc-addons-nh-navigation-popup [tabindex]:not([tabindex="-1"]):focus-visible,
21
+ .pc-addons-nh-navigation-popup [tabindex]:not([tabindex="-1"])[data-keyboard-focus=true] {
22
+ outline: 2px solid var(--g-color-line-focus);
23
+ outline-offset: 4px;
13
24
  }