@atlaskit/navigation-system 9.3.0 → 9.4.0
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/CHANGELOG.md +15 -0
- package/constellation/index/migration-guide.mdx +338 -150
- package/dist/cjs/components/skip-links/focus-element.js +54 -0
- package/dist/cjs/components/skip-links/skip-link.js +32 -65
- package/dist/cjs/components/skip-links/skip-links-popup.js +26 -4
- package/dist/cjs/context/skip-links/use-skip-link-internal.js +5 -3
- package/dist/cjs/ui/page-layout/side-nav/side-nav.js +16 -5
- package/dist/cjs/ui/page-layout/side-nav/use-expand-side-nav.js +104 -38
- package/dist/cjs/ui/page-layout/side-nav/use-toggle-side-nav.js +33 -3
- package/dist/es2019/components/skip-links/focus-element.js +49 -0
- package/dist/es2019/components/skip-links/skip-link.js +32 -65
- package/dist/es2019/components/skip-links/skip-links-popup.js +26 -4
- package/dist/es2019/context/skip-links/use-skip-link-internal.js +5 -3
- package/dist/es2019/ui/page-layout/side-nav/side-nav.js +16 -5
- package/dist/es2019/ui/page-layout/side-nav/use-expand-side-nav.js +104 -38
- package/dist/es2019/ui/page-layout/side-nav/use-toggle-side-nav.js +33 -3
- package/dist/esm/components/skip-links/focus-element.js +49 -0
- package/dist/esm/components/skip-links/skip-link.js +32 -65
- package/dist/esm/components/skip-links/skip-links-popup.js +26 -4
- package/dist/esm/context/skip-links/use-skip-link-internal.js +5 -3
- package/dist/esm/ui/page-layout/side-nav/side-nav.js +16 -5
- package/dist/esm/ui/page-layout/side-nav/use-expand-side-nav.js +104 -38
- package/dist/esm/ui/page-layout/side-nav/use-toggle-side-nav.js +33 -3
- package/dist/types/components/skip-links/focus-element.d.ts +4 -0
- package/dist/types/components/skip-links/skip-link.d.ts +2 -1
- package/dist/types/context/skip-links/types.d.ts +18 -1
- package/dist/types/context/skip-links/use-skip-link-internal.d.ts +3 -3
- package/dist/types-ts4.5/components/skip-links/focus-element.d.ts +4 -0
- package/dist/types-ts4.5/components/skip-links/skip-link.d.ts +2 -1
- package/dist/types-ts4.5/context/skip-links/types.d.ts +18 -1
- package/dist/types-ts4.5/context/skip-links/use-skip-link-internal.d.ts +3 -3
- package/package.json +3 -3
|
@@ -2,62 +2,14 @@
|
|
|
2
2
|
import "./skip-link.compiled.css";
|
|
3
3
|
import { ax, ix } from "@compiled/react/runtime";
|
|
4
4
|
import React, { useCallback } from 'react';
|
|
5
|
-
import { bind } from 'bind-event-listener';
|
|
6
5
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
7
6
|
// eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- to be migrated to @atlaskit/primitives/compiled – go/akcss
|
|
8
7
|
import { Anchor } from '@atlaskit/primitives/compiled';
|
|
8
|
+
import { focusElement } from './focus-element';
|
|
9
9
|
const styles = {
|
|
10
10
|
skipLinkListItem: "_1pfhze3t",
|
|
11
11
|
skipLinkListItemNew: "_1rjcu2gc"
|
|
12
12
|
};
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Used for moving focus to the corresponding slot or custom target after clicking on a skip link.
|
|
16
|
-
*/
|
|
17
|
-
function focusElement(element) {
|
|
18
|
-
/**
|
|
19
|
-
* Elements without an explicit `tabindex` attribute are not guaranteed to be focusable:
|
|
20
|
-
* https://html.spec.whatwg.org/multipage/interaction.html#attr-tabindex
|
|
21
|
-
*
|
|
22
|
-
* Our slots are not interactive, so this is required.
|
|
23
|
-
*
|
|
24
|
-
* In the future we may want to check if there is an existing `tabindex` attribute,
|
|
25
|
-
* as custom skip linked elements might already have one.
|
|
26
|
-
*/
|
|
27
|
-
element.setAttribute('tabindex', '-1');
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Cleanup the `tabindex` attribute we set when the slot or custom target loses focus.
|
|
31
|
-
*
|
|
32
|
-
* This is preferable to always having `tabindex="-1"` because always applying the tab index can:
|
|
33
|
-
*
|
|
34
|
-
* - mess with click events
|
|
35
|
-
* - potentially cause a focus ring to be always visible
|
|
36
|
-
*/
|
|
37
|
-
bind(element, {
|
|
38
|
-
type: 'blur',
|
|
39
|
-
listener() {
|
|
40
|
-
element.removeAttribute('tabindex');
|
|
41
|
-
},
|
|
42
|
-
options: {
|
|
43
|
-
// Using a one-time listener so it cleans itself up
|
|
44
|
-
once: true
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Move focus to the slot or custom target.
|
|
50
|
-
*
|
|
51
|
-
* Calling `.focus()` will also scroll the element into view:
|
|
52
|
-
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
|
|
53
|
-
*/
|
|
54
|
-
element.focus({
|
|
55
|
-
// Forces the focus ring to appear after moving focus to the slot
|
|
56
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#focusvisible
|
|
57
|
-
// @ts-expect-error - new and not in types yet
|
|
58
|
-
focusVisible: true
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
13
|
/**
|
|
62
14
|
* A link that moves current tab position to a different element
|
|
63
15
|
*
|
|
@@ -66,26 +18,41 @@ function focusElement(element) {
|
|
|
66
18
|
export const SkipLink = ({
|
|
67
19
|
id,
|
|
68
20
|
children,
|
|
69
|
-
onBeforeNavigate
|
|
21
|
+
onBeforeNavigate,
|
|
22
|
+
navigate
|
|
70
23
|
}) => {
|
|
71
24
|
const href = `#${id}`;
|
|
72
|
-
const
|
|
25
|
+
const handleClick = useCallback(event => {
|
|
73
26
|
event.preventDefault();
|
|
27
|
+
if (navigate && fg('platform_dst_nav4_skip_link_a11y_1')) {
|
|
28
|
+
/**
|
|
29
|
+
* The consumer takes over the navigation effect (e.g. expanding the
|
|
30
|
+
* side nav and focusing the first nav item). The universal pre/post
|
|
31
|
+
* work below (e.g. `window.scrollTo`) still runs around it.
|
|
32
|
+
*/
|
|
33
|
+
navigate();
|
|
34
|
+
} else {
|
|
35
|
+
// Intentionally not using `document.querySelector` because many valid IDs are not valid selectors.
|
|
36
|
+
const target = document.getElementById(id);
|
|
37
|
+
if (!target) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
74
40
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Legacy `onBeforeNavigate` hook. Intentionally NOT called when
|
|
43
|
+
* `platform_dst_nav4_skip_link_a11y_1` is enabled — under the gate the
|
|
44
|
+
* gate-on path delegates state mutation + focus management to `navigate`,
|
|
45
|
+
* and `SkipLinksPopup` injects its popup-close behavior into the
|
|
46
|
+
* `navigate` wrapper instead of relying on this hook.
|
|
47
|
+
*
|
|
48
|
+
* This callback can be removed entirely on gate cleanup.
|
|
49
|
+
*/
|
|
50
|
+
if (!fg('platform_dst_nav4_skip_link_a11y_1')) {
|
|
51
|
+
onBeforeNavigate === null || onBeforeNavigate === void 0 ? void 0 : onBeforeNavigate();
|
|
52
|
+
}
|
|
53
|
+
focusElement(target);
|
|
79
54
|
}
|
|
80
55
|
|
|
81
|
-
/**
|
|
82
|
-
* Internal slots can attach an `onBeforeNavigate` callback.
|
|
83
|
-
*
|
|
84
|
-
* Side nav uses this to ensure it is expanded.
|
|
85
|
-
*/
|
|
86
|
-
onBeforeNavigate === null || onBeforeNavigate === void 0 ? void 0 : onBeforeNavigate();
|
|
87
|
-
focusElement(target);
|
|
88
|
-
|
|
89
56
|
/**
|
|
90
57
|
* We should look into removing this, or only calling it in specific cases.
|
|
91
58
|
*
|
|
@@ -99,7 +66,7 @@ export const SkipLink = ({
|
|
|
99
66
|
* E.g. jumping to main / aside it makes sense to look at the start of the content.
|
|
100
67
|
*/
|
|
101
68
|
window.scrollTo(0, 0);
|
|
102
|
-
}, [id, onBeforeNavigate]);
|
|
69
|
+
}, [id, onBeforeNavigate, navigate]);
|
|
103
70
|
return /*#__PURE__*/React.createElement("li", {
|
|
104
71
|
className: ax([styles.skipLinkListItem, fg('platform_dst_nav4_skip_link_a11y_1') && styles.skipLinkListItemNew])
|
|
105
72
|
}, /*#__PURE__*/React.createElement(Anchor
|
|
@@ -111,6 +78,6 @@ export const SkipLink = ({
|
|
|
111
78
|
*/, {
|
|
112
79
|
tabIndex: 0,
|
|
113
80
|
href: href,
|
|
114
|
-
onClick:
|
|
81
|
+
onClick: handleClick
|
|
115
82
|
}, children));
|
|
116
83
|
};
|
|
@@ -8,6 +8,7 @@ import { flushSync } from 'react-dom';
|
|
|
8
8
|
import Button from '@atlaskit/button/new';
|
|
9
9
|
import mergeRefs from '@atlaskit/ds-lib/merge-refs';
|
|
10
10
|
import Popup from '@atlaskit/popup';
|
|
11
|
+
import { focusElement } from './focus-element';
|
|
11
12
|
import { SkipLink } from './skip-link';
|
|
12
13
|
const contentStyles = {
|
|
13
14
|
root: "_1rjcu2gc _18zrpxbi _1e0c1txw _2lx21bp4 _1pbyjh3g",
|
|
@@ -85,13 +86,34 @@ export function SkipLinksPopup({
|
|
|
85
86
|
}, links.map(({
|
|
86
87
|
id,
|
|
87
88
|
label,
|
|
88
|
-
|
|
89
|
+
navigate
|
|
89
90
|
}) => /*#__PURE__*/React.createElement(SkipLink, {
|
|
90
91
|
key: id,
|
|
91
|
-
id: id
|
|
92
|
-
|
|
92
|
+
id: id
|
|
93
|
+
/**
|
|
94
|
+
* The popup always owns the navigation effect under the
|
|
95
|
+
* `platform_dst_nav4_skip_link_a11y_1` gate (the only path
|
|
96
|
+
* that renders `SkipLinksPopup`). It first closes itself
|
|
97
|
+
* synchronously so its focus lock is released, then either:
|
|
98
|
+
*
|
|
99
|
+
* - delegates to the consumer's `navigate` (e.g. SideNav
|
|
100
|
+
* expanding and focusing its first nav item), or
|
|
101
|
+
* - falls back to focusing the slot element with `id`.
|
|
102
|
+
*
|
|
103
|
+
* This means `SkipLink`'s `onBeforeNavigate` hook is never
|
|
104
|
+
* needed under the gate and can be removed on cleanup.
|
|
105
|
+
*/,
|
|
106
|
+
navigate: () => {
|
|
93
107
|
closePopup();
|
|
94
|
-
|
|
108
|
+
if (navigate) {
|
|
109
|
+
navigate();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Intentionally not using `document.querySelector` because many valid IDs are not valid selectors.
|
|
113
|
+
const target = document.getElementById(id);
|
|
114
|
+
if (target) {
|
|
115
|
+
focusElement(target);
|
|
116
|
+
}
|
|
95
117
|
}
|
|
96
118
|
}, label)))),
|
|
97
119
|
trigger: triggerProps => /*#__PURE__*/React.createElement("div", {
|
|
@@ -5,14 +5,15 @@ import { SkipLinksContext } from './skip-links-context';
|
|
|
5
5
|
*
|
|
6
6
|
* `useSkipLink` is the public API wrapper of this.
|
|
7
7
|
*
|
|
8
|
-
* This private version exists for us to support `onBeforeNavigate` for the side nav use case,
|
|
9
|
-
* where we might need to expand it before moving focus, without having to support
|
|
8
|
+
* This private version exists for us to support `onBeforeNavigate` / `navigate` for the side nav use case,
|
|
9
|
+
* where we might need to expand it before moving focus, without having to support those publicly.
|
|
10
10
|
*/
|
|
11
11
|
export const useSkipLinkInternal = ({
|
|
12
12
|
id,
|
|
13
13
|
label,
|
|
14
14
|
listIndex,
|
|
15
15
|
onBeforeNavigate,
|
|
16
|
+
navigate,
|
|
16
17
|
isHidden
|
|
17
18
|
}) => {
|
|
18
19
|
const {
|
|
@@ -32,10 +33,11 @@ export const useSkipLinkInternal = ({
|
|
|
32
33
|
label,
|
|
33
34
|
listIndex,
|
|
34
35
|
onBeforeNavigate,
|
|
36
|
+
navigate,
|
|
35
37
|
isHidden
|
|
36
38
|
});
|
|
37
39
|
return () => {
|
|
38
40
|
unregisterSkipLink(id);
|
|
39
41
|
};
|
|
40
|
-
}, [id, isHidden, label, listIndex, onBeforeNavigate, registerSkipLink, unregisterSkipLink]);
|
|
42
|
+
}, [id, isHidden, label, listIndex, onBeforeNavigate, navigate, registerSkipLink, unregisterSkipLink]);
|
|
41
43
|
};
|
|
@@ -9,6 +9,7 @@ import { useAnalyticsEvents } from '@atlaskit/analytics-next';
|
|
|
9
9
|
import mergeRefs from '@atlaskit/ds-lib/merge-refs';
|
|
10
10
|
import useStableRef from '@atlaskit/ds-lib/use-stable-ref';
|
|
11
11
|
import { OpenLayerObserverNamespaceProvider, useOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
|
|
12
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
12
13
|
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
13
14
|
import { media } from '@atlaskit/primitives/responsive';
|
|
14
15
|
import { useSkipLinkInternal } from '../../../context/skip-links/use-skip-link-internal';
|
|
@@ -98,28 +99,38 @@ function SideNavInternal({
|
|
|
98
99
|
const id = useLayoutId({
|
|
99
100
|
providedId
|
|
100
101
|
});
|
|
101
|
-
const
|
|
102
|
+
const expandAndFocusSideNav = useExpandSideNav({
|
|
102
103
|
trigger: 'skip-link'
|
|
103
104
|
});
|
|
104
105
|
/**
|
|
105
106
|
* Called after clicking on the side nav skip link, and ensures the side nav is expanded so that it is focusable.
|
|
106
107
|
*
|
|
107
108
|
* We need to update the DOM synchronously because `.focus()` is called synchronously after this state update.
|
|
109
|
+
*
|
|
110
|
+
* Only used when `platform_dst_nav4_skip_link_a11y_1` is OFF; can be removed on gate cleanup.
|
|
108
111
|
*/
|
|
109
112
|
const synchronouslyExpandSideNav = useCallback(() => {
|
|
110
113
|
flushSync(() => {
|
|
111
114
|
/**
|
|
112
115
|
* Calling this unconditionally and relying on it to avoid no-op renders.
|
|
113
116
|
*
|
|
114
|
-
* We _could_ call it conditionally, but we'd be duplicating the screen size checks `
|
|
117
|
+
* We _could_ call it conditionally, but we'd be duplicating the screen size checks `expandAndFocusSideNav` makes.
|
|
115
118
|
*/
|
|
116
|
-
|
|
119
|
+
expandAndFocusSideNav();
|
|
117
120
|
});
|
|
118
|
-
}, [
|
|
121
|
+
}, [expandAndFocusSideNav]);
|
|
119
122
|
useSkipLinkInternal({
|
|
120
123
|
id,
|
|
121
124
|
label: skipLinkLabel,
|
|
122
|
-
|
|
125
|
+
/**
|
|
126
|
+
* `navigate` is the gate-on contract: it owns expanding the side nav AND moving
|
|
127
|
+
* focus to the first nav item, atomically (via `useExpandSideNav`'s `flushSync`).
|
|
128
|
+
*
|
|
129
|
+
* `onBeforeNavigate` is the legacy contract used only when the gate is OFF.
|
|
130
|
+
* On gate cleanup, drop `onBeforeNavigate` and `synchronouslyExpandSideNav` here.
|
|
131
|
+
*/
|
|
132
|
+
navigate: fg('platform_dst_nav4_skip_link_a11y_1') ? expandAndFocusSideNav : undefined,
|
|
133
|
+
onBeforeNavigate: fg('platform_dst_nav4_skip_link_a11y_1') ? undefined : synchronouslyExpandSideNav
|
|
123
134
|
});
|
|
124
135
|
const sideNavState = useContext(SideNavVisibilityState);
|
|
125
136
|
const setSideNavState = useContext(SetSideNavVisibilityState);
|
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
import { useCallback, useContext } from 'react';
|
|
2
|
+
import { bind } from 'bind-event-listener';
|
|
3
|
+
import { flushSync } from 'react-dom';
|
|
4
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
2
5
|
import { SetSideNavVisibilityState } from './set-side-nav-visibility-state';
|
|
6
|
+
import { useSideNavRef } from './use-side-nav-ref';
|
|
7
|
+
/**
|
|
8
|
+
* Moves focus to the first focusable item in the side nav, or the side nav element itself as a fallback.
|
|
9
|
+
*/
|
|
10
|
+
function focusFirstNavItem(sideNavElement) {
|
|
11
|
+
var _firstNavItem$checkVi, _firstNavItem$checkVi2;
|
|
12
|
+
/**
|
|
13
|
+
* Try to get the first focusable item in the side nav.
|
|
14
|
+
* The selector is not very broad, but should be appropriate for items from this package.
|
|
15
|
+
*/
|
|
16
|
+
const firstNavItem = sideNavElement.querySelector('a, button');
|
|
17
|
+
const isFirstNavItemVisible = firstNavItem !== null && ((_firstNavItem$checkVi = (_firstNavItem$checkVi2 = firstNavItem.checkVisibility) === null || _firstNavItem$checkVi2 === void 0 ? void 0 : _firstNavItem$checkVi2.call(firstNavItem)) !== null && _firstNavItem$checkVi !== void 0 ? _firstNavItem$checkVi : false);
|
|
18
|
+
const itemToFocus = isFirstNavItemVisible ? firstNavItem : sideNavElement;
|
|
19
|
+
if (itemToFocus === sideNavElement) {
|
|
20
|
+
/**
|
|
21
|
+
* Elements without an explicit `tabindex` attribute are not guaranteed to be focusable:
|
|
22
|
+
* https://html.spec.whatwg.org/multipage/interaction.html#attr-tabindex
|
|
23
|
+
*
|
|
24
|
+
* Our slots are not interactive, so this is required.
|
|
25
|
+
*
|
|
26
|
+
* In the future we may want to check if there is an existing `tabindex` attribute,
|
|
27
|
+
* as custom skip linked elements might already have one.
|
|
28
|
+
*/
|
|
29
|
+
sideNavElement.setAttribute('tabindex', '-1');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Cleanup the `tabindex` attribute we set when the slot or custom target loses focus.
|
|
33
|
+
*
|
|
34
|
+
* This is preferable to always having `tabindex="-1"` because always applying the tab index can:
|
|
35
|
+
*
|
|
36
|
+
* - mess with click events
|
|
37
|
+
* - potentially cause a focus ring to be always visible
|
|
38
|
+
*/
|
|
39
|
+
bind(sideNavElement, {
|
|
40
|
+
type: 'blur',
|
|
41
|
+
listener() {
|
|
42
|
+
sideNavElement.removeAttribute('tabindex');
|
|
43
|
+
},
|
|
44
|
+
options: {
|
|
45
|
+
once: true
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Not using `focusVisible` option because we don't want clicks on the toggle button to show a focus ring.
|
|
52
|
+
*/
|
|
53
|
+
itemToFocus.focus();
|
|
54
|
+
}
|
|
55
|
+
const triggersWithFocusOnExpand = new Set(['toggle-button', 'skip-link']);
|
|
56
|
+
|
|
3
57
|
/**
|
|
4
58
|
* __useExpandSideNav__
|
|
5
59
|
*
|
|
@@ -13,49 +67,61 @@ export function useExpandSideNav({
|
|
|
13
67
|
trigger = 'programmatic'
|
|
14
68
|
} = {}) {
|
|
15
69
|
const setSideNavState = useContext(SetSideNavVisibilityState);
|
|
70
|
+
const sideNavRef = useSideNavRef();
|
|
16
71
|
const expandSideNav = useCallback(() => {
|
|
17
72
|
const {
|
|
18
|
-
matches
|
|
73
|
+
matches: isDesktop
|
|
19
74
|
} = window.matchMedia('(min-width: 64rem)');
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
75
|
+
const runUpdate = () => {
|
|
76
|
+
if (isDesktop) {
|
|
77
|
+
setSideNavState(currentState => {
|
|
78
|
+
// No-op if the side nav state has not been initialised yet
|
|
79
|
+
// e.g. if the SideNav has not been mounted yet
|
|
80
|
+
if (!currentState) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
27
83
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
84
|
+
// Skip the re-render if it's a no-op change
|
|
85
|
+
if (currentState.desktop === 'expanded' && currentState.flyout === 'closed') {
|
|
86
|
+
return currentState;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
mobile: currentState.mobile,
|
|
90
|
+
desktop: 'expanded',
|
|
91
|
+
flyout: 'closed',
|
|
92
|
+
lastTrigger: trigger
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
setSideNavState(currentState => {
|
|
97
|
+
// No-op if the side nav state has not been initialised yet
|
|
98
|
+
// e.g. if the SideNav has not been mounted yet
|
|
99
|
+
if (!currentState) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
46
102
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
// Skip the re-render if it's a no-op change
|
|
104
|
+
if (currentState.mobile === 'expanded' && currentState.flyout === 'closed') {
|
|
105
|
+
return currentState;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
desktop: currentState.desktop,
|
|
109
|
+
mobile: 'expanded',
|
|
110
|
+
flyout: 'closed',
|
|
111
|
+
lastTrigger: trigger
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
if (triggersWithFocusOnExpand.has(trigger) && fg('platform_dst_nav4_skip_link_a11y_1')) {
|
|
117
|
+
flushSync(runUpdate);
|
|
118
|
+
const sideNavElement = sideNavRef.current;
|
|
119
|
+
if (sideNavElement) {
|
|
120
|
+
focusFirstNavItem(sideNavElement);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
runUpdate();
|
|
58
124
|
}
|
|
59
|
-
}, [setSideNavState, trigger]);
|
|
125
|
+
}, [setSideNavState, sideNavRef, trigger]);
|
|
60
126
|
return expandSideNav;
|
|
61
127
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useCallback, useContext } from 'react';
|
|
2
|
+
import { fg } from '@atlaskit/platform-feature-flags';
|
|
2
3
|
import { SetSideNavVisibilityState } from './set-side-nav-visibility-state';
|
|
4
|
+
import { SideNavVisibilityState } from './side-nav-visibility-state';
|
|
5
|
+
import { useExpandSideNav } from './use-expand-side-nav';
|
|
3
6
|
/**
|
|
4
7
|
* __useToggleSideNav__
|
|
5
8
|
*
|
|
@@ -13,11 +16,38 @@ export function useToggleSideNav({
|
|
|
13
16
|
trigger = 'programmatic'
|
|
14
17
|
} = {}) {
|
|
15
18
|
const setSideNavState = useContext(SetSideNavVisibilityState);
|
|
19
|
+
const sideNavState = useContext(SideNavVisibilityState);
|
|
20
|
+
const expandSideNav = useExpandSideNav({
|
|
21
|
+
trigger
|
|
22
|
+
});
|
|
23
|
+
const isCollapsedOnDesktop = (sideNavState === null || sideNavState === void 0 ? void 0 : sideNavState.desktop) === 'collapsed';
|
|
24
|
+
const isCollapsedOnMobile = (sideNavState === null || sideNavState === void 0 ? void 0 : sideNavState.mobile) === 'collapsed';
|
|
16
25
|
const toggleSideNav = useCallback(() => {
|
|
17
26
|
const {
|
|
18
|
-
matches
|
|
27
|
+
matches: isDesktop
|
|
19
28
|
} = window.matchMedia('(min-width: 64rem)');
|
|
20
|
-
if (
|
|
29
|
+
if (fg('platform_dst_nav4_skip_link_a11y_1')) {
|
|
30
|
+
const isExpanding = isDesktop ? isCollapsedOnDesktop : isCollapsedOnMobile;
|
|
31
|
+
if (isExpanding) {
|
|
32
|
+
expandSideNav();
|
|
33
|
+
} else {
|
|
34
|
+
setSideNavState(currentState => {
|
|
35
|
+
// No-op if the side nav state has not been initialised yet
|
|
36
|
+
// e.g. if the SideNav has not been mounted yet
|
|
37
|
+
if (!currentState) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
mobile: isDesktop ? currentState.mobile : 'collapsed',
|
|
42
|
+
desktop: isDesktop ? 'collapsed' : currentState.desktop,
|
|
43
|
+
flyout: 'closed',
|
|
44
|
+
lastTrigger: trigger
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (isDesktop) {
|
|
21
51
|
setSideNavState(currentState => {
|
|
22
52
|
// No-op if the side nav state has not been initialised yet
|
|
23
53
|
// e.g. if the SideNav has not been mounted yet
|
|
@@ -46,6 +76,6 @@ export function useToggleSideNav({
|
|
|
46
76
|
};
|
|
47
77
|
});
|
|
48
78
|
}
|
|
49
|
-
}, [setSideNavState, trigger]);
|
|
79
|
+
}, [expandSideNav, isCollapsedOnDesktop, isCollapsedOnMobile, setSideNavState, trigger]);
|
|
50
80
|
return toggleSideNav;
|
|
51
81
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { bind } from 'bind-event-listener';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Used for moving focus to the corresponding slot or custom target after clicking on a skip link.
|
|
5
|
+
*/
|
|
6
|
+
export function focusElement(element) {
|
|
7
|
+
/**
|
|
8
|
+
* Elements without an explicit `tabindex` attribute are not guaranteed to be focusable:
|
|
9
|
+
* https://html.spec.whatwg.org/multipage/interaction.html#attr-tabindex
|
|
10
|
+
*
|
|
11
|
+
* Our slots are not interactive, so this is required.
|
|
12
|
+
*
|
|
13
|
+
* In the future we may want to check if there is an existing `tabindex` attribute,
|
|
14
|
+
* as custom skip linked elements might already have one.
|
|
15
|
+
*/
|
|
16
|
+
element.setAttribute('tabindex', '-1');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Cleanup the `tabindex` attribute we set when the slot or custom target loses focus.
|
|
20
|
+
*
|
|
21
|
+
* This is preferable to always having `tabindex="-1"` because always applying the tab index can:
|
|
22
|
+
*
|
|
23
|
+
* - mess with click events
|
|
24
|
+
* - potentially cause a focus ring to be always visible
|
|
25
|
+
*/
|
|
26
|
+
bind(element, {
|
|
27
|
+
type: 'blur',
|
|
28
|
+
listener: function listener() {
|
|
29
|
+
element.removeAttribute('tabindex');
|
|
30
|
+
},
|
|
31
|
+
options: {
|
|
32
|
+
// Using a one-time listener so it cleans itself up
|
|
33
|
+
once: true
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Move focus to the slot or custom target.
|
|
39
|
+
*
|
|
40
|
+
* Calling `.focus()` will also scroll the element into view:
|
|
41
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
|
|
42
|
+
*/
|
|
43
|
+
element.focus({
|
|
44
|
+
// Forces the focus ring to appear after moving focus to the slot
|
|
45
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#focusvisible
|
|
46
|
+
// @ts-expect-error - new and not in types yet
|
|
47
|
+
focusVisible: true
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -2,62 +2,14 @@
|
|
|
2
2
|
import "./skip-link.compiled.css";
|
|
3
3
|
import { ax, ix } from "@compiled/react/runtime";
|
|
4
4
|
import React, { useCallback } from 'react';
|
|
5
|
-
import { bind } from 'bind-event-listener';
|
|
6
5
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
7
6
|
// eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- to be migrated to @atlaskit/primitives/compiled – go/akcss
|
|
8
7
|
import { Anchor } from '@atlaskit/primitives/compiled';
|
|
8
|
+
import { focusElement } from './focus-element';
|
|
9
9
|
var styles = {
|
|
10
10
|
skipLinkListItem: "_1pfhze3t",
|
|
11
11
|
skipLinkListItemNew: "_1rjcu2gc"
|
|
12
12
|
};
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Used for moving focus to the corresponding slot or custom target after clicking on a skip link.
|
|
16
|
-
*/
|
|
17
|
-
function focusElement(element) {
|
|
18
|
-
/**
|
|
19
|
-
* Elements without an explicit `tabindex` attribute are not guaranteed to be focusable:
|
|
20
|
-
* https://html.spec.whatwg.org/multipage/interaction.html#attr-tabindex
|
|
21
|
-
*
|
|
22
|
-
* Our slots are not interactive, so this is required.
|
|
23
|
-
*
|
|
24
|
-
* In the future we may want to check if there is an existing `tabindex` attribute,
|
|
25
|
-
* as custom skip linked elements might already have one.
|
|
26
|
-
*/
|
|
27
|
-
element.setAttribute('tabindex', '-1');
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Cleanup the `tabindex` attribute we set when the slot or custom target loses focus.
|
|
31
|
-
*
|
|
32
|
-
* This is preferable to always having `tabindex="-1"` because always applying the tab index can:
|
|
33
|
-
*
|
|
34
|
-
* - mess with click events
|
|
35
|
-
* - potentially cause a focus ring to be always visible
|
|
36
|
-
*/
|
|
37
|
-
bind(element, {
|
|
38
|
-
type: 'blur',
|
|
39
|
-
listener: function listener() {
|
|
40
|
-
element.removeAttribute('tabindex');
|
|
41
|
-
},
|
|
42
|
-
options: {
|
|
43
|
-
// Using a one-time listener so it cleans itself up
|
|
44
|
-
once: true
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Move focus to the slot or custom target.
|
|
50
|
-
*
|
|
51
|
-
* Calling `.focus()` will also scroll the element into view:
|
|
52
|
-
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
|
|
53
|
-
*/
|
|
54
|
-
element.focus({
|
|
55
|
-
// Forces the focus ring to appear after moving focus to the slot
|
|
56
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#focusvisible
|
|
57
|
-
// @ts-expect-error - new and not in types yet
|
|
58
|
-
focusVisible: true
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
13
|
/**
|
|
62
14
|
* A link that moves current tab position to a different element
|
|
63
15
|
*
|
|
@@ -66,25 +18,40 @@ function focusElement(element) {
|
|
|
66
18
|
export var SkipLink = function SkipLink(_ref) {
|
|
67
19
|
var id = _ref.id,
|
|
68
20
|
children = _ref.children,
|
|
69
|
-
onBeforeNavigate = _ref.onBeforeNavigate
|
|
21
|
+
onBeforeNavigate = _ref.onBeforeNavigate,
|
|
22
|
+
navigate = _ref.navigate;
|
|
70
23
|
var href = "#".concat(id);
|
|
71
|
-
var
|
|
24
|
+
var handleClick = useCallback(function (event) {
|
|
72
25
|
event.preventDefault();
|
|
26
|
+
if (navigate && fg('platform_dst_nav4_skip_link_a11y_1')) {
|
|
27
|
+
/**
|
|
28
|
+
* The consumer takes over the navigation effect (e.g. expanding the
|
|
29
|
+
* side nav and focusing the first nav item). The universal pre/post
|
|
30
|
+
* work below (e.g. `window.scrollTo`) still runs around it.
|
|
31
|
+
*/
|
|
32
|
+
navigate();
|
|
33
|
+
} else {
|
|
34
|
+
// Intentionally not using `document.querySelector` because many valid IDs are not valid selectors.
|
|
35
|
+
var target = document.getElementById(id);
|
|
36
|
+
if (!target) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
73
39
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Legacy `onBeforeNavigate` hook. Intentionally NOT called when
|
|
42
|
+
* `platform_dst_nav4_skip_link_a11y_1` is enabled — under the gate the
|
|
43
|
+
* gate-on path delegates state mutation + focus management to `navigate`,
|
|
44
|
+
* and `SkipLinksPopup` injects its popup-close behavior into the
|
|
45
|
+
* `navigate` wrapper instead of relying on this hook.
|
|
46
|
+
*
|
|
47
|
+
* This callback can be removed entirely on gate cleanup.
|
|
48
|
+
*/
|
|
49
|
+
if (!fg('platform_dst_nav4_skip_link_a11y_1')) {
|
|
50
|
+
onBeforeNavigate === null || onBeforeNavigate === void 0 || onBeforeNavigate();
|
|
51
|
+
}
|
|
52
|
+
focusElement(target);
|
|
78
53
|
}
|
|
79
54
|
|
|
80
|
-
/**
|
|
81
|
-
* Internal slots can attach an `onBeforeNavigate` callback.
|
|
82
|
-
*
|
|
83
|
-
* Side nav uses this to ensure it is expanded.
|
|
84
|
-
*/
|
|
85
|
-
onBeforeNavigate === null || onBeforeNavigate === void 0 || onBeforeNavigate();
|
|
86
|
-
focusElement(target);
|
|
87
|
-
|
|
88
55
|
/**
|
|
89
56
|
* We should look into removing this, or only calling it in specific cases.
|
|
90
57
|
*
|
|
@@ -98,7 +65,7 @@ export var SkipLink = function SkipLink(_ref) {
|
|
|
98
65
|
* E.g. jumping to main / aside it makes sense to look at the start of the content.
|
|
99
66
|
*/
|
|
100
67
|
window.scrollTo(0, 0);
|
|
101
|
-
}, [id, onBeforeNavigate]);
|
|
68
|
+
}, [id, onBeforeNavigate, navigate]);
|
|
102
69
|
return /*#__PURE__*/React.createElement("li", {
|
|
103
70
|
className: ax([styles.skipLinkListItem, fg('platform_dst_nav4_skip_link_a11y_1') && styles.skipLinkListItemNew])
|
|
104
71
|
}, /*#__PURE__*/React.createElement(Anchor
|
|
@@ -110,6 +77,6 @@ export var SkipLink = function SkipLink(_ref) {
|
|
|
110
77
|
*/, {
|
|
111
78
|
tabIndex: 0,
|
|
112
79
|
href: href,
|
|
113
|
-
onClick:
|
|
80
|
+
onClick: handleClick
|
|
114
81
|
}, children));
|
|
115
82
|
};
|