@atlaskit/navigation-system 4.3.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 +28 -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 +55 -3
  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 +54 -3
  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 +54 -3
  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 +24 -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 +24 -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,33 @@
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
+
23
+ ## 4.4.0
24
+
25
+ ### Minor Changes
26
+
27
+ - [`327d6a06eebb2`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/327d6a06eebb2) -
28
+ Adds `onPeekStart` and `onPeekEnd` callbacks to the `SideNav` to use for monitoring when users
29
+ peek at the side navigation when it is collapsed.
30
+
3
31
  ## 4.3.0
4
32
 
5
33
  ### 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
+ }
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", {
7
7
  value: true
8
8
  });
9
9
  exports.SideNav = SideNav;
10
+ exports.onPeekStartDelayMs = void 0;
10
11
  require("./side-nav.compiled.css");
11
12
  var _react = _interopRequireWildcard(require("react"));
12
13
  var React = _react;
@@ -17,6 +18,7 @@ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/sli
17
18
  var _bindEventListener = require("bind-event-listener");
18
19
  var _reactDom = require("react-dom");
19
20
  var _mergeRefs = _interopRequireDefault(require("@atlaskit/ds-lib/merge-refs"));
21
+ var _useStableRef = _interopRequireDefault(require("@atlaskit/ds-lib/use-stable-ref"));
20
22
  var _openLayerObserver = require("@atlaskit/layering/experimental/open-layer-observer");
21
23
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
22
24
  var _adapter = require("@atlaskit/pragmatic-drag-and-drop/element/adapter");
@@ -34,6 +36,7 @@ var _elementContext = require("./element-context");
34
36
  var _flyoutCloseDelayMs = require("./flyout-close-delay-ms");
35
37
  var _toggleButtonContext = require("./toggle-button-context");
36
38
  var _useExpandSideNav = require("./use-expand-side-nav");
39
+ var _useSideNavToggleKeyboardShortcut = require("./use-side-nav-toggle-keyboard-shortcut");
37
40
  var _useSideNavVisibility2 = require("./use-side-nav-visibility");
38
41
  var _useSideNavVisibilityCallbacks = require("./use-side-nav-visibility-callbacks");
39
42
  var _useToggleSideNav = require("./use-toggle-side-nav");
@@ -77,6 +80,7 @@ var styles = {
77
80
  fullHeightSidebar: "_165t56xv _180k1wjm _26vxoned _1mt19dtb"
78
81
  };
79
82
  var fallbackDefaultWidth = 320;
83
+ var onPeekStartDelayMs = exports.onPeekStartDelayMs = 500;
80
84
 
81
85
  /**
82
86
  * We need an additional component layer so we can wrap the side nav in a `OpenLayerObserver` and have access to the
@@ -95,7 +99,10 @@ function SideNavInternal(_ref) {
95
99
  skipLinkLabel = _ref$skipLinkLabel === void 0 ? label : _ref$skipLinkLabel,
96
100
  onExpand = _ref.onExpand,
97
101
  onCollapse = _ref.onCollapse,
98
- providedId = _ref.id;
102
+ onPeekStart = _ref.onPeekStart,
103
+ onPeekEnd = _ref.onPeekEnd,
104
+ providedId = _ref.id,
105
+ canToggleWithShortcut = _ref.canToggleWithShortcut;
99
106
  var id = (0, _idUtils.useLayoutId)({
100
107
  providedId: providedId
101
108
  });
@@ -161,6 +168,16 @@ function SideNavInternal(_ref) {
161
168
  type: 'not-active'
162
169
  });
163
170
  var isFlyoutVisible = (sideNavState === null || sideNavState === void 0 ? void 0 : sideNavState.flyout) === 'open';
171
+ var isExpandedOnDesktopRef = (0, _useStableRef.default)(isExpandedOnDesktop);
172
+ var hasPeekStartedRef = (0, _react.useRef)(false);
173
+ var onPeekStartRef = (0, _useStableRef.default)(onPeekStart);
174
+ var onPeekEndRef = (0, _useStableRef.default)(onPeekEnd);
175
+ var onPeekStartTimeoutIdRef = (0, _react.useRef)(undefined);
176
+ (0, _react.useEffect)(function () {
177
+ return function () {
178
+ clearTimeout(onPeekStartTimeoutIdRef.current);
179
+ };
180
+ }, []);
164
181
  var updateFlyoutState = (0, _react.useMemo)(function () {
165
182
  function tryAbortPendingClose() {
166
183
  if (flyoutStateRef.current.type === 'waiting-for-close') {
@@ -168,6 +185,7 @@ function SideNavInternal(_ref) {
168
185
  }
169
186
  }
170
187
  function open() {
188
+ var prevFlyoutState = flyoutStateRef.current;
171
189
  tryAbortPendingClose();
172
190
  flyoutStateRef.current = {
173
191
  type: 'open'
@@ -180,8 +198,24 @@ function SideNavInternal(_ref) {
180
198
  }
181
199
  return currentState;
182
200
  });
201
+
202
+ // Avoid redundant calls to `onPeekStart()`
203
+ if (prevFlyoutState.type === 'not-active') {
204
+ clearTimeout(onPeekStartTimeoutIdRef.current);
205
+ onPeekStartTimeoutIdRef.current = setTimeout(function () {
206
+ var _onPeekStartRef$curre;
207
+ // If the flyout isn't still open after ~500ms then we won't count the peek
208
+ // As we want to track user intention rather than all hovers
209
+ if (isExpandedOnDesktopRef.current || flyoutStateRef.current.type !== 'open') {
210
+ return;
211
+ }
212
+ hasPeekStartedRef.current = true;
213
+ (_onPeekStartRef$curre = onPeekStartRef.current) === null || _onPeekStartRef$curre === void 0 || _onPeekStartRef$curre.call(onPeekStartRef);
214
+ }, onPeekStartDelayMs);
215
+ }
183
216
  }
184
217
  function close() {
218
+ var prevFlyoutState = flyoutStateRef.current;
185
219
  tryAbortPendingClose();
186
220
  flyoutStateRef.current = {
187
221
  type: 'not-active'
@@ -194,6 +228,15 @@ function SideNavInternal(_ref) {
194
228
  }
195
229
  return currentState;
196
230
  });
231
+
232
+ // Avoid redundant calls to `onPeekEnd()`
233
+ if (prevFlyoutState.type !== 'not-active' && hasPeekStartedRef.current) {
234
+ var _onPeekEndRef$current;
235
+ hasPeekStartedRef.current = false;
236
+ (_onPeekEndRef$current = onPeekEndRef.current) === null || _onPeekEndRef$current === void 0 || _onPeekEndRef$current.call(onPeekEndRef, {
237
+ trigger: isExpandedOnDesktopRef.current ? 'side-nav-expand' : 'mouse-leave'
238
+ });
239
+ }
197
240
  }
198
241
  return function onAction(action) {
199
242
  if (action === 'drag-from-flyout-started') {
@@ -252,7 +295,7 @@ function SideNavInternal(_ref) {
252
295
  return;
253
296
  }
254
297
  };
255
- }, [openLayerObserver, setSideNavState]);
298
+ }, [isExpandedOnDesktopRef, onPeekEndRef, onPeekStartRef, openLayerObserver, setSideNavState]);
256
299
  var toggleVisibilityByScreenResize = (0, _useToggleSideNav.useToggleSideNav)({
257
300
  trigger: 'screen-resize'
258
301
  });
@@ -604,6 +647,9 @@ function SideNavInternal(_ref) {
604
647
  }
605
648
  devTimeOnlyAttributes['data-visible'] = visible.length ? visible.join(',') : 'false';
606
649
  }
650
+ (0, _useSideNavToggleKeyboardShortcut.useSideNavToggleKeyboardShortcut)({
651
+ canToggleWithShortcut: canToggleWithShortcut
652
+ });
607
653
  (0, _useResizingWidthCssVarOnRootElement.useResizingWidthCssVarOnRootElement)({
608
654
  isEnabled: true,
609
655
  cssVar: panelSplitterResizingVar,
@@ -703,6 +749,9 @@ function SideNav(_ref8) {
703
749
  skipLinkLabel = _ref8$skipLinkLabel === void 0 ? label : _ref8$skipLinkLabel,
704
750
  onExpand = _ref8.onExpand,
705
751
  onCollapse = _ref8.onCollapse,
752
+ onPeekStart = _ref8.onPeekStart,
753
+ onPeekEnd = _ref8.onPeekEnd,
754
+ canToggleWithShortcut = _ref8.canToggleWithShortcut,
706
755
  id = _ref8.id;
707
756
  return /*#__PURE__*/React.createElement(_openLayerObserver.OpenLayerObserverNamespaceProvider, {
708
757
  namespace: openLayerObserverSideNavNamespace
@@ -714,6 +763,9 @@ function SideNav(_ref8) {
714
763
  skipLinkLabel: skipLinkLabel,
715
764
  onExpand: onExpand,
716
765
  onCollapse: onCollapse,
717
- id: id
766
+ onPeekStart: onPeekStart,
767
+ onPeekEnd: onPeekEnd,
768
+ id: id,
769
+ canToggleWithShortcut: canToggleWithShortcut
718
770
  }, children));
719
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
+ }
@@ -7,6 +7,7 @@ import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, u
7
7
  import { bind } from 'bind-event-listener';
8
8
  import { flushSync } from 'react-dom';
9
9
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
10
+ import useStableRef from '@atlaskit/ds-lib/use-stable-ref';
10
11
  import { OpenLayerObserverNamespaceProvider, useOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
11
12
  import { fg } from '@atlaskit/platform-feature-flags';
12
13
  import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
@@ -24,6 +25,7 @@ import { useSideNavRef } from './element-context';
24
25
  import { sideNavFlyoutCloseDelayMs } from './flyout-close-delay-ms';
25
26
  import { SideNavToggleButtonElement } from './toggle-button-context';
26
27
  import { useExpandSideNav } from './use-expand-side-nav';
28
+ import { useSideNavToggleKeyboardShortcut } from './use-side-nav-toggle-keyboard-shortcut';
27
29
  import { useSideNavVisibility } from './use-side-nav-visibility';
28
30
  import { useSideNavVisibilityCallbacks } from './use-side-nav-visibility-callbacks';
29
31
  import { useToggleSideNav } from './use-toggle-side-nav';
@@ -64,6 +66,7 @@ const styles = {
64
66
  fullHeightSidebar: "_165t56xv _180k1wjm _26vxoned _1mt19dtb"
65
67
  };
66
68
  const fallbackDefaultWidth = 320;
69
+ export const onPeekStartDelayMs = 500;
67
70
 
68
71
  /**
69
72
  * We need an additional component layer so we can wrap the side nav in a `OpenLayerObserver` and have access to the
@@ -78,7 +81,10 @@ function SideNavInternal({
78
81
  skipLinkLabel = label,
79
82
  onExpand,
80
83
  onCollapse,
81
- id: providedId
84
+ onPeekStart,
85
+ onPeekEnd,
86
+ id: providedId,
87
+ canToggleWithShortcut
82
88
  }) {
83
89
  var _sideNavState$lastTri;
84
90
  const id = useLayoutId({
@@ -142,6 +148,16 @@ function SideNavInternal({
142
148
  type: 'not-active'
143
149
  });
144
150
  const isFlyoutVisible = (sideNavState === null || sideNavState === void 0 ? void 0 : sideNavState.flyout) === 'open';
151
+ const isExpandedOnDesktopRef = useStableRef(isExpandedOnDesktop);
152
+ const hasPeekStartedRef = useRef(false);
153
+ const onPeekStartRef = useStableRef(onPeekStart);
154
+ const onPeekEndRef = useStableRef(onPeekEnd);
155
+ const onPeekStartTimeoutIdRef = useRef(undefined);
156
+ useEffect(() => {
157
+ return () => {
158
+ clearTimeout(onPeekStartTimeoutIdRef.current);
159
+ };
160
+ }, []);
145
161
  const updateFlyoutState = useMemo(() => {
146
162
  function tryAbortPendingClose() {
147
163
  if (flyoutStateRef.current.type === 'waiting-for-close') {
@@ -149,6 +165,7 @@ function SideNavInternal({
149
165
  }
150
166
  }
151
167
  function open() {
168
+ const prevFlyoutState = flyoutStateRef.current;
152
169
  tryAbortPendingClose();
153
170
  flyoutStateRef.current = {
154
171
  type: 'open'
@@ -162,8 +179,24 @@ function SideNavInternal({
162
179
  }
163
180
  return currentState;
164
181
  });
182
+
183
+ // Avoid redundant calls to `onPeekStart()`
184
+ if (prevFlyoutState.type === 'not-active') {
185
+ clearTimeout(onPeekStartTimeoutIdRef.current);
186
+ onPeekStartTimeoutIdRef.current = setTimeout(() => {
187
+ var _onPeekStartRef$curre;
188
+ // If the flyout isn't still open after ~500ms then we won't count the peek
189
+ // As we want to track user intention rather than all hovers
190
+ if (isExpandedOnDesktopRef.current || flyoutStateRef.current.type !== 'open') {
191
+ return;
192
+ }
193
+ hasPeekStartedRef.current = true;
194
+ (_onPeekStartRef$curre = onPeekStartRef.current) === null || _onPeekStartRef$curre === void 0 ? void 0 : _onPeekStartRef$curre.call(onPeekStartRef);
195
+ }, onPeekStartDelayMs);
196
+ }
165
197
  }
166
198
  function close() {
199
+ const prevFlyoutState = flyoutStateRef.current;
167
200
  tryAbortPendingClose();
168
201
  flyoutStateRef.current = {
169
202
  type: 'not-active'
@@ -177,6 +210,15 @@ function SideNavInternal({
177
210
  }
178
211
  return currentState;
179
212
  });
213
+
214
+ // Avoid redundant calls to `onPeekEnd()`
215
+ if (prevFlyoutState.type !== 'not-active' && hasPeekStartedRef.current) {
216
+ var _onPeekEndRef$current;
217
+ hasPeekStartedRef.current = false;
218
+ (_onPeekEndRef$current = onPeekEndRef.current) === null || _onPeekEndRef$current === void 0 ? void 0 : _onPeekEndRef$current.call(onPeekEndRef, {
219
+ trigger: isExpandedOnDesktopRef.current ? 'side-nav-expand' : 'mouse-leave'
220
+ });
221
+ }
180
222
  }
181
223
  return function onAction(action) {
182
224
  if (action === 'drag-from-flyout-started') {
@@ -235,7 +277,7 @@ function SideNavInternal({
235
277
  return;
236
278
  }
237
279
  };
238
- }, [openLayerObserver, setSideNavState]);
280
+ }, [isExpandedOnDesktopRef, onPeekEndRef, onPeekStartRef, openLayerObserver, setSideNavState]);
239
281
  const toggleVisibilityByScreenResize = useToggleSideNav({
240
282
  trigger: 'screen-resize'
241
283
  });
@@ -593,6 +635,9 @@ function SideNavInternal({
593
635
  }
594
636
  devTimeOnlyAttributes['data-visible'] = visible.length ? visible.join(',') : 'false';
595
637
  }
638
+ useSideNavToggleKeyboardShortcut({
639
+ canToggleWithShortcut
640
+ });
596
641
  useResizingWidthCssVarOnRootElement({
597
642
  isEnabled: true,
598
643
  cssVar: panelSplitterResizingVar,
@@ -695,6 +740,9 @@ export function SideNav({
695
740
  // Default value is defined in `SideNavInternal`
696
741
  onExpand,
697
742
  onCollapse,
743
+ onPeekStart,
744
+ onPeekEnd,
745
+ canToggleWithShortcut,
698
746
  id
699
747
  }) {
700
748
  return /*#__PURE__*/React.createElement(OpenLayerObserverNamespaceProvider, {
@@ -707,6 +755,9 @@ export function SideNav({
707
755
  skipLinkLabel: skipLinkLabel,
708
756
  onExpand: onExpand,
709
757
  onCollapse: onCollapse,
710
- id: id
758
+ onPeekStart: onPeekStart,
759
+ onPeekEnd: onPeekEnd,
760
+ id: id,
761
+ canToggleWithShortcut: canToggleWithShortcut
711
762
  }, children));
712
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
+ }
@@ -11,6 +11,7 @@ import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, u
11
11
  import { bind } from 'bind-event-listener';
12
12
  import { flushSync } from 'react-dom';
13
13
  import mergeRefs from '@atlaskit/ds-lib/merge-refs';
14
+ import useStableRef from '@atlaskit/ds-lib/use-stable-ref';
14
15
  import { OpenLayerObserverNamespaceProvider, useOpenLayerObserver } from '@atlaskit/layering/experimental/open-layer-observer';
15
16
  import { fg } from '@atlaskit/platform-feature-flags';
16
17
  import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
@@ -28,6 +29,7 @@ import { useSideNavRef } from './element-context';
28
29
  import { sideNavFlyoutCloseDelayMs } from './flyout-close-delay-ms';
29
30
  import { SideNavToggleButtonElement } from './toggle-button-context';
30
31
  import { useExpandSideNav } from './use-expand-side-nav';
32
+ import { useSideNavToggleKeyboardShortcut } from './use-side-nav-toggle-keyboard-shortcut';
31
33
  import { useSideNavVisibility } from './use-side-nav-visibility';
32
34
  import { useSideNavVisibilityCallbacks } from './use-side-nav-visibility-callbacks';
33
35
  import { useToggleSideNav } from './use-toggle-side-nav';
@@ -68,6 +70,7 @@ var styles = {
68
70
  fullHeightSidebar: "_165t56xv _180k1wjm _26vxoned _1mt19dtb"
69
71
  };
70
72
  var fallbackDefaultWidth = 320;
73
+ export var onPeekStartDelayMs = 500;
71
74
 
72
75
  /**
73
76
  * We need an additional component layer so we can wrap the side nav in a `OpenLayerObserver` and have access to the
@@ -86,7 +89,10 @@ function SideNavInternal(_ref) {
86
89
  skipLinkLabel = _ref$skipLinkLabel === void 0 ? label : _ref$skipLinkLabel,
87
90
  onExpand = _ref.onExpand,
88
91
  onCollapse = _ref.onCollapse,
89
- providedId = _ref.id;
92
+ onPeekStart = _ref.onPeekStart,
93
+ onPeekEnd = _ref.onPeekEnd,
94
+ providedId = _ref.id,
95
+ canToggleWithShortcut = _ref.canToggleWithShortcut;
90
96
  var id = useLayoutId({
91
97
  providedId: providedId
92
98
  });
@@ -152,6 +158,16 @@ function SideNavInternal(_ref) {
152
158
  type: 'not-active'
153
159
  });
154
160
  var isFlyoutVisible = (sideNavState === null || sideNavState === void 0 ? void 0 : sideNavState.flyout) === 'open';
161
+ var isExpandedOnDesktopRef = useStableRef(isExpandedOnDesktop);
162
+ var hasPeekStartedRef = useRef(false);
163
+ var onPeekStartRef = useStableRef(onPeekStart);
164
+ var onPeekEndRef = useStableRef(onPeekEnd);
165
+ var onPeekStartTimeoutIdRef = useRef(undefined);
166
+ useEffect(function () {
167
+ return function () {
168
+ clearTimeout(onPeekStartTimeoutIdRef.current);
169
+ };
170
+ }, []);
155
171
  var updateFlyoutState = useMemo(function () {
156
172
  function tryAbortPendingClose() {
157
173
  if (flyoutStateRef.current.type === 'waiting-for-close') {
@@ -159,6 +175,7 @@ function SideNavInternal(_ref) {
159
175
  }
160
176
  }
161
177
  function open() {
178
+ var prevFlyoutState = flyoutStateRef.current;
162
179
  tryAbortPendingClose();
163
180
  flyoutStateRef.current = {
164
181
  type: 'open'
@@ -171,8 +188,24 @@ function SideNavInternal(_ref) {
171
188
  }
172
189
  return currentState;
173
190
  });
191
+
192
+ // Avoid redundant calls to `onPeekStart()`
193
+ if (prevFlyoutState.type === 'not-active') {
194
+ clearTimeout(onPeekStartTimeoutIdRef.current);
195
+ onPeekStartTimeoutIdRef.current = setTimeout(function () {
196
+ var _onPeekStartRef$curre;
197
+ // If the flyout isn't still open after ~500ms then we won't count the peek
198
+ // As we want to track user intention rather than all hovers
199
+ if (isExpandedOnDesktopRef.current || flyoutStateRef.current.type !== 'open') {
200
+ return;
201
+ }
202
+ hasPeekStartedRef.current = true;
203
+ (_onPeekStartRef$curre = onPeekStartRef.current) === null || _onPeekStartRef$curre === void 0 || _onPeekStartRef$curre.call(onPeekStartRef);
204
+ }, onPeekStartDelayMs);
205
+ }
174
206
  }
175
207
  function close() {
208
+ var prevFlyoutState = flyoutStateRef.current;
176
209
  tryAbortPendingClose();
177
210
  flyoutStateRef.current = {
178
211
  type: 'not-active'
@@ -185,6 +218,15 @@ function SideNavInternal(_ref) {
185
218
  }
186
219
  return currentState;
187
220
  });
221
+
222
+ // Avoid redundant calls to `onPeekEnd()`
223
+ if (prevFlyoutState.type !== 'not-active' && hasPeekStartedRef.current) {
224
+ var _onPeekEndRef$current;
225
+ hasPeekStartedRef.current = false;
226
+ (_onPeekEndRef$current = onPeekEndRef.current) === null || _onPeekEndRef$current === void 0 || _onPeekEndRef$current.call(onPeekEndRef, {
227
+ trigger: isExpandedOnDesktopRef.current ? 'side-nav-expand' : 'mouse-leave'
228
+ });
229
+ }
188
230
  }
189
231
  return function onAction(action) {
190
232
  if (action === 'drag-from-flyout-started') {
@@ -243,7 +285,7 @@ function SideNavInternal(_ref) {
243
285
  return;
244
286
  }
245
287
  };
246
- }, [openLayerObserver, setSideNavState]);
288
+ }, [isExpandedOnDesktopRef, onPeekEndRef, onPeekStartRef, openLayerObserver, setSideNavState]);
247
289
  var toggleVisibilityByScreenResize = useToggleSideNav({
248
290
  trigger: 'screen-resize'
249
291
  });
@@ -595,6 +637,9 @@ function SideNavInternal(_ref) {
595
637
  }
596
638
  devTimeOnlyAttributes['data-visible'] = visible.length ? visible.join(',') : 'false';
597
639
  }
640
+ useSideNavToggleKeyboardShortcut({
641
+ canToggleWithShortcut: canToggleWithShortcut
642
+ });
598
643
  useResizingWidthCssVarOnRootElement({
599
644
  isEnabled: true,
600
645
  cssVar: panelSplitterResizingVar,
@@ -694,6 +739,9 @@ export function SideNav(_ref8) {
694
739
  skipLinkLabel = _ref8$skipLinkLabel === void 0 ? label : _ref8$skipLinkLabel,
695
740
  onExpand = _ref8.onExpand,
696
741
  onCollapse = _ref8.onCollapse,
742
+ onPeekStart = _ref8.onPeekStart,
743
+ onPeekEnd = _ref8.onPeekEnd,
744
+ canToggleWithShortcut = _ref8.canToggleWithShortcut,
697
745
  id = _ref8.id;
698
746
  return /*#__PURE__*/React.createElement(OpenLayerObserverNamespaceProvider, {
699
747
  namespace: openLayerObserverSideNavNamespace
@@ -705,6 +753,9 @@ export function SideNav(_ref8) {
705
753
  skipLinkLabel: skipLinkLabel,
706
754
  onExpand: onExpand,
707
755
  onCollapse: onCollapse,
708
- id: id
756
+ onPeekStart: onPeekStart,
757
+ onPeekEnd: onPeekEnd,
758
+ id: id,
759
+ canToggleWithShortcut: canToggleWithShortcut
709
760
  }, children));
710
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;
@@ -53,7 +53,30 @@ type SideNavProps = CommonSlotProps & {
53
53
  * Note: The trigger parameter is only provided when the `navx-full-height-sidebar` feature flag is enabled.
54
54
  */
55
55
  onCollapse?: VisibilityCallback;
56
+ /**
57
+ * Called when the side nav begins peeking / flyout.
58
+ */
59
+ onPeekStart?: () => void;
60
+ /**
61
+ * Called when the side nav stops peeking / flyout.
62
+ */
63
+ onPeekEnd?: (args: {
64
+ trigger: 'mouse-leave' | 'side-nav-expand';
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;
56
78
  };
79
+ export declare const onPeekStartDelayMs = 500;
57
80
  /**
58
81
  * The side navigation layout area. It will show on the left (inline start) of the screen.
59
82
  *
@@ -64,5 +87,5 @@ type SideNavProps = CommonSlotProps & {
64
87
  */
65
88
  export declare function SideNav({ children, defaultCollapsed, defaultWidth, testId, label, // Default value is defined in `SideNavInternal`
66
89
  skipLinkLabel, // Default value is defined in `SideNavInternal`
67
- onExpand, onCollapse, id, }: SideNavProps): JSX.Element;
90
+ onExpand, onCollapse, onPeekStart, onPeekEnd, canToggleWithShortcut, id, }: SideNavProps): JSX.Element;
68
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;
@@ -53,7 +53,30 @@ type SideNavProps = CommonSlotProps & {
53
53
  * Note: The trigger parameter is only provided when the `navx-full-height-sidebar` feature flag is enabled.
54
54
  */
55
55
  onCollapse?: VisibilityCallback;
56
+ /**
57
+ * Called when the side nav begins peeking / flyout.
58
+ */
59
+ onPeekStart?: () => void;
60
+ /**
61
+ * Called when the side nav stops peeking / flyout.
62
+ */
63
+ onPeekEnd?: (args: {
64
+ trigger: 'mouse-leave' | 'side-nav-expand';
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;
56
78
  };
79
+ export declare const onPeekStartDelayMs = 500;
57
80
  /**
58
81
  * The side navigation layout area. It will show on the left (inline start) of the screen.
59
82
  *
@@ -64,5 +87,5 @@ type SideNavProps = CommonSlotProps & {
64
87
  */
65
88
  export declare function SideNav({ children, defaultCollapsed, defaultWidth, testId, label, // Default value is defined in `SideNavInternal`
66
89
  skipLinkLabel, // Default value is defined in `SideNavInternal`
67
- onExpand, onCollapse, id, }: SideNavProps): JSX.Element;
90
+ onExpand, onCollapse, onPeekStart, onPeekEnd, canToggleWithShortcut, id, }: SideNavProps): JSX.Element;
68
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.3.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"