@atlaskit/navigation-system 9.3.1 → 9.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/constellation/index/migration-guide.mdx +338 -150
  3. package/dist/cjs/components/skip-links/focus-element.js +54 -0
  4. package/dist/cjs/components/skip-links/skip-link.js +32 -65
  5. package/dist/cjs/components/skip-links/skip-links-popup.js +26 -4
  6. package/dist/cjs/context/skip-links/use-skip-link-internal.js +5 -3
  7. package/dist/cjs/ui/page-layout/side-nav/side-nav.js +16 -5
  8. package/dist/cjs/ui/page-layout/side-nav/use-expand-side-nav.js +104 -38
  9. package/dist/cjs/ui/page-layout/side-nav/use-toggle-side-nav.js +33 -3
  10. package/dist/es2019/components/skip-links/focus-element.js +49 -0
  11. package/dist/es2019/components/skip-links/skip-link.js +32 -65
  12. package/dist/es2019/components/skip-links/skip-links-popup.js +26 -4
  13. package/dist/es2019/context/skip-links/use-skip-link-internal.js +5 -3
  14. package/dist/es2019/ui/page-layout/side-nav/side-nav.js +16 -5
  15. package/dist/es2019/ui/page-layout/side-nav/use-expand-side-nav.js +104 -38
  16. package/dist/es2019/ui/page-layout/side-nav/use-toggle-side-nav.js +33 -3
  17. package/dist/esm/components/skip-links/focus-element.js +49 -0
  18. package/dist/esm/components/skip-links/skip-link.js +32 -65
  19. package/dist/esm/components/skip-links/skip-links-popup.js +26 -4
  20. package/dist/esm/context/skip-links/use-skip-link-internal.js +5 -3
  21. package/dist/esm/ui/page-layout/side-nav/side-nav.js +16 -5
  22. package/dist/esm/ui/page-layout/side-nav/use-expand-side-nav.js +104 -38
  23. package/dist/esm/ui/page-layout/side-nav/use-toggle-side-nav.js +33 -3
  24. package/dist/types/components/skip-links/focus-element.d.ts +4 -0
  25. package/dist/types/components/skip-links/skip-link.d.ts +2 -1
  26. package/dist/types/context/skip-links/types.d.ts +18 -1
  27. package/dist/types/context/skip-links/use-skip-link-internal.d.ts +3 -3
  28. package/dist/types-ts4.5/components/skip-links/focus-element.d.ts +4 -0
  29. package/dist/types-ts4.5/components/skip-links/skip-link.d.ts +2 -1
  30. package/dist/types-ts4.5/context/skip-links/types.d.ts +18 -1
  31. package/dist/types-ts4.5/context/skip-links/use-skip-link-internal.d.ts +3 -3
  32. package/package.json +4 -4
@@ -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 onClick = (0, _react.useCallback)(function (event) {
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
- // Intentionally not using `document.querySelector` because many valid IDs are not valid selectors.
83
- var target = document.getElementById(id);
84
- if (!target) {
85
- return;
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: 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
- _onBeforeNavigate = _ref2.onBeforeNavigate;
102
+ _navigate = _ref2.navigate;
102
103
  return /*#__PURE__*/React.createElement(_skipLink.SkipLink, {
103
104
  key: id,
104
- id: id,
105
- onBeforeNavigate: function onBeforeNavigate() {
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
- _onBeforeNavigate === null || _onBeforeNavigate === void 0 || _onBeforeNavigate();
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 `onBeforeNavigate` publicly.
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 expandSideNav = (0, _useExpandSideNav.useExpandSideNav)({
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 `expandSideNav` makes.
133
+ * We _could_ call it conditionally, but we'd be duplicating the screen size checks `expandAndFocusSideNav` makes.
131
134
  */
132
- expandSideNav();
135
+ expandAndFocusSideNav();
133
136
  });
134
- }, [expandSideNav]);
137
+ }, [expandAndFocusSideNav]);
135
138
  (0, _useSkipLinkInternal.useSkipLinkInternal)({
136
139
  id: id,
137
140
  label: skipLinkLabel,
138
- onBeforeNavigate: synchronouslyExpandSideNav
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
- matches = _window$matchMedia.matches;
26
- if (matches) {
27
- setSideNavState(function (currentState) {
28
- // No-op if the side nav state has not been initialised yet
29
- // e.g. if the SideNav has not been mounted yet
30
- if (!currentState) {
31
- return null;
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
- // Skip the re-render if it's a no-op change
35
- if (currentState.desktop === 'expanded' && currentState.flyout === 'closed') {
36
- return currentState;
37
- }
38
- return {
39
- mobile: currentState.mobile,
40
- desktop: 'expanded',
41
- flyout: 'closed',
42
- lastTrigger: trigger
43
- };
44
- });
45
- } else {
46
- setSideNavState(function (currentState) {
47
- // No-op if the side nav state has not been initialised yet
48
- // e.g. if the SideNav has not been mounted yet
49
- if (!currentState) {
50
- return null;
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
- // Skip the re-render if it's a no-op change
54
- if (currentState.mobile === 'expanded' && currentState.flyout === 'closed') {
55
- return currentState;
56
- }
57
- return {
58
- desktop: currentState.desktop,
59
- mobile: 'expanded',
60
- flyout: 'closed',
61
- lastTrigger: trigger
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
- matches = _window$matchMedia.matches;
26
- if (matches) {
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
+ }