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