@doyourjob/gravity-ui-page-constructor-addons 2.1.46 → 2.1.47
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.
- package/build/cjs/components/Navigation/containers/Footer/GroupLinks/GroupLinks.js +3 -4
- package/build/cjs/components/NewHeader/NewHeader.css +5 -0
- package/build/cjs/components/NewHeader/NewHeader.js +3 -2
- package/build/cjs/components/NewHeader/components/NHLoginButton/NHLoginButton.css +18 -0
- package/build/cjs/components/NewHeader/components/NHLoginButton/NHLoginButton.js +18 -6
- package/build/cjs/components/NewHeader/components/NHMobileNavigation/NHMobileNavigation.css +10 -0
- package/build/cjs/components/NewHeader/components/NHMobileNavigation/NHMobileNavigation.js +8 -3
- package/build/cjs/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.css +5 -0
- package/build/cjs/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.js +2 -1
- package/build/cjs/components/NewHeader/components/NHMobileNavigationPopup/NHMobileNavigationPopup.d.ts +2 -1
- package/build/cjs/components/NewHeader/components/NHMobileNavigationPopup/NHMobileNavigationPopup.js +2 -2
- package/build/cjs/components/NewHeader/components/NHNavigation/NHNavigation.d.ts +4 -1
- package/build/cjs/components/NewHeader/components/NHNavigation/NHNavigation.js +72 -9
- package/build/cjs/components/NewHeader/components/NHNavigationItem/NHNavigationItem.css +5 -0
- package/build/cjs/components/NewHeader/components/NHNavigationItem/NHNavigationItem.d.ts +3 -0
- package/build/cjs/components/NewHeader/components/NHNavigationItem/NHNavigationItem.js +12 -3
- package/build/cjs/components/NewHeader/components/NHNavigationPopup/NHNavigationPopup.css +13 -2
- package/build/cjs/components/NewHeader/components/NHNavigationPopup/NHNavigationPopup.js +58 -6
- package/build/cjs/components/NewHeader/components/NHNavigationPopup/keyboardNavigation.d.ts +4 -0
- package/build/cjs/components/NewHeader/components/NHNavigationPopup/keyboardNavigation.js +43 -0
- package/build/cjs/components/NewHeader/components/NHPopupItem/NHPopupItem.css +5 -0
- package/build/esm/components/Navigation/containers/Footer/GroupLinks/GroupLinks.js +3 -4
- package/build/esm/components/NewHeader/NewHeader.css +5 -0
- package/build/esm/components/NewHeader/NewHeader.js +3 -2
- package/build/esm/components/NewHeader/components/NHLoginButton/NHLoginButton.css +18 -0
- package/build/esm/components/NewHeader/components/NHLoginButton/NHLoginButton.js +19 -7
- package/build/esm/components/NewHeader/components/NHMobileNavigation/NHMobileNavigation.css +10 -0
- package/build/esm/components/NewHeader/components/NHMobileNavigation/NHMobileNavigation.js +9 -4
- package/build/esm/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.css +5 -0
- package/build/esm/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.js +2 -1
- package/build/esm/components/NewHeader/components/NHMobileNavigationPopup/NHMobileNavigationPopup.d.ts +2 -1
- package/build/esm/components/NewHeader/components/NHMobileNavigationPopup/NHMobileNavigationPopup.js +2 -2
- package/build/esm/components/NewHeader/components/NHNavigation/NHNavigation.d.ts +4 -1
- package/build/esm/components/NewHeader/components/NHNavigation/NHNavigation.js +72 -9
- package/build/esm/components/NewHeader/components/NHNavigationItem/NHNavigationItem.css +5 -0
- package/build/esm/components/NewHeader/components/NHNavigationItem/NHNavigationItem.d.ts +3 -0
- package/build/esm/components/NewHeader/components/NHNavigationItem/NHNavigationItem.js +12 -3
- package/build/esm/components/NewHeader/components/NHNavigationPopup/NHNavigationPopup.css +13 -2
- package/build/esm/components/NewHeader/components/NHNavigationPopup/NHNavigationPopup.js +58 -6
- package/build/esm/components/NewHeader/components/NHNavigationPopup/keyboardNavigation.d.ts +4 -0
- package/build/esm/components/NewHeader/components/NHNavigationPopup/keyboardNavigation.js +38 -0
- package/build/esm/components/NewHeader/components/NHPopupItem/NHPopupItem.css +5 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 }),
|
|
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 },
|
package/build/esm/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.css
CHANGED
|
@@ -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;
|
package/build/esm/components/NewHeader/components/NHMobileNavigationItem/NHMobileNavigationItem.js
CHANGED
|
@@ -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 {};
|
package/build/esm/components/NewHeader/components/NHMobileNavigationPopup/NHMobileNavigationPopup.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}, [
|
|
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",
|
|
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
|
}
|
|
@@ -6,6 +6,9 @@ interface NavigationItemOwnProps {
|
|
|
6
6
|
item: NHNavigationItemData;
|
|
7
7
|
isActive: boolean;
|
|
8
8
|
handleActiveTab: (currentIndex: number) => void;
|
|
9
|
+
handleFocusTab: (currentIndex: number) => void;
|
|
10
|
+
handleFocusTabPopup: (currentIndex: number) => void;
|
|
11
|
+
handleToggleTab: (currentIndex: number) => void;
|
|
9
12
|
children?: ReactNode;
|
|
10
13
|
tooltipId?: string;
|
|
11
14
|
}
|
|
@@ -9,21 +9,30 @@ import { ChevronDown } from '../ChevronDown';
|
|
|
9
9
|
import { ChevronUp } from '../ChevronUp';
|
|
10
10
|
import './NHNavigationItem.css';
|
|
11
11
|
const b = block('nh-navigation-item');
|
|
12
|
-
export const NHNavigationItem = ({ item, isActive, handleActiveTab, index, children, tooltipId, }) => {
|
|
12
|
+
export const NHNavigationItem = ({ item, isActive, handleActiveTab, handleFocusTab, handleFocusTabPopup, handleToggleTab, index, children, tooltipId, }) => {
|
|
13
13
|
var _a, _b, _c, _d;
|
|
14
14
|
const setupRouteChangeHandler = useContext(RouteChangeHandlerContext);
|
|
15
15
|
const handleMouseEnter = useCallback(() => handleActiveTab(index), [handleActiveTab, index]);
|
|
16
16
|
const handleMouseLeave = useCallback(() => handleActiveTab(NO_MENU_TAB_SELECTED), [handleActiveTab]);
|
|
17
|
+
const handleToggle = useCallback(() => handleToggleTab(index), [handleToggleTab, index]);
|
|
18
|
+
const handlePopupFocus = useCallback(() => handleFocusTab(index), [handleFocusTab, index]);
|
|
19
|
+
const handleLinkFocus = useCallback(() => handleFocusTab(NO_MENU_TAB_SELECTED), [handleFocusTab]);
|
|
20
|
+
const handleKeyDown = useCallback((event) => {
|
|
21
|
+
if (event.key === 'ArrowDown') {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
handleFocusTabPopup(index);
|
|
24
|
+
}
|
|
25
|
+
}, [handleFocusTabPopup, index]);
|
|
17
26
|
useEffect(() => setupRouteChangeHandler === null || setupRouteChangeHandler === void 0 ? void 0 : setupRouteChangeHandler(() => {
|
|
18
27
|
handleMouseLeave();
|
|
19
28
|
}), [handleMouseLeave, setupRouteChangeHandler]);
|
|
20
29
|
if (item.type === NHNavigationItemType.Link) {
|
|
21
30
|
return (React.createElement("li", { className: b({ disable: !((_a = item.data) === null || _a === void 0 ? void 0 : _a.url) }), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
|
|
22
|
-
React.createElement("a", Object.assign({ className: b('text', { active: isActive }), href: (_b = item.data) === null || _b === void 0 ? void 0 : _b.url }, 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),
|
|
31
|
+
React.createElement("a", Object.assign({ className: b('text', { active: isActive }), href: (_b = item.data) === null || _b === void 0 ? void 0 : _b.url, onFocus: handleLinkFocus }, 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),
|
|
23
32
|
children));
|
|
24
33
|
}
|
|
25
34
|
return (React.createElement("li", { className: b({}), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave },
|
|
26
|
-
React.createElement("button", { className: b('text', { active: isActive, cursor: 'default' }), onClick:
|
|
35
|
+
React.createElement("button", { className: b('text', { active: isActive, cursor: 'default' }), type: "button", onClick: handleToggle, onFocus: handlePopupFocus, onKeyDown: handleKeyDown, "aria-expanded": isActive, "aria-controls": tooltipId },
|
|
27
36
|
item.title,
|
|
28
37
|
React.createElement(Icon, { className: b('icon'), data: isActive ? ChevronUp : ChevronDown, size: 16 })),
|
|
29
38
|
children));
|
|
@@ -8,6 +8,17 @@
|
|
|
8
8
|
z-index: 10;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
.pc-addons-nh-navigation-popup:focus
|
|
12
|
-
|
|
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
|
}
|
|
@@ -1,14 +1,66 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import ReactDOM from 'react-dom';
|
|
3
|
-
import { FocusTrap } from '../../../../utils/FocusTrap';
|
|
4
3
|
import { block } from '../../../../utils/cn';
|
|
4
|
+
import { focusFromKeyboard, getFocusableElements } from './keyboardNavigation';
|
|
5
5
|
import './NHNavigationPopup.css';
|
|
6
6
|
const b = block('nh-navigation-popup');
|
|
7
|
+
const nextKeys = new Set(['ArrowDown', 'ArrowRight']);
|
|
8
|
+
const previousKeys = new Set(['ArrowUp', 'ArrowLeft']);
|
|
9
|
+
const getNextIndex = (currentIndex, offset, elementsCount) => {
|
|
10
|
+
if (currentIndex === -1) {
|
|
11
|
+
return offset > 0 ? 0 : elementsCount - 1;
|
|
12
|
+
}
|
|
13
|
+
return (currentIndex + offset + elementsCount) % elementsCount;
|
|
14
|
+
};
|
|
7
15
|
export const NHNavigationPopup = ({ headerRef, children, id }) => {
|
|
16
|
+
const popupRef = useRef(null);
|
|
17
|
+
const focusRelativeElement = useCallback((offset) => {
|
|
18
|
+
const popup = popupRef.current;
|
|
19
|
+
const focusableElements = getFocusableElements(popup);
|
|
20
|
+
if (!popup || !focusableElements.length) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const activeElement = popup.ownerDocument.activeElement;
|
|
24
|
+
const currentIndex = activeElement ? focusableElements.indexOf(activeElement) : -1;
|
|
25
|
+
const nextIndex = getNextIndex(currentIndex, offset, focusableElements.length);
|
|
26
|
+
focusFromKeyboard(focusableElements[nextIndex], popup);
|
|
27
|
+
}, []);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const popup = popupRef.current;
|
|
30
|
+
if (!popup) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const handleKeyDown = (event) => {
|
|
34
|
+
if (event.key === 'Tab') {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
focusRelativeElement(event.shiftKey ? -1 : 1);
|
|
37
|
+
}
|
|
38
|
+
else if (nextKeys.has(event.key)) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
focusRelativeElement(1);
|
|
41
|
+
}
|
|
42
|
+
else if (previousKeys.has(event.key)) {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
focusRelativeElement(-1);
|
|
45
|
+
}
|
|
46
|
+
else if (event.key === 'Home') {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
const popupElement = popupRef.current;
|
|
49
|
+
focusFromKeyboard(getFocusableElements(popupElement)[0], popupElement);
|
|
50
|
+
}
|
|
51
|
+
else if (event.key === 'End') {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
const popupElement = popupRef.current;
|
|
54
|
+
const focusableElements = getFocusableElements(popupElement);
|
|
55
|
+
focusFromKeyboard(focusableElements[focusableElements.length - 1], popupElement);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
popup.addEventListener('keydown', handleKeyDown);
|
|
59
|
+
return () => {
|
|
60
|
+
popup.removeEventListener('keydown', handleKeyDown);
|
|
61
|
+
};
|
|
62
|
+
}, [focusRelativeElement]);
|
|
8
63
|
return (headerRef === null || headerRef === void 0 ? void 0 : headerRef.current)
|
|
9
|
-
? ReactDOM.createPortal(React.createElement(
|
|
10
|
-
React.createElement("div", {
|
|
11
|
-
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
12
|
-
tabIndex: 0, className: b(), id: id }, children)), headerRef.current)
|
|
64
|
+
? ReactDOM.createPortal(React.createElement("div", { className: b(), id: id, ref: popupRef }, children), headerRef.current)
|
|
13
65
|
: null;
|
|
14
66
|
};
|
|
@@ -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,38 @@
|
|
|
1
|
+
export const keyboardFocusAttribute = 'data-keyboard-focus';
|
|
2
|
+
export const focusableElementSelector = [
|
|
3
|
+
'a[href]',
|
|
4
|
+
'button:not([disabled])',
|
|
5
|
+
'input:not([disabled])',
|
|
6
|
+
'select:not([disabled])',
|
|
7
|
+
'textarea:not([disabled])',
|
|
8
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
9
|
+
].join(',');
|
|
10
|
+
const keyboardFocusSelector = `[${keyboardFocusAttribute}='true']`;
|
|
11
|
+
const blurHandlers = new WeakMap();
|
|
12
|
+
const clearKeyboardFocus = (root) => {
|
|
13
|
+
root.querySelectorAll(keyboardFocusSelector).forEach((element) => {
|
|
14
|
+
const blurHandler = blurHandlers.get(element);
|
|
15
|
+
if (blurHandler) {
|
|
16
|
+
element.removeEventListener('blur', blurHandler);
|
|
17
|
+
blurHandlers.delete(element);
|
|
18
|
+
}
|
|
19
|
+
element.removeAttribute(keyboardFocusAttribute);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
export const getFocusableElements = (root) => Array.from((root === null || root === void 0 ? void 0 : root.querySelectorAll(focusableElementSelector)) || []);
|
|
23
|
+
export const focusFromKeyboard = (element, root) => {
|
|
24
|
+
if (!element) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const focusRoot = root || element.ownerDocument;
|
|
28
|
+
const handleBlur = () => {
|
|
29
|
+
element.removeAttribute(keyboardFocusAttribute);
|
|
30
|
+
element.removeEventListener('blur', handleBlur);
|
|
31
|
+
blurHandlers.delete(element);
|
|
32
|
+
};
|
|
33
|
+
clearKeyboardFocus(focusRoot);
|
|
34
|
+
element.setAttribute(keyboardFocusAttribute, 'true');
|
|
35
|
+
element.addEventListener('blur', handleBlur);
|
|
36
|
+
blurHandlers.set(element, handleBlur);
|
|
37
|
+
element.focus({ preventScroll: true });
|
|
38
|
+
};
|
|
@@ -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
|
}
|