@atlaskit/navigation-system 4.4.0 → 4.5.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.
Files changed (22) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cjs/ui/page-layout/root.js +8 -3
  3. package/dist/cjs/ui/page-layout/side-nav/is-side-nav-shortcut-enabled-context.js +36 -0
  4. package/dist/cjs/ui/page-layout/side-nav/side-nav.js +9 -2
  5. package/dist/cjs/ui/page-layout/side-nav/use-side-nav-toggle-keyboard-shortcut.js +59 -0
  6. package/dist/es2019/ui/page-layout/root.js +7 -3
  7. package/dist/es2019/ui/page-layout/side-nav/is-side-nav-shortcut-enabled-context.js +29 -0
  8. package/dist/es2019/ui/page-layout/side-nav/side-nav.js +9 -2
  9. package/dist/es2019/ui/page-layout/side-nav/use-side-nav-toggle-keyboard-shortcut.js +54 -0
  10. package/dist/esm/ui/page-layout/root.js +8 -3
  11. package/dist/esm/ui/page-layout/side-nav/is-side-nav-shortcut-enabled-context.js +28 -0
  12. package/dist/esm/ui/page-layout/side-nav/side-nav.js +9 -2
  13. package/dist/esm/ui/page-layout/side-nav/use-side-nav-toggle-keyboard-shortcut.js +53 -0
  14. package/dist/types/ui/page-layout/root.d.ts +19 -1
  15. package/dist/types/ui/page-layout/side-nav/is-side-nav-shortcut-enabled-context.d.ts +13 -0
  16. package/dist/types/ui/page-layout/side-nav/side-nav.d.ts +13 -1
  17. package/dist/types/ui/page-layout/side-nav/use-side-nav-toggle-keyboard-shortcut.d.ts +6 -0
  18. package/dist/types-ts4.5/ui/page-layout/root.d.ts +19 -1
  19. package/dist/types-ts4.5/ui/page-layout/side-nav/is-side-nav-shortcut-enabled-context.d.ts +13 -0
  20. package/dist/types-ts4.5/ui/page-layout/side-nav/side-nav.d.ts +13 -1
  21. package/dist/types-ts4.5/ui/page-layout/side-nav/use-side-nav-toggle-keyboard-shortcut.d.ts +6 -0
  22. package/package.json +7 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @atlassian/navigation-system
2
2
 
3
+ ## 4.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`8a71ce992f8c8`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/8a71ce992f8c8) -
8
+ `SideNav` now provides a built-in keyboard shortcut for expanding and collapsing. This is behind
9
+ the feature gate `navx-full-height-sidebar`.
10
+ - The shortcut key is `Ctrl` + `[`.
11
+ - The shortcut is not enabled by default.
12
+ - The prop `isSideNavShortcutEnabled` has been added to `Root`, as a way to control whether the
13
+ shortcut is enabled (whether the keyboard event listener is binded). It defaults to `false`.
14
+ - The prop `canToggleWithShortcut()` has been added to `SideNav`, as a way to run additional
15
+ checks after the shortcut is pressed, before the side nav is toggled.
16
+ - The shortcut will also be ignored if there are any open modals. This check is behind the feature
17
+ gate `platform-dst-open-layer-observer-layer-type`.
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+
3
23
  ## 4.4.0
4
24
 
5
25
  ### Minor Changes
@@ -15,6 +15,7 @@ var _skipLinksProvider = require("../../context/skip-links/skip-links-provider")
15
15
  var _topNavStartContextProvider = require("../../context/top-nav-start/top-nav-start-context-provider");
16
16
  var _hoistSlotSizesContext = require("./hoist-slot-sizes-context");
17
17
  var _elementContext = require("./side-nav/element-context");
18
+ var _isSideNavShortcutEnabledContext = require("./side-nav/is-side-nav-shortcut-enabled-context");
18
19
  var _toggleButtonProvider = require("./side-nav/toggle-button-provider");
19
20
  var _visibilityProvider = require("./side-nav/visibility-provider");
20
21
  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); }
@@ -36,7 +37,9 @@ function Root(_ref) {
36
37
  _ref$skipLinksLabel = _ref.skipLinksLabel,
37
38
  skipLinksLabel = _ref$skipLinksLabel === void 0 ? 'Skip to:' : _ref$skipLinksLabel,
38
39
  testId = _ref.testId,
39
- defaultSideNavCollapsed = _ref.defaultSideNavCollapsed;
40
+ defaultSideNavCollapsed = _ref.defaultSideNavCollapsed,
41
+ _ref$isSideNavShortcu = _ref.isSideNavShortcutEnabled,
42
+ isSideNavShortcutEnabled = _ref$isSideNavShortcu === void 0 ? false : _ref$isSideNavShortcu;
40
43
  var ref = (0, _react.useRef)(null);
41
44
  (0, _react.useEffect)(function () {
42
45
  if (process.env.NODE_ENV !== 'production') {
@@ -53,7 +56,9 @@ function Root(_ref) {
53
56
  }, []);
54
57
  return /*#__PURE__*/_react.default.createElement(_visibilityProvider.SideNavVisibilityProvider, {
55
58
  defaultCollapsed: defaultSideNavCollapsed
56
- }, /*#__PURE__*/_react.default.createElement(_toggleButtonProvider.SideNavToggleButtonProvider, null, /*#__PURE__*/_react.default.createElement(_elementContext.SideNavElementProvider, null, /*#__PURE__*/_react.default.createElement(_topNavStartContextProvider.TopNavStartProvider, null, /*#__PURE__*/_react.default.createElement(_openLayerObserver.OpenLayerObserver, null, /*#__PURE__*/_react.default.createElement(_hoistSlotSizesContext.DangerouslyHoistSlotSizes.Provider, {
59
+ }, /*#__PURE__*/_react.default.createElement(_toggleButtonProvider.SideNavToggleButtonProvider, null, /*#__PURE__*/_react.default.createElement(_elementContext.SideNavElementProvider, null, /*#__PURE__*/_react.default.createElement(_isSideNavShortcutEnabledContext.IsSideNavShortcutEnabledProvider, {
60
+ isSideNavShortcutEnabled: isSideNavShortcutEnabled
61
+ }, /*#__PURE__*/_react.default.createElement(_topNavStartContextProvider.TopNavStartProvider, null, /*#__PURE__*/_react.default.createElement(_openLayerObserver.OpenLayerObserver, null, /*#__PURE__*/_react.default.createElement(_hoistSlotSizesContext.DangerouslyHoistSlotSizes.Provider, {
57
62
  value: UNSAFE_dangerouslyHoistSlotSizes
58
63
  }, /*#__PURE__*/_react.default.createElement(_skipLinksProvider.SkipLinksProvider, {
59
64
  label: skipLinksLabel,
@@ -63,5 +68,5 @@ function Root(_ref) {
63
68
  className: (0, _runtime.ax)([styles.root, xcss]),
64
69
  id: gridRootId,
65
70
  "data-testid": testId
66
- }, children))))))));
71
+ }, children)))))))));
67
72
  }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ var _typeof = require("@babel/runtime/helpers/typeof");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.IsSideNavShortcutEnabledProvider = IsSideNavShortcutEnabledProvider;
8
+ exports.useIsSideNavShortcutEnabled = useIsSideNavShortcutEnabled;
9
+ var _react = _interopRequireWildcard(require("react"));
10
+ 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); }
11
+ /**
12
+ * Context for whether the side nav toggle shortcut is enabled.
13
+ *
14
+ * Used to share the `isSideNavShortcutEnabled` prop value from `Root` with other components,
15
+ * so the visual keyboard shortcut in tooltips can be conditionally displayed.
16
+ */
17
+ var IsSideNavShortcutEnabledContext = /*#__PURE__*/(0, _react.createContext)(false);
18
+
19
+ /**
20
+ * Provider for the `IsSideNavShortcutEnabledContext`.
21
+ */
22
+ function IsSideNavShortcutEnabledProvider(_ref) {
23
+ var children = _ref.children,
24
+ isSideNavShortcutEnabled = _ref.isSideNavShortcutEnabled;
25
+ return /*#__PURE__*/_react.default.createElement(IsSideNavShortcutEnabledContext.Provider, {
26
+ value: isSideNavShortcutEnabled
27
+ }, children);
28
+ }
29
+
30
+ /**
31
+ * Returns the value of the `isSideNavShortcutEnabled` prop from the `Root` component, which
32
+ * is shared through context.
33
+ */
34
+ function useIsSideNavShortcutEnabled() {
35
+ return (0, _react.useContext)(IsSideNavShortcutEnabledContext);
36
+ }
@@ -36,6 +36,7 @@ var _elementContext = require("./element-context");
36
36
  var _flyoutCloseDelayMs = require("./flyout-close-delay-ms");
37
37
  var _toggleButtonContext = require("./toggle-button-context");
38
38
  var _useExpandSideNav = require("./use-expand-side-nav");
39
+ var _useSideNavToggleKeyboardShortcut = require("./use-side-nav-toggle-keyboard-shortcut");
39
40
  var _useSideNavVisibility2 = require("./use-side-nav-visibility");
40
41
  var _useSideNavVisibilityCallbacks = require("./use-side-nav-visibility-callbacks");
41
42
  var _useToggleSideNav = require("./use-toggle-side-nav");
@@ -100,7 +101,8 @@ function SideNavInternal(_ref) {
100
101
  onCollapse = _ref.onCollapse,
101
102
  onPeekStart = _ref.onPeekStart,
102
103
  onPeekEnd = _ref.onPeekEnd,
103
- providedId = _ref.id;
104
+ providedId = _ref.id,
105
+ canToggleWithShortcut = _ref.canToggleWithShortcut;
104
106
  var id = (0, _idUtils.useLayoutId)({
105
107
  providedId: providedId
106
108
  });
@@ -645,6 +647,9 @@ function SideNavInternal(_ref) {
645
647
  }
646
648
  devTimeOnlyAttributes['data-visible'] = visible.length ? visible.join(',') : 'false';
647
649
  }
650
+ (0, _useSideNavToggleKeyboardShortcut.useSideNavToggleKeyboardShortcut)({
651
+ canToggleWithShortcut: canToggleWithShortcut
652
+ });
648
653
  (0, _useResizingWidthCssVarOnRootElement.useResizingWidthCssVarOnRootElement)({
649
654
  isEnabled: true,
650
655
  cssVar: panelSplitterResizingVar,
@@ -746,6 +751,7 @@ function SideNav(_ref8) {
746
751
  onCollapse = _ref8.onCollapse,
747
752
  onPeekStart = _ref8.onPeekStart,
748
753
  onPeekEnd = _ref8.onPeekEnd,
754
+ canToggleWithShortcut = _ref8.canToggleWithShortcut,
749
755
  id = _ref8.id;
750
756
  return /*#__PURE__*/React.createElement(_openLayerObserver.OpenLayerObserverNamespaceProvider, {
751
757
  namespace: openLayerObserverSideNavNamespace
@@ -759,6 +765,7 @@ function SideNav(_ref8) {
759
765
  onCollapse: onCollapse,
760
766
  onPeekStart: onPeekStart,
761
767
  onPeekEnd: onPeekEnd,
762
- id: id
768
+ id: id,
769
+ canToggleWithShortcut: canToggleWithShortcut
763
770
  }, children));
764
771
  }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.useSideNavToggleKeyboardShortcut = useSideNavToggleKeyboardShortcut;
8
+ var _react = require("react");
9
+ var _bindEventListener = require("bind-event-listener");
10
+ var _useStableRef = _interopRequireDefault(require("@atlaskit/ds-lib/use-stable-ref"));
11
+ var _openLayerObserver = require("@atlaskit/layering/experimental/open-layer-observer");
12
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
13
+ var _isSideNavShortcutEnabledContext = require("./is-side-nav-shortcut-enabled-context");
14
+ var _useToggleSideNav = require("./use-toggle-side-nav");
15
+ /**
16
+ * Binds the keyboard shortcut to toggle the side nav.
17
+ */
18
+ function useSideNavToggleKeyboardShortcut(_ref) {
19
+ var canToggleWithShortcut = _ref.canToggleWithShortcut;
20
+ var openLayerObserver = (0, _openLayerObserver.useOpenLayerObserver)();
21
+ var toggleVisibilityByShortcut = (0, _useToggleSideNav.useToggleSideNav)({
22
+ trigger: 'keyboard'
23
+ });
24
+ var canToggleWithShortcutStableRef = (0, _useStableRef.default)(canToggleWithShortcut);
25
+ var isSideNavShortcutEnabled = (0, _isSideNavShortcutEnabledContext.useIsSideNavShortcutEnabled)();
26
+ (0, _react.useEffect)(function () {
27
+ if (!(0, _platformFeatureFlags.fg)('navx-full-height-sidebar')) {
28
+ return;
29
+ }
30
+ if (!isSideNavShortcutEnabled) {
31
+ return;
32
+ }
33
+ return (0, _bindEventListener.bind)(window, {
34
+ type: 'keydown',
35
+ listener: function listener(event) {
36
+ if (event.ctrlKey && event.key === '[') {
37
+ if (canToggleWithShortcutStableRef.current && !canToggleWithShortcutStableRef.current()) {
38
+ // Return early if the callback returns false.
39
+ // If the callback is not provided, we assume the shortcut is enabled.
40
+ return;
41
+ }
42
+ if (event.repeat) {
43
+ // Ignore repeated keydown events from holding down the keys
44
+ return;
45
+ }
46
+ if (openLayerObserver.getCount({
47
+ type: 'modal'
48
+ }) > 0 && (0, _platformFeatureFlags.fg)('platform-dst-open-layer-observer-layer-type')) {
49
+ // Return early if there are any open modals
50
+ // This check is behind the layer type FG, as `getCount` will return the count of all layers when
51
+ // the FG is disabled - meaning we would ignore the shortcut if there is any open layer (not just modals).
52
+ return;
53
+ }
54
+ toggleVisibilityByShortcut();
55
+ }
56
+ }
57
+ });
58
+ }, [canToggleWithShortcutStableRef, openLayerObserver, toggleVisibilityByShortcut, isSideNavShortcutEnabled]);
59
+ }
@@ -7,6 +7,7 @@ import { SkipLinksProvider } from '../../context/skip-links/skip-links-provider'
7
7
  import { TopNavStartProvider } from '../../context/top-nav-start/top-nav-start-context-provider';
8
8
  import { DangerouslyHoistSlotSizes } from './hoist-slot-sizes-context';
9
9
  import { SideNavElementProvider } from './side-nav/element-context';
10
+ import { IsSideNavShortcutEnabledProvider } from './side-nav/is-side-nav-shortcut-enabled-context';
10
11
  import { SideNavToggleButtonProvider } from './side-nav/toggle-button-provider';
11
12
  import { SideNavVisibilityProvider } from './side-nav/visibility-provider';
12
13
 
@@ -26,7 +27,8 @@ export function Root({
26
27
  UNSAFE_dangerouslyHoistSlotSizes = false,
27
28
  skipLinksLabel = 'Skip to:',
28
29
  testId,
29
- defaultSideNavCollapsed
30
+ defaultSideNavCollapsed,
31
+ isSideNavShortcutEnabled = false
30
32
  }) {
31
33
  const ref = useRef(null);
32
34
  useEffect(() => {
@@ -53,7 +55,9 @@ This message will not be displayed in production.
53
55
  }, []);
54
56
  return /*#__PURE__*/React.createElement(SideNavVisibilityProvider, {
55
57
  defaultCollapsed: defaultSideNavCollapsed
56
- }, /*#__PURE__*/React.createElement(SideNavToggleButtonProvider, null, /*#__PURE__*/React.createElement(SideNavElementProvider, null, /*#__PURE__*/React.createElement(TopNavStartProvider, null, /*#__PURE__*/React.createElement(OpenLayerObserver, null, /*#__PURE__*/React.createElement(DangerouslyHoistSlotSizes.Provider, {
58
+ }, /*#__PURE__*/React.createElement(SideNavToggleButtonProvider, null, /*#__PURE__*/React.createElement(SideNavElementProvider, null, /*#__PURE__*/React.createElement(IsSideNavShortcutEnabledProvider, {
59
+ isSideNavShortcutEnabled: isSideNavShortcutEnabled
60
+ }, /*#__PURE__*/React.createElement(TopNavStartProvider, null, /*#__PURE__*/React.createElement(OpenLayerObserver, null, /*#__PURE__*/React.createElement(DangerouslyHoistSlotSizes.Provider, {
57
61
  value: UNSAFE_dangerouslyHoistSlotSizes
58
62
  }, /*#__PURE__*/React.createElement(SkipLinksProvider, {
59
63
  label: skipLinksLabel,
@@ -63,5 +67,5 @@ This message will not be displayed in production.
63
67
  className: ax([styles.root, xcss]),
64
68
  id: gridRootId,
65
69
  "data-testid": testId
66
- }, children))))))));
70
+ }, children)))))))));
67
71
  }
@@ -0,0 +1,29 @@
1
+ import React, { createContext, useContext } from 'react';
2
+
3
+ /**
4
+ * Context for whether the side nav toggle shortcut is enabled.
5
+ *
6
+ * Used to share the `isSideNavShortcutEnabled` prop value from `Root` with other components,
7
+ * so the visual keyboard shortcut in tooltips can be conditionally displayed.
8
+ */
9
+ const IsSideNavShortcutEnabledContext = /*#__PURE__*/createContext(false);
10
+
11
+ /**
12
+ * Provider for the `IsSideNavShortcutEnabledContext`.
13
+ */
14
+ export function IsSideNavShortcutEnabledProvider({
15
+ children,
16
+ isSideNavShortcutEnabled
17
+ }) {
18
+ return /*#__PURE__*/React.createElement(IsSideNavShortcutEnabledContext.Provider, {
19
+ value: isSideNavShortcutEnabled
20
+ }, children);
21
+ }
22
+
23
+ /**
24
+ * Returns the value of the `isSideNavShortcutEnabled` prop from the `Root` component, which
25
+ * is shared through context.
26
+ */
27
+ export function useIsSideNavShortcutEnabled() {
28
+ return useContext(IsSideNavShortcutEnabledContext);
29
+ }
@@ -25,6 +25,7 @@ import { useSideNavRef } from './element-context';
25
25
  import { sideNavFlyoutCloseDelayMs } from './flyout-close-delay-ms';
26
26
  import { SideNavToggleButtonElement } from './toggle-button-context';
27
27
  import { useExpandSideNav } from './use-expand-side-nav';
28
+ import { useSideNavToggleKeyboardShortcut } from './use-side-nav-toggle-keyboard-shortcut';
28
29
  import { useSideNavVisibility } from './use-side-nav-visibility';
29
30
  import { useSideNavVisibilityCallbacks } from './use-side-nav-visibility-callbacks';
30
31
  import { useToggleSideNav } from './use-toggle-side-nav';
@@ -82,7 +83,8 @@ function SideNavInternal({
82
83
  onCollapse,
83
84
  onPeekStart,
84
85
  onPeekEnd,
85
- id: providedId
86
+ id: providedId,
87
+ canToggleWithShortcut
86
88
  }) {
87
89
  var _sideNavState$lastTri;
88
90
  const id = useLayoutId({
@@ -633,6 +635,9 @@ function SideNavInternal({
633
635
  }
634
636
  devTimeOnlyAttributes['data-visible'] = visible.length ? visible.join(',') : 'false';
635
637
  }
638
+ useSideNavToggleKeyboardShortcut({
639
+ canToggleWithShortcut
640
+ });
636
641
  useResizingWidthCssVarOnRootElement({
637
642
  isEnabled: true,
638
643
  cssVar: panelSplitterResizingVar,
@@ -737,6 +742,7 @@ export function SideNav({
737
742
  onCollapse,
738
743
  onPeekStart,
739
744
  onPeekEnd,
745
+ canToggleWithShortcut,
740
746
  id
741
747
  }) {
742
748
  return /*#__PURE__*/React.createElement(OpenLayerObserverNamespaceProvider, {
@@ -751,6 +757,7 @@ export function SideNav({
751
757
  onCollapse: onCollapse,
752
758
  onPeekStart: onPeekStart,
753
759
  onPeekEnd: onPeekEnd,
754
- id: id
760
+ id: id,
761
+ canToggleWithShortcut: canToggleWithShortcut
755
762
  }, children));
756
763
  }
@@ -0,0 +1,54 @@
1
+ import { useEffect } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import useStableRef from '@atlaskit/ds-lib/use-stable-ref';
4
+ import { useOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
5
+ import { fg } from '@atlaskit/platform-feature-flags';
6
+ import { useIsSideNavShortcutEnabled } from './is-side-nav-shortcut-enabled-context';
7
+ import { useToggleSideNav } from './use-toggle-side-nav';
8
+
9
+ /**
10
+ * Binds the keyboard shortcut to toggle the side nav.
11
+ */
12
+ export function useSideNavToggleKeyboardShortcut({
13
+ canToggleWithShortcut
14
+ }) {
15
+ const openLayerObserver = useOpenLayerObserver();
16
+ const toggleVisibilityByShortcut = useToggleSideNav({
17
+ trigger: 'keyboard'
18
+ });
19
+ const canToggleWithShortcutStableRef = useStableRef(canToggleWithShortcut);
20
+ const isSideNavShortcutEnabled = useIsSideNavShortcutEnabled();
21
+ useEffect(() => {
22
+ if (!fg('navx-full-height-sidebar')) {
23
+ return;
24
+ }
25
+ if (!isSideNavShortcutEnabled) {
26
+ return;
27
+ }
28
+ return bind(window, {
29
+ type: 'keydown',
30
+ listener(event) {
31
+ if (event.ctrlKey && event.key === '[') {
32
+ if (canToggleWithShortcutStableRef.current && !canToggleWithShortcutStableRef.current()) {
33
+ // Return early if the callback returns false.
34
+ // If the callback is not provided, we assume the shortcut is enabled.
35
+ return;
36
+ }
37
+ if (event.repeat) {
38
+ // Ignore repeated keydown events from holding down the keys
39
+ return;
40
+ }
41
+ if (openLayerObserver.getCount({
42
+ type: 'modal'
43
+ }) > 0 && fg('platform-dst-open-layer-observer-layer-type')) {
44
+ // Return early if there are any open modals
45
+ // This check is behind the layer type FG, as `getCount` will return the count of all layers when
46
+ // the FG is disabled - meaning we would ignore the shortcut if there is any open layer (not just modals).
47
+ return;
48
+ }
49
+ toggleVisibilityByShortcut();
50
+ }
51
+ }
52
+ });
53
+ }, [canToggleWithShortcutStableRef, openLayerObserver, toggleVisibilityByShortcut, isSideNavShortcutEnabled]);
54
+ }
@@ -7,6 +7,7 @@ import { SkipLinksProvider } from '../../context/skip-links/skip-links-provider'
7
7
  import { TopNavStartProvider } from '../../context/top-nav-start/top-nav-start-context-provider';
8
8
  import { DangerouslyHoistSlotSizes } from './hoist-slot-sizes-context';
9
9
  import { SideNavElementProvider } from './side-nav/element-context';
10
+ import { IsSideNavShortcutEnabledProvider } from './side-nav/is-side-nav-shortcut-enabled-context';
10
11
  import { SideNavToggleButtonProvider } from './side-nav/toggle-button-provider';
11
12
  import { SideNavVisibilityProvider } from './side-nav/visibility-provider';
12
13
 
@@ -28,7 +29,9 @@ export function Root(_ref) {
28
29
  _ref$skipLinksLabel = _ref.skipLinksLabel,
29
30
  skipLinksLabel = _ref$skipLinksLabel === void 0 ? 'Skip to:' : _ref$skipLinksLabel,
30
31
  testId = _ref.testId,
31
- defaultSideNavCollapsed = _ref.defaultSideNavCollapsed;
32
+ defaultSideNavCollapsed = _ref.defaultSideNavCollapsed,
33
+ _ref$isSideNavShortcu = _ref.isSideNavShortcutEnabled,
34
+ isSideNavShortcutEnabled = _ref$isSideNavShortcu === void 0 ? false : _ref$isSideNavShortcu;
32
35
  var ref = useRef(null);
33
36
  useEffect(function () {
34
37
  if (process.env.NODE_ENV !== 'production') {
@@ -45,7 +48,9 @@ export function Root(_ref) {
45
48
  }, []);
46
49
  return /*#__PURE__*/React.createElement(SideNavVisibilityProvider, {
47
50
  defaultCollapsed: defaultSideNavCollapsed
48
- }, /*#__PURE__*/React.createElement(SideNavToggleButtonProvider, null, /*#__PURE__*/React.createElement(SideNavElementProvider, null, /*#__PURE__*/React.createElement(TopNavStartProvider, null, /*#__PURE__*/React.createElement(OpenLayerObserver, null, /*#__PURE__*/React.createElement(DangerouslyHoistSlotSizes.Provider, {
51
+ }, /*#__PURE__*/React.createElement(SideNavToggleButtonProvider, null, /*#__PURE__*/React.createElement(SideNavElementProvider, null, /*#__PURE__*/React.createElement(IsSideNavShortcutEnabledProvider, {
52
+ isSideNavShortcutEnabled: isSideNavShortcutEnabled
53
+ }, /*#__PURE__*/React.createElement(TopNavStartProvider, null, /*#__PURE__*/React.createElement(OpenLayerObserver, null, /*#__PURE__*/React.createElement(DangerouslyHoistSlotSizes.Provider, {
49
54
  value: UNSAFE_dangerouslyHoistSlotSizes
50
55
  }, /*#__PURE__*/React.createElement(SkipLinksProvider, {
51
56
  label: skipLinksLabel,
@@ -55,5 +60,5 @@ export function Root(_ref) {
55
60
  className: ax([styles.root, xcss]),
56
61
  id: gridRootId,
57
62
  "data-testid": testId
58
- }, children))))))));
63
+ }, children)))))))));
59
64
  }
@@ -0,0 +1,28 @@
1
+ import React, { createContext, useContext } from 'react';
2
+
3
+ /**
4
+ * Context for whether the side nav toggle shortcut is enabled.
5
+ *
6
+ * Used to share the `isSideNavShortcutEnabled` prop value from `Root` with other components,
7
+ * so the visual keyboard shortcut in tooltips can be conditionally displayed.
8
+ */
9
+ var IsSideNavShortcutEnabledContext = /*#__PURE__*/createContext(false);
10
+
11
+ /**
12
+ * Provider for the `IsSideNavShortcutEnabledContext`.
13
+ */
14
+ export function IsSideNavShortcutEnabledProvider(_ref) {
15
+ var children = _ref.children,
16
+ isSideNavShortcutEnabled = _ref.isSideNavShortcutEnabled;
17
+ return /*#__PURE__*/React.createElement(IsSideNavShortcutEnabledContext.Provider, {
18
+ value: isSideNavShortcutEnabled
19
+ }, children);
20
+ }
21
+
22
+ /**
23
+ * Returns the value of the `isSideNavShortcutEnabled` prop from the `Root` component, which
24
+ * is shared through context.
25
+ */
26
+ export function useIsSideNavShortcutEnabled() {
27
+ return useContext(IsSideNavShortcutEnabledContext);
28
+ }
@@ -29,6 +29,7 @@ import { useSideNavRef } from './element-context';
29
29
  import { sideNavFlyoutCloseDelayMs } from './flyout-close-delay-ms';
30
30
  import { SideNavToggleButtonElement } from './toggle-button-context';
31
31
  import { useExpandSideNav } from './use-expand-side-nav';
32
+ import { useSideNavToggleKeyboardShortcut } from './use-side-nav-toggle-keyboard-shortcut';
32
33
  import { useSideNavVisibility } from './use-side-nav-visibility';
33
34
  import { useSideNavVisibilityCallbacks } from './use-side-nav-visibility-callbacks';
34
35
  import { useToggleSideNav } from './use-toggle-side-nav';
@@ -90,7 +91,8 @@ function SideNavInternal(_ref) {
90
91
  onCollapse = _ref.onCollapse,
91
92
  onPeekStart = _ref.onPeekStart,
92
93
  onPeekEnd = _ref.onPeekEnd,
93
- providedId = _ref.id;
94
+ providedId = _ref.id,
95
+ canToggleWithShortcut = _ref.canToggleWithShortcut;
94
96
  var id = useLayoutId({
95
97
  providedId: providedId
96
98
  });
@@ -635,6 +637,9 @@ function SideNavInternal(_ref) {
635
637
  }
636
638
  devTimeOnlyAttributes['data-visible'] = visible.length ? visible.join(',') : 'false';
637
639
  }
640
+ useSideNavToggleKeyboardShortcut({
641
+ canToggleWithShortcut: canToggleWithShortcut
642
+ });
638
643
  useResizingWidthCssVarOnRootElement({
639
644
  isEnabled: true,
640
645
  cssVar: panelSplitterResizingVar,
@@ -736,6 +741,7 @@ export function SideNav(_ref8) {
736
741
  onCollapse = _ref8.onCollapse,
737
742
  onPeekStart = _ref8.onPeekStart,
738
743
  onPeekEnd = _ref8.onPeekEnd,
744
+ canToggleWithShortcut = _ref8.canToggleWithShortcut,
739
745
  id = _ref8.id;
740
746
  return /*#__PURE__*/React.createElement(OpenLayerObserverNamespaceProvider, {
741
747
  namespace: openLayerObserverSideNavNamespace
@@ -749,6 +755,7 @@ export function SideNav(_ref8) {
749
755
  onCollapse: onCollapse,
750
756
  onPeekStart: onPeekStart,
751
757
  onPeekEnd: onPeekEnd,
752
- id: id
758
+ id: id,
759
+ canToggleWithShortcut: canToggleWithShortcut
753
760
  }, children));
754
761
  }
@@ -0,0 +1,53 @@
1
+ import { useEffect } from 'react';
2
+ import { bind } from 'bind-event-listener';
3
+ import useStableRef from '@atlaskit/ds-lib/use-stable-ref';
4
+ import { useOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
5
+ import { fg } from '@atlaskit/platform-feature-flags';
6
+ import { useIsSideNavShortcutEnabled } from './is-side-nav-shortcut-enabled-context';
7
+ import { useToggleSideNav } from './use-toggle-side-nav';
8
+
9
+ /**
10
+ * Binds the keyboard shortcut to toggle the side nav.
11
+ */
12
+ export function useSideNavToggleKeyboardShortcut(_ref) {
13
+ var canToggleWithShortcut = _ref.canToggleWithShortcut;
14
+ var openLayerObserver = useOpenLayerObserver();
15
+ var toggleVisibilityByShortcut = useToggleSideNav({
16
+ trigger: 'keyboard'
17
+ });
18
+ var canToggleWithShortcutStableRef = useStableRef(canToggleWithShortcut);
19
+ var isSideNavShortcutEnabled = useIsSideNavShortcutEnabled();
20
+ useEffect(function () {
21
+ if (!fg('navx-full-height-sidebar')) {
22
+ return;
23
+ }
24
+ if (!isSideNavShortcutEnabled) {
25
+ return;
26
+ }
27
+ return bind(window, {
28
+ type: 'keydown',
29
+ listener: function listener(event) {
30
+ if (event.ctrlKey && event.key === '[') {
31
+ if (canToggleWithShortcutStableRef.current && !canToggleWithShortcutStableRef.current()) {
32
+ // Return early if the callback returns false.
33
+ // If the callback is not provided, we assume the shortcut is enabled.
34
+ return;
35
+ }
36
+ if (event.repeat) {
37
+ // Ignore repeated keydown events from holding down the keys
38
+ return;
39
+ }
40
+ if (openLayerObserver.getCount({
41
+ type: 'modal'
42
+ }) > 0 && fg('platform-dst-open-layer-observer-layer-type')) {
43
+ // Return early if there are any open modals
44
+ // This check is behind the layer type FG, as `getCount` will return the count of all layers when
45
+ // the FG is disabled - meaning we would ignore the shortcut if there is any open layer (not just modals).
46
+ return;
47
+ }
48
+ toggleVisibilityByShortcut();
49
+ }
50
+ }
51
+ });
52
+ }, [canToggleWithShortcutStableRef, openLayerObserver, toggleVisibilityByShortcut, isSideNavShortcutEnabled]);
53
+ }
@@ -9,7 +9,7 @@ export declare const gridRootId = "unsafe-design-system-page-layout-root";
9
9
  * The root component of the navigation system. It wraps the underlying components with the necessary contexts allowing to use certain data and hooks
10
10
  * @param skipLinksLabel - The very first element of the layout is a skip links container that can be accessed by pressing Tab button and holds the links to the other sections of the layout thus improving accessibility. This parameter defines the header text for this container
11
11
  */
12
- export declare function Root({ children, xcss, UNSAFE_dangerouslyHoistSlotSizes, skipLinksLabel, testId, defaultSideNavCollapsed, }: {
12
+ export declare function Root({ children, xcss, UNSAFE_dangerouslyHoistSlotSizes, skipLinksLabel, testId, defaultSideNavCollapsed, isSideNavShortcutEnabled, }: {
13
13
  /**
14
14
  * For rendering the layout areas, e.g. TopNav, SideNav, Main.
15
15
  * They should be rendered as immediate children.
@@ -47,4 +47,22 @@ export declare function Root({ children, xcss, UNSAFE_dangerouslyHoistSlotSizes,
47
47
  * __Note:__ When provided, the `defaultCollapsed` props on `SideNav` and `SideNavToggleButton` will be ignored.
48
48
  */
49
49
  defaultSideNavCollapsed?: boolean;
50
+ /**
51
+ * Controls whether the side nav toggle shortcut is enabled. This will be used to bind the keyboard event listener,
52
+ * and to display the keyboard shortcuts in the appropriate tooltips (`SideNavToggleButton`, `SideNavPanelSplitter`).
53
+ *
54
+ * The shortcut key is `Ctrl` + `[`.
55
+ *
56
+ * The shortcut is not enabled by default.
57
+ *
58
+ * The shortcut will also be ignored if there are any open ADS modal dialogs (`@atlaskit/modal-dialog`). This is behind
59
+ * the `platform-dst-open-layer-observer-layer-type` feature flag.
60
+ *
61
+ * `SideNav` has another prop `canToggleWithShortcut()` that can be used to run additional checks after the shortcut
62
+ * is pressed, before the SideNav is toggled. You can use this to conditionally disable the shortcut based on your
63
+ * your own custom checks, e.g. if there is a legacy dialog open.
64
+ *
65
+ * Note: The built-in keyboard shortcut is behind the `navx-full-height-sidebar` feature flag.
66
+ */
67
+ isSideNavShortcutEnabled?: boolean;
50
68
  }): JSX.Element;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ /**
3
+ * Provider for the `IsSideNavShortcutEnabledContext`.
4
+ */
5
+ export declare function IsSideNavShortcutEnabledProvider({ children, isSideNavShortcutEnabled, }: {
6
+ children: React.ReactNode;
7
+ isSideNavShortcutEnabled: boolean;
8
+ }): React.JSX.Element;
9
+ /**
10
+ * Returns the value of the `isSideNavShortcutEnabled` prop from the `Root` component, which
11
+ * is shared through context.
12
+ */
13
+ export declare function useIsSideNavShortcutEnabled(): boolean;
@@ -63,6 +63,18 @@ type SideNavProps = CommonSlotProps & {
63
63
  onPeekEnd?: (args: {
64
64
  trigger: 'mouse-leave' | 'side-nav-expand';
65
65
  }) => void;
66
+ /**
67
+ * Whether the side nav should be toggled in response to the built-in keyboard shortcut. Use this callback to
68
+ * conditionally disable the shortcut based on your own custom checks, e.g. if there is a legacy dialog open.
69
+ *
70
+ * This prop will do nothing if `isSideNavShortcutEnabled` on Root is not set to `true`, as the keyboard event
71
+ * listener is only binded if `isSideNavShortcutEnabled` is `true`.
72
+ *
73
+ * The shortcut key is `Ctrl` + `[`.
74
+ *
75
+ * Note: The built-in keyboard shortcut is behind the `navx-full-height-sidebar` feature flag.
76
+ */
77
+ canToggleWithShortcut?: () => boolean;
66
78
  };
67
79
  export declare const onPeekStartDelayMs = 500;
68
80
  /**
@@ -75,5 +87,5 @@ export declare const onPeekStartDelayMs = 500;
75
87
  */
76
88
  export declare function SideNav({ children, defaultCollapsed, defaultWidth, testId, label, // Default value is defined in `SideNavInternal`
77
89
  skipLinkLabel, // Default value is defined in `SideNavInternal`
78
- onExpand, onCollapse, onPeekStart, onPeekEnd, id, }: SideNavProps): JSX.Element;
90
+ onExpand, onCollapse, onPeekStart, onPeekEnd, canToggleWithShortcut, id, }: SideNavProps): JSX.Element;
79
91
  export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Binds the keyboard shortcut to toggle the side nav.
3
+ */
4
+ export declare function useSideNavToggleKeyboardShortcut({ canToggleWithShortcut, }: {
5
+ canToggleWithShortcut?: () => boolean;
6
+ }): void;
@@ -9,7 +9,7 @@ export declare const gridRootId = "unsafe-design-system-page-layout-root";
9
9
  * The root component of the navigation system. It wraps the underlying components with the necessary contexts allowing to use certain data and hooks
10
10
  * @param skipLinksLabel - The very first element of the layout is a skip links container that can be accessed by pressing Tab button and holds the links to the other sections of the layout thus improving accessibility. This parameter defines the header text for this container
11
11
  */
12
- export declare function Root({ children, xcss, UNSAFE_dangerouslyHoistSlotSizes, skipLinksLabel, testId, defaultSideNavCollapsed, }: {
12
+ export declare function Root({ children, xcss, UNSAFE_dangerouslyHoistSlotSizes, skipLinksLabel, testId, defaultSideNavCollapsed, isSideNavShortcutEnabled, }: {
13
13
  /**
14
14
  * For rendering the layout areas, e.g. TopNav, SideNav, Main.
15
15
  * They should be rendered as immediate children.
@@ -47,4 +47,22 @@ export declare function Root({ children, xcss, UNSAFE_dangerouslyHoistSlotSizes,
47
47
  * __Note:__ When provided, the `defaultCollapsed` props on `SideNav` and `SideNavToggleButton` will be ignored.
48
48
  */
49
49
  defaultSideNavCollapsed?: boolean;
50
+ /**
51
+ * Controls whether the side nav toggle shortcut is enabled. This will be used to bind the keyboard event listener,
52
+ * and to display the keyboard shortcuts in the appropriate tooltips (`SideNavToggleButton`, `SideNavPanelSplitter`).
53
+ *
54
+ * The shortcut key is `Ctrl` + `[`.
55
+ *
56
+ * The shortcut is not enabled by default.
57
+ *
58
+ * The shortcut will also be ignored if there are any open ADS modal dialogs (`@atlaskit/modal-dialog`). This is behind
59
+ * the `platform-dst-open-layer-observer-layer-type` feature flag.
60
+ *
61
+ * `SideNav` has another prop `canToggleWithShortcut()` that can be used to run additional checks after the shortcut
62
+ * is pressed, before the SideNav is toggled. You can use this to conditionally disable the shortcut based on your
63
+ * your own custom checks, e.g. if there is a legacy dialog open.
64
+ *
65
+ * Note: The built-in keyboard shortcut is behind the `navx-full-height-sidebar` feature flag.
66
+ */
67
+ isSideNavShortcutEnabled?: boolean;
50
68
  }): JSX.Element;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ /**
3
+ * Provider for the `IsSideNavShortcutEnabledContext`.
4
+ */
5
+ export declare function IsSideNavShortcutEnabledProvider({ children, isSideNavShortcutEnabled, }: {
6
+ children: React.ReactNode;
7
+ isSideNavShortcutEnabled: boolean;
8
+ }): React.JSX.Element;
9
+ /**
10
+ * Returns the value of the `isSideNavShortcutEnabled` prop from the `Root` component, which
11
+ * is shared through context.
12
+ */
13
+ export declare function useIsSideNavShortcutEnabled(): boolean;
@@ -63,6 +63,18 @@ type SideNavProps = CommonSlotProps & {
63
63
  onPeekEnd?: (args: {
64
64
  trigger: 'mouse-leave' | 'side-nav-expand';
65
65
  }) => void;
66
+ /**
67
+ * Whether the side nav should be toggled in response to the built-in keyboard shortcut. Use this callback to
68
+ * conditionally disable the shortcut based on your own custom checks, e.g. if there is a legacy dialog open.
69
+ *
70
+ * This prop will do nothing if `isSideNavShortcutEnabled` on Root is not set to `true`, as the keyboard event
71
+ * listener is only binded if `isSideNavShortcutEnabled` is `true`.
72
+ *
73
+ * The shortcut key is `Ctrl` + `[`.
74
+ *
75
+ * Note: The built-in keyboard shortcut is behind the `navx-full-height-sidebar` feature flag.
76
+ */
77
+ canToggleWithShortcut?: () => boolean;
66
78
  };
67
79
  export declare const onPeekStartDelayMs = 500;
68
80
  /**
@@ -75,5 +87,5 @@ export declare const onPeekStartDelayMs = 500;
75
87
  */
76
88
  export declare function SideNav({ children, defaultCollapsed, defaultWidth, testId, label, // Default value is defined in `SideNavInternal`
77
89
  skipLinkLabel, // Default value is defined in `SideNavInternal`
78
- onExpand, onCollapse, onPeekStart, onPeekEnd, id, }: SideNavProps): JSX.Element;
90
+ onExpand, onCollapse, onPeekStart, onPeekEnd, canToggleWithShortcut, id, }: SideNavProps): JSX.Element;
79
91
  export {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Binds the keyboard shortcut to toggle the side nav.
3
+ */
4
+ export declare function useSideNavToggleKeyboardShortcut({ canToggleWithShortcut, }: {
5
+ canToggleWithShortcut?: () => boolean;
6
+ }): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/navigation-system",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "The latest navigation system for Atlassian apps.",
5
5
  "repository": "https://bitbucket.org/atlassian/atlassian-frontend-mirror",
6
6
  "author": "Atlassian Pty Ltd",
@@ -72,7 +72,7 @@
72
72
  "@atlaskit/css": "^0.15.0",
73
73
  "@atlaskit/ds-lib": "^5.1.0",
74
74
  "@atlaskit/icon": "^28.5.0",
75
- "@atlaskit/layering": "^3.1.0",
75
+ "@atlaskit/layering": "^3.2.0",
76
76
  "@atlaskit/logo": "^19.9.0",
77
77
  "@atlaskit/platform-feature-flags": "^1.1.0",
78
78
  "@atlaskit/popup": "^4.4.0",
@@ -107,6 +107,7 @@
107
107
  "@atlaskit/link": "^3.2.0",
108
108
  "@atlaskit/lozenge": "^13.0.0",
109
109
  "@atlaskit/menu": "^8.4.0",
110
+ "@atlaskit/modal-dialog": "^14.6.0",
110
111
  "@atlaskit/onboarding": "^14.4.0",
111
112
  "@atlaskit/page-header": "^12.1.0",
112
113
  "@atlaskit/page-layout": "^4.2.0",
@@ -118,6 +119,7 @@
118
119
  "@atlassian/gemini": "^1.20.0",
119
120
  "@atlassian/search-dialog": "^9.7.0",
120
121
  "@atlassian/ssr-tests": "^0.3.0",
122
+ "@atlassian/testing-library": "^0.4.0",
121
123
  "@axe-core/playwright": "^4.8.0",
122
124
  "@testing-library/react": "^13.4.0",
123
125
  "@testing-library/react-hooks": "^8.0.1",
@@ -184,6 +186,9 @@
184
186
  },
185
187
  "platform_dst_side_nav_remove_custom_tooltip": {
186
188
  "type": "boolean"
189
+ },
190
+ "platform-dst-open-layer-observer-layer-type": {
191
+ "type": "boolean"
187
192
  }
188
193
  },
189
194
  "homepage": "https://atlassian.design/components/navigation-system"