@atlaskit/page-layout 1.3.10 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @atlaskit/page-layout
2
2
 
3
+ ## 1.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`2a9f6f800ef`](https://bitbucket.org/atlassian/atlassian-frontend/commits/2a9f6f800ef) - **Fixes**
8
+
9
+ - `onLeftSidebarExpand` is no longer called when the sidebar is already open. `onLeftSidebarExpand` oculd previously be incorrectly called if a user resized an expanded sidebar to slightly smaller than the default sidebar width, or when the user cancelled a sidebar resizing operation with the `"Escape"` key
10
+ - the latest provided `onLeftSidebarCollapse` and `onLeftSidebarExpand` functions are now called when collapsing / expanding respectively. Previously, only the initial `onLeftSidebarCollapse` and `onLeftSidebarExpand` were called (due to a stale closure)
11
+ - `onLeftSidebarCollapse` and `onLeftSidebarExpand` are now called with the latest state values. Previously there were only ever called with the initial left sidebar state value (due to a stale closure)
12
+
13
+ **Improvements**
14
+
15
+ - no longer possible to trigger the collapse of the sidebar when it is already collapsed
16
+ - no longer possible to trigger an expand of the sidebar when it is already expanded
17
+ - triggering an expand while the sidebar is collapsing will now flush the pending `onLeftSidebarExpand`
18
+ - triggering an collapse while the sidebar is expanding will now flush the pending `onLeftSidebarCollapse`
19
+ - only adding the event listener for `"transitionend"` when the sidebar is expanding or collapsing.
20
+ - removing `"transitionend"` event listener when `<LeftSidebar />` is unmounted
21
+ - explicitly aborting pending collapse / expand actions when `<LeftSidebar />` is unmounted while collapsing / expanding.
22
+
23
+ ## 1.4.0
24
+
25
+ ### Minor Changes
26
+
27
+ - [`955ee3ea8fe`](https://bitbucket.org/atlassian/atlassian-frontend/commits/955ee3ea8fe) - [ux] **fix**: if a `"mousedown"`, `"click"`, `"resize"` or `"visibilitychange"` event occurs while the sidebar is being resized, then the resizing operation will end
28
+
29
+ [ux] **new**: if a user presses the `"Escape"` key while the sidebar is being resized, then the resizing operation will end
30
+
3
31
  ## 1.3.10
4
32
 
5
33
  ### Patch Changes
@@ -12,6 +12,7 @@ var _react = require("@emotion/react");
12
12
  var _colors = require("@atlaskit/theme/colors");
13
13
  var _constants = require("../../common/constants");
14
14
  var _excluded = ["testId", "isLeftSidebarCollapsed"];
15
+ /** @jsx jsx */
15
16
  /**
16
17
  * Determines the color of the grab line.
17
18
  *
@@ -19,7 +19,7 @@ var _grabArea = _interopRequireDefault(require("./grab-area"));
19
19
  var _resizeButton2 = _interopRequireDefault(require("./resize-button"));
20
20
  var _shadow = _interopRequireDefault(require("./shadow"));
21
21
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
22
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
22
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } /** @jsx jsx */ /* import useUpdateCssVar from '../../controllers/use-update-css-vars'; */
23
23
  var cssSelector = (0, _defineProperty2.default)({}, _constants.RESIZE_CONTROL_SELECTOR, true);
24
24
  var resizeControlStyles = (0, _react2.css)({
25
25
  position: 'absolute',
@@ -47,7 +47,7 @@ var ResizeControl = function ResizeControl(_ref) {
47
47
  setLeftSidebarState = _useContext.setLeftSidebarState;
48
48
  var isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed,
49
49
  isResizing = leftSidebarState.isResizing;
50
- var x = (0, _react.useRef)(leftSidebarState[_constants.VAR_LEFT_SIDEBAR_WIDTH]);
50
+ var sidebarWidth = (0, _react.useRef)(leftSidebarState[_constants.VAR_LEFT_SIDEBAR_WIDTH]);
51
51
  // Distance of mouse from left sidebar onMouseDown
52
52
  var offset = (0, _react.useRef)(0);
53
53
  var keyboardEventTimeout = (0, _react.useRef)();
@@ -77,13 +77,67 @@ var ResizeControl = function ResizeControl(_ref) {
77
77
  if (isLeftSidebarCollapsed) {
78
78
  return;
79
79
  }
80
+
81
+ // TODO: should only a primary pointer be able to start a resize?
82
+ // Keeping as is for now, but worth considering
83
+
84
+ // It is possible for a mousedown to fire during a resize
85
+ // Example: the user presses another pointer button while dragging
86
+ if (leftSidebarState.isResizing) {
87
+ // the resize will be cancelled by our global event listeners
88
+ return;
89
+ }
80
90
  offset.current = event.clientX - leftSidebarState[_constants.VAR_LEFT_SIDEBAR_WIDTH] - (0, _utils.getLeftPanelWidth)();
81
- unbindEvents.current = (0, _bindEventListener.bindAll)(document, [{
91
+ unbindEvents.current = (0, _bindEventListener.bindAll)(window, [{
82
92
  type: 'mousemove',
83
- listener: onMouseMove
93
+ listener: onUpdateResize
84
94
  }, {
85
95
  type: 'mouseup',
86
- listener: onMouseUp
96
+ listener: onFinishResizing
97
+ }, {
98
+ type: 'mousedown',
99
+ // this mousedown event listener is being added in the bubble phase
100
+ // on a higher event target than the resize handle.
101
+ // This means that the original mousedown event that triggers a resize
102
+ // can hit this mousedown handler. To get around that, we only call
103
+ // `onFinishResizing` after an animation frame so we don't pick up the original event
104
+ // Alternatives:
105
+ // 1. Add the window 'mousedown' event listener in the capture phase
106
+ // 👎 A 'mousedown' during a resize would trigger a new resize to start
107
+ // 2. Do 1. and call `event.preventDefault()`, then check for `event.defaultPrevented` inside
108
+ // the grab handle `onMouseDown`
109
+ // 👎 Not ideal to cancel events if we don't have to
110
+ listener: function () {
111
+ var hasFramePassed = false;
112
+ requestAnimationFrame(function () {
113
+ hasFramePassed = true;
114
+ });
115
+ return function listener() {
116
+ if (hasFramePassed) {
117
+ onFinishResizing();
118
+ }
119
+ };
120
+ }()
121
+ }, {
122
+ type: 'visibilitychange',
123
+ listener: onFinishResizing
124
+ },
125
+ // A 'click' event should never be hit as the 'mouseup' will come first and cause
126
+ // these event listeners to be unbound. I just added 'click' for extreme safety (paranoia)
127
+ {
128
+ type: 'click',
129
+ listener: onFinishResizing
130
+ }, {
131
+ type: 'keydown',
132
+ listener: function listener(event) {
133
+ // Can cancel resizing by pressing "Escape"
134
+ // Will return sidebar to the same size it was before the resizing started
135
+ if (event.key === 'Escape') {
136
+ sidebarWidth.current = Math.max(leftSidebarState.lastLeftSidebarWidth, _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH);
137
+ document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(sidebarWidth.current, "px"));
138
+ onFinishResizing();
139
+ }
140
+ }
87
141
  }]);
88
142
  document.documentElement.setAttribute(_constants.IS_SIDEBAR_DRAGGING, 'true');
89
143
  var newLeftbarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, {
@@ -92,37 +146,38 @@ var ResizeControl = function ResizeControl(_ref) {
92
146
  setLeftSidebarState(newLeftbarState);
93
147
  onResizeStart && onResizeStart(newLeftbarState);
94
148
  };
95
- var cancelDrag = function cancelDrag(shouldCollapse) {
149
+ var onResizeOffLeftOfScreen = function onResizeOffLeftOfScreen() {
96
150
  var _unbindEvents$current;
97
- onMouseMove.cancel();
151
+ onUpdateResize.cancel();
98
152
  (_unbindEvents$current = unbindEvents.current) === null || _unbindEvents$current === void 0 ? void 0 : _unbindEvents$current.call(unbindEvents);
99
153
  unbindEvents.current = null;
100
154
  document.documentElement.removeAttribute(_constants.IS_SIDEBAR_DRAGGING);
101
155
  offset.current = 0;
102
156
  collapseLeftSidebar(undefined, true);
103
157
  };
104
- var onMouseMove = (0, _rafSchd.default)(function (event) {
158
+ var onUpdateResize = (0, _rafSchd.default)(function (event) {
105
159
  // Allow the sidebar to be 50% of the available page width
106
160
  var maxWidth = Math.round(window.innerWidth / 2);
107
161
  var leftPanelWidth = (0, _utils.getLeftPanelWidth)();
108
162
  var leftSidebarWidth = leftSidebarState.leftSidebarWidth;
109
- var invalidDrag = event.clientX < 0;
110
- if (invalidDrag) {
111
- cancelDrag();
163
+ var hasResizedOffLeftOfScreen = event.clientX < 0;
164
+ if (hasResizedOffLeftOfScreen) {
165
+ onResizeOffLeftOfScreen();
166
+ return;
112
167
  }
113
168
  var delta = Math.max(Math.min(event.clientX - leftSidebarWidth - leftPanelWidth, maxWidth - leftSidebarWidth - leftPanelWidth), _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH - leftSidebarWidth - leftPanelWidth);
114
- x.current = Math.max(leftSidebarWidth + delta - offset.current, _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH);
115
- document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(x.current, "px"));
169
+ sidebarWidth.current = Math.max(leftSidebarWidth + delta - offset.current, _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH);
170
+ document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(sidebarWidth.current, "px"));
116
171
  });
117
172
  var cleanupAfterResize = function cleanupAfterResize() {
118
173
  var _unbindEvents$current2;
119
- x.current = 0;
174
+ sidebarWidth.current = 0;
120
175
  offset.current = 0;
121
176
  (_unbindEvents$current2 = unbindEvents.current) === null || _unbindEvents$current2 === void 0 ? void 0 : _unbindEvents$current2.call(unbindEvents);
122
177
  unbindEvents.current = null;
123
178
  };
124
179
  var updatedLeftSidebarState = {};
125
- var onMouseUp = function onMouseUp(event) {
180
+ var onFinishResizing = function onFinishResizing() {
126
181
  if (isLeftSidebarCollapsed) {
127
182
  return;
128
183
  }
@@ -130,14 +185,14 @@ var ResizeControl = function ResizeControl(_ref) {
130
185
 
131
186
  // If it is dragged to below the threshold,
132
187
  // collapse the navigation
133
- if (x.current < _constants.MIN_LEFT_SIDEBAR_DRAG_THRESHOLD) {
188
+ if (sidebarWidth.current < _constants.MIN_LEFT_SIDEBAR_DRAG_THRESHOLD) {
134
189
  document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(_constants.COLLAPSED_LEFT_SIDEBAR_WIDTH, "px"));
135
190
  collapseLeftSidebar(undefined, true);
136
191
  }
137
192
  // If it is dragged to position in between the
138
193
  // min threshold and default width
139
194
  // expand the nav to the default width
140
- else if (x.current > _constants.MIN_LEFT_SIDEBAR_DRAG_THRESHOLD && x.current < _constants.DEFAULT_LEFT_SIDEBAR_WIDTH) {
195
+ else if (sidebarWidth.current > _constants.MIN_LEFT_SIDEBAR_DRAG_THRESHOLD && sidebarWidth.current < _constants.DEFAULT_LEFT_SIDEBAR_WIDTH) {
141
196
  var _objectSpread2;
142
197
  document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(_constants.DEFAULT_LEFT_SIDEBAR_WIDTH, "px"));
143
198
  updatedLeftSidebarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, (_objectSpread2 = {
@@ -149,11 +204,11 @@ var ResizeControl = function ResizeControl(_ref) {
149
204
  // otherwise resize it to the desired width
150
205
  updatedLeftSidebarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, (_objectSpread3 = {
151
206
  isResizing: false
152
- }, (0, _defineProperty2.default)(_objectSpread3, _constants.VAR_LEFT_SIDEBAR_WIDTH, x.current), (0, _defineProperty2.default)(_objectSpread3, "lastLeftSidebarWidth", x.current), _objectSpread3));
207
+ }, (0, _defineProperty2.default)(_objectSpread3, _constants.VAR_LEFT_SIDEBAR_WIDTH, sidebarWidth.current), (0, _defineProperty2.default)(_objectSpread3, "lastLeftSidebarWidth", sidebarWidth.current), _objectSpread3));
153
208
  setLeftSidebarState(updatedLeftSidebarState);
154
209
  }
155
210
  requestAnimationFrame(function () {
156
- onMouseMove.cancel();
211
+ onUpdateResize.cancel();
157
212
  setIsGrabAreaFocused(false);
158
213
  onResizeEnd && onResizeEnd(updatedLeftSidebarState);
159
214
  cleanupAfterResize();
@@ -15,6 +15,7 @@ var _durations = require("@atlaskit/motion/durations");
15
15
  var _colors = require("@atlaskit/theme/colors");
16
16
  var _constants = require("../../common/constants");
17
17
  var _excluded = ["isLeftSidebarCollapsed", "label", "testId"];
18
+ /** @jsx jsx */
18
19
  var increaseHitAreaStyles = (0, _react.css)({
19
20
  position: 'absolute',
20
21
  top: -8,
@@ -13,7 +13,7 @@ var _colors = require("@atlaskit/theme/colors");
13
13
  var _constants = require("../../common/constants");
14
14
  var _controllers = require("../../controllers");
15
15
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
16
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
16
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } /* eslint-disable @repo/internal/dom-events/no-unsafe-event-listeners */ /** @jsx jsx */
17
17
  // eslint-disable-next-line @repo/internal/react/consistent-css-prop-usage
18
18
  var prefersReducedMotionStyles = (0, _react.css)((0, _motion.prefersReducedMotion)());
19
19
  var skipLinkStyles = (0, _react.css)({
@@ -17,7 +17,7 @@ var _leftSidebarOuter = _interopRequireDefault(require("./internal/left-sidebar-
17
17
  var _resizableChildrenWrapper = _interopRequireDefault(require("./internal/resizable-children-wrapper"));
18
18
  var _slotDimensions = _interopRequireDefault(require("./slot-dimensions"));
19
19
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
20
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
20
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } /* eslint-disable @repo/internal/dom-events/no-unsafe-event-listeners */ /** @jsx jsx */
21
21
  /**
22
22
  * __Left sidebar__
23
23
  *
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "__esModule", {
8
8
  exports.SidebarResizeController = void 0;
9
9
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
10
10
  var _react = _interopRequireWildcard(require("react"));
11
+ var _bindEventListener = require("bind-event-listener");
11
12
  var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
12
13
  var _motion = require("@atlaskit/motion");
13
14
  var _constants = require("../common/constants");
@@ -22,7 +23,7 @@ var handleDataAttributesAndCb = function handleDataAttributesAndCb() {
22
23
  document.documentElement.removeAttribute(_constants.IS_SIDEBAR_COLLAPSING);
23
24
  callback(leftSidebarState);
24
25
  };
25
-
26
+ var leftSidebarSelector = (0, _utils.getPageLayoutSlotCSSSelector)('left-sidebar');
26
27
  // eslint-disable-next-line @repo/internal/react/require-jsdoc
27
28
  var SidebarResizeController = function SidebarResizeController(_ref) {
28
29
  var children = _ref.children,
@@ -41,34 +42,35 @@ var SidebarResizeController = function SidebarResizeController(_ref) {
41
42
  leftSidebarState = _useState2[0],
42
43
  setLeftSidebarState = _useState2[1];
43
44
  var isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed;
44
- var leftSidebarSelector = (0, _utils.getPageLayoutSlotCSSSelector)('left-sidebar');
45
- var transitionEventHandler = (0, _react.useCallback)(function (event) {
46
- if (event.propertyName === 'width' && event.target && event.target.matches(leftSidebarSelector)) {
47
- var $leftSidebarResizeController = document.querySelector("[".concat(_constants.GRAB_AREA_SELECTOR, "]"));
48
- var isCollapsed = !!$leftSidebarResizeController && $leftSidebarResizeController.hasAttribute('disabled');
49
- handleDataAttributesAndCb(isCollapsed ? onCollapse : onExpand, isCollapsed, leftSidebarState);
50
45
 
51
- // Make sure multiple event handlers do not get attached
52
- // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
53
- document.querySelector(leftSidebarSelector).removeEventListener('transitionend', transitionEventHandler);
54
- }
55
- // eslint-disable-next-line react-hooks/exhaustive-deps
56
- }, []);
46
+ // We put the latest callbacks into a ref so we can always have the latest
47
+ // functions in our transitionend listeners
48
+ var stableRef = (0, _react.useRef)({
49
+ onExpand: onExpand,
50
+ onCollapse: onCollapse
51
+ });
57
52
  (0, _react.useEffect)(function () {
58
- var $leftSidebar = document.querySelector(leftSidebarSelector);
59
- if ($leftSidebar && !(0, _motion.isReducedMotion)()) {
60
- // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
61
- $leftSidebar.addEventListener('transitionend', transitionEventHandler);
62
- }
63
- }, [isLeftSidebarCollapsed, leftSidebarSelector, leftSidebarState, onCollapse, onExpand, transitionEventHandler]);
53
+ stableRef.current = {
54
+ onExpand: onExpand,
55
+ onCollapse: onCollapse
56
+ };
57
+ });
58
+ var transition = (0, _react.useRef)(null);
64
59
  var expandLeftSidebar = (0, _react.useCallback)(function () {
60
+ var _transition$current, _transition$current2;
65
61
  var lastLeftSidebarWidth = leftSidebarState.lastLeftSidebarWidth,
66
62
  isResizing = leftSidebarState.isResizing,
67
63
  flyoutLockCount = leftSidebarState.flyoutLockCount,
68
- isFixed = leftSidebarState.isFixed;
69
- if (isResizing) {
64
+ isFixed = leftSidebarState.isFixed,
65
+ isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed;
66
+ if (isResizing || !isLeftSidebarCollapsed ||
67
+ // already expanding
68
+ ((_transition$current = transition.current) === null || _transition$current === void 0 ? void 0 : _transition$current.action) === 'expand') {
70
69
  return;
71
70
  }
71
+
72
+ // flush existing transition
73
+ (_transition$current2 = transition.current) === null || _transition$current2 === void 0 ? void 0 : _transition$current2.complete();
72
74
  var width = Math.max(lastLeftSidebarWidth, _constants.DEFAULT_LEFT_SIDEBAR_WIDTH);
73
75
  var updatedLeftSidebarState = {
74
76
  isLeftSidebarCollapsed: false,
@@ -80,22 +82,55 @@ var SidebarResizeController = function SidebarResizeController(_ref) {
80
82
  isFixed: isFixed
81
83
  };
82
84
  setLeftSidebarState(updatedLeftSidebarState);
83
-
85
+ function finish() {
86
+ handleDataAttributesAndCb(stableRef.current.onExpand, false,
87
+ // isCollapsed
88
+ updatedLeftSidebarState);
89
+ }
90
+ var sidebar = document.querySelector(leftSidebarSelector);
84
91
  // onTransitionEnd isn't triggered when a user prefers reduced motion
85
- if ((0, _motion.isReducedMotion)()) {
86
- handleDataAttributesAndCb(onExpand, false, updatedLeftSidebarState);
92
+ if ((0, _motion.isReducedMotion)() || !sidebar) {
93
+ finish();
94
+ return;
87
95
  }
88
- }, [leftSidebarState, onExpand]);
96
+ var unbindEvent = (0, _bindEventListener.bind)(sidebar, {
97
+ type: 'transitionend',
98
+ listener: function listener(event) {
99
+ if (event.target === sidebar && event.propertyName === 'width') {
100
+ var _transition$current3;
101
+ (_transition$current3 = transition.current) === null || _transition$current3 === void 0 ? void 0 : _transition$current3.complete();
102
+ }
103
+ }
104
+ });
105
+ var value = {
106
+ action: 'expand',
107
+ complete: function complete() {
108
+ value.abort();
109
+ finish();
110
+ },
111
+ abort: function abort() {
112
+ unbindEvent();
113
+ transition.current = null;
114
+ }
115
+ };
116
+ transition.current = value;
117
+ }, [leftSidebarState]);
89
118
  var collapseLeftSidebar = (0, _react.useCallback)(function (event, collapseWithoutTransition) {
119
+ var _transition$current4, _transition$current5;
90
120
  var leftSidebarWidth = leftSidebarState.leftSidebarWidth,
91
121
  isResizing = leftSidebarState.isResizing,
92
122
  flyoutLockCount = leftSidebarState.flyoutLockCount,
93
123
  isFixed = leftSidebarState.isFixed,
94
124
  isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed;
95
- if (isResizing || isLeftSidebarCollapsed) {
125
+ if (isResizing || isLeftSidebarCollapsed ||
126
+ // already collapsing
127
+ ((_transition$current4 = transition.current) === null || _transition$current4 === void 0 ? void 0 : _transition$current4.action) === 'collapse') {
96
128
  return;
97
129
  }
98
130
 
131
+ // flush existing transition
132
+ (_transition$current5 = transition.current) === null || _transition$current5 === void 0 ? void 0 : _transition$current5.complete();
133
+
99
134
  // data-attribute is used as a CSS selector to sync the hiding/showing
100
135
  // of the nav contents with expand/collapse animation
101
136
  document.documentElement.setAttribute(_constants.IS_SIDEBAR_COLLAPSING, 'true');
@@ -109,12 +144,46 @@ var SidebarResizeController = function SidebarResizeController(_ref) {
109
144
  isFixed: isFixed
110
145
  };
111
146
  setLeftSidebarState(updatedLeftSidebarState);
147
+ function finish() {
148
+ handleDataAttributesAndCb(stableRef.current.onCollapse, true, updatedLeftSidebarState);
149
+ }
150
+ var sidebar = document.querySelector(leftSidebarSelector);
112
151
 
113
152
  // onTransitionEnd isn't triggered when a user prefers reduced motion
114
- if (collapseWithoutTransition || (0, _motion.isReducedMotion)()) {
115
- handleDataAttributesAndCb(onCollapse, true, updatedLeftSidebarState);
153
+ if (collapseWithoutTransition || (0, _motion.isReducedMotion)() || !sidebar) {
154
+ finish();
155
+ return;
116
156
  }
117
- }, [leftSidebarState, onCollapse]);
157
+ var unbindEvent = (0, _bindEventListener.bind)(sidebar, {
158
+ type: 'transitionend',
159
+ listener: function listener(event) {
160
+ if (sidebar === event.target && event.propertyName === 'width') {
161
+ var _transition$current6;
162
+ (_transition$current6 = transition.current) === null || _transition$current6 === void 0 ? void 0 : _transition$current6.complete();
163
+ }
164
+ }
165
+ });
166
+ var value = {
167
+ action: 'collapse',
168
+ complete: function complete() {
169
+ value.abort();
170
+ finish();
171
+ },
172
+ abort: function abort() {
173
+ unbindEvent();
174
+ transition.current = null;
175
+ }
176
+ };
177
+ transition.current = value;
178
+ }, [leftSidebarState]);
179
+
180
+ // Make sure we finish any lingering transitions when unmounting
181
+ (0, _react.useEffect)(function mount() {
182
+ return function unmount() {
183
+ var _transition$current7;
184
+ (_transition$current7 = transition.current) === null || _transition$current7 === void 0 ? void 0 : _transition$current7.abort();
185
+ };
186
+ }, []);
118
187
  var context = (0, _react.useMemo)(function () {
119
188
  return {
120
189
  isLeftSidebarCollapsed: isLeftSidebarCollapsed,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/page-layout",
3
- "version": "1.3.10",
3
+ "version": "1.5.0",
4
4
  "sideEffects": false
5
5
  }
@@ -43,7 +43,7 @@ const ResizeControl = ({
43
43
  isLeftSidebarCollapsed,
44
44
  isResizing
45
45
  } = leftSidebarState;
46
- const x = useRef(leftSidebarState[VAR_LEFT_SIDEBAR_WIDTH]);
46
+ const sidebarWidth = useRef(leftSidebarState[VAR_LEFT_SIDEBAR_WIDTH]);
47
47
  // Distance of mouse from left sidebar onMouseDown
48
48
  const offset = useRef(0);
49
49
  const keyboardEventTimeout = useRef();
@@ -70,13 +70,67 @@ const ResizeControl = ({
70
70
  if (isLeftSidebarCollapsed) {
71
71
  return;
72
72
  }
73
+
74
+ // TODO: should only a primary pointer be able to start a resize?
75
+ // Keeping as is for now, but worth considering
76
+
77
+ // It is possible for a mousedown to fire during a resize
78
+ // Example: the user presses another pointer button while dragging
79
+ if (leftSidebarState.isResizing) {
80
+ // the resize will be cancelled by our global event listeners
81
+ return;
82
+ }
73
83
  offset.current = event.clientX - leftSidebarState[VAR_LEFT_SIDEBAR_WIDTH] - getLeftPanelWidth();
74
- unbindEvents.current = bindAll(document, [{
84
+ unbindEvents.current = bindAll(window, [{
75
85
  type: 'mousemove',
76
- listener: onMouseMove
86
+ listener: onUpdateResize
77
87
  }, {
78
88
  type: 'mouseup',
79
- listener: onMouseUp
89
+ listener: onFinishResizing
90
+ }, {
91
+ type: 'mousedown',
92
+ // this mousedown event listener is being added in the bubble phase
93
+ // on a higher event target than the resize handle.
94
+ // This means that the original mousedown event that triggers a resize
95
+ // can hit this mousedown handler. To get around that, we only call
96
+ // `onFinishResizing` after an animation frame so we don't pick up the original event
97
+ // Alternatives:
98
+ // 1. Add the window 'mousedown' event listener in the capture phase
99
+ // 👎 A 'mousedown' during a resize would trigger a new resize to start
100
+ // 2. Do 1. and call `event.preventDefault()`, then check for `event.defaultPrevented` inside
101
+ // the grab handle `onMouseDown`
102
+ // 👎 Not ideal to cancel events if we don't have to
103
+ listener: (() => {
104
+ let hasFramePassed = false;
105
+ requestAnimationFrame(() => {
106
+ hasFramePassed = true;
107
+ });
108
+ return function listener() {
109
+ if (hasFramePassed) {
110
+ onFinishResizing();
111
+ }
112
+ };
113
+ })()
114
+ }, {
115
+ type: 'visibilitychange',
116
+ listener: onFinishResizing
117
+ },
118
+ // A 'click' event should never be hit as the 'mouseup' will come first and cause
119
+ // these event listeners to be unbound. I just added 'click' for extreme safety (paranoia)
120
+ {
121
+ type: 'click',
122
+ listener: onFinishResizing
123
+ }, {
124
+ type: 'keydown',
125
+ listener: event => {
126
+ // Can cancel resizing by pressing "Escape"
127
+ // Will return sidebar to the same size it was before the resizing started
128
+ if (event.key === 'Escape') {
129
+ sidebarWidth.current = Math.max(leftSidebarState.lastLeftSidebarWidth, COLLAPSED_LEFT_SIDEBAR_WIDTH);
130
+ document.documentElement.style.setProperty(`--${VAR_LEFT_SIDEBAR_WIDTH}`, `${sidebarWidth.current}px`);
131
+ onFinishResizing();
132
+ }
133
+ }
80
134
  }]);
81
135
  document.documentElement.setAttribute(IS_SIDEBAR_DRAGGING, 'true');
82
136
  const newLeftbarState = {
@@ -86,39 +140,40 @@ const ResizeControl = ({
86
140
  setLeftSidebarState(newLeftbarState);
87
141
  onResizeStart && onResizeStart(newLeftbarState);
88
142
  };
89
- const cancelDrag = shouldCollapse => {
143
+ const onResizeOffLeftOfScreen = () => {
90
144
  var _unbindEvents$current;
91
- onMouseMove.cancel();
145
+ onUpdateResize.cancel();
92
146
  (_unbindEvents$current = unbindEvents.current) === null || _unbindEvents$current === void 0 ? void 0 : _unbindEvents$current.call(unbindEvents);
93
147
  unbindEvents.current = null;
94
148
  document.documentElement.removeAttribute(IS_SIDEBAR_DRAGGING);
95
149
  offset.current = 0;
96
150
  collapseLeftSidebar(undefined, true);
97
151
  };
98
- const onMouseMove = rafSchd(event => {
152
+ const onUpdateResize = rafSchd(event => {
99
153
  // Allow the sidebar to be 50% of the available page width
100
154
  const maxWidth = Math.round(window.innerWidth / 2);
101
155
  const leftPanelWidth = getLeftPanelWidth();
102
156
  const {
103
157
  leftSidebarWidth
104
158
  } = leftSidebarState;
105
- const invalidDrag = event.clientX < 0;
106
- if (invalidDrag) {
107
- cancelDrag();
159
+ const hasResizedOffLeftOfScreen = event.clientX < 0;
160
+ if (hasResizedOffLeftOfScreen) {
161
+ onResizeOffLeftOfScreen();
162
+ return;
108
163
  }
109
164
  const delta = Math.max(Math.min(event.clientX - leftSidebarWidth - leftPanelWidth, maxWidth - leftSidebarWidth - leftPanelWidth), COLLAPSED_LEFT_SIDEBAR_WIDTH - leftSidebarWidth - leftPanelWidth);
110
- x.current = Math.max(leftSidebarWidth + delta - offset.current, COLLAPSED_LEFT_SIDEBAR_WIDTH);
111
- document.documentElement.style.setProperty(`--${VAR_LEFT_SIDEBAR_WIDTH}`, `${x.current}px`);
165
+ sidebarWidth.current = Math.max(leftSidebarWidth + delta - offset.current, COLLAPSED_LEFT_SIDEBAR_WIDTH);
166
+ document.documentElement.style.setProperty(`--${VAR_LEFT_SIDEBAR_WIDTH}`, `${sidebarWidth.current}px`);
112
167
  });
113
168
  const cleanupAfterResize = () => {
114
169
  var _unbindEvents$current2;
115
- x.current = 0;
170
+ sidebarWidth.current = 0;
116
171
  offset.current = 0;
117
172
  (_unbindEvents$current2 = unbindEvents.current) === null || _unbindEvents$current2 === void 0 ? void 0 : _unbindEvents$current2.call(unbindEvents);
118
173
  unbindEvents.current = null;
119
174
  };
120
175
  let updatedLeftSidebarState = {};
121
- const onMouseUp = event => {
176
+ const onFinishResizing = () => {
122
177
  if (isLeftSidebarCollapsed) {
123
178
  return;
124
179
  }
@@ -126,14 +181,14 @@ const ResizeControl = ({
126
181
 
127
182
  // If it is dragged to below the threshold,
128
183
  // collapse the navigation
129
- if (x.current < MIN_LEFT_SIDEBAR_DRAG_THRESHOLD) {
184
+ if (sidebarWidth.current < MIN_LEFT_SIDEBAR_DRAG_THRESHOLD) {
130
185
  document.documentElement.style.setProperty(`--${VAR_LEFT_SIDEBAR_WIDTH}`, `${COLLAPSED_LEFT_SIDEBAR_WIDTH}px`);
131
186
  collapseLeftSidebar(undefined, true);
132
187
  }
133
188
  // If it is dragged to position in between the
134
189
  // min threshold and default width
135
190
  // expand the nav to the default width
136
- else if (x.current > MIN_LEFT_SIDEBAR_DRAG_THRESHOLD && x.current < DEFAULT_LEFT_SIDEBAR_WIDTH) {
191
+ else if (sidebarWidth.current > MIN_LEFT_SIDEBAR_DRAG_THRESHOLD && sidebarWidth.current < DEFAULT_LEFT_SIDEBAR_WIDTH) {
137
192
  document.documentElement.style.setProperty(`--${VAR_LEFT_SIDEBAR_WIDTH}`, `${DEFAULT_LEFT_SIDEBAR_WIDTH}px`);
138
193
  updatedLeftSidebarState = {
139
194
  ...leftSidebarState,
@@ -147,13 +202,13 @@ const ResizeControl = ({
147
202
  updatedLeftSidebarState = {
148
203
  ...leftSidebarState,
149
204
  isResizing: false,
150
- [VAR_LEFT_SIDEBAR_WIDTH]: x.current,
151
- lastLeftSidebarWidth: x.current
205
+ [VAR_LEFT_SIDEBAR_WIDTH]: sidebarWidth.current,
206
+ lastLeftSidebarWidth: sidebarWidth.current
152
207
  };
153
208
  setLeftSidebarState(updatedLeftSidebarState);
154
209
  }
155
210
  requestAnimationFrame(() => {
156
- onMouseMove.cancel();
211
+ onUpdateResize.cancel();
157
212
  setIsGrabAreaFocused(false);
158
213
  onResizeEnd && onResizeEnd(updatedLeftSidebarState);
159
214
  cleanupAfterResize();
@@ -1,14 +1,15 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { bind } from 'bind-event-listener';
2
3
  import noop from '@atlaskit/ds-lib/noop';
3
4
  import { isReducedMotion } from '@atlaskit/motion';
4
- import { COLLAPSED_LEFT_SIDEBAR_WIDTH, DEFAULT_LEFT_SIDEBAR_WIDTH, GRAB_AREA_SELECTOR, IS_SIDEBAR_COLLAPSING } from '../common/constants';
5
+ import { COLLAPSED_LEFT_SIDEBAR_WIDTH, DEFAULT_LEFT_SIDEBAR_WIDTH, IS_SIDEBAR_COLLAPSING } from '../common/constants';
5
6
  import { getPageLayoutSlotCSSSelector } from '../common/utils';
6
7
  import { SidebarResizeContext } from './sidebar-resize-context';
7
8
  const handleDataAttributesAndCb = (callback = noop, isLeftSidebarCollapsed, leftSidebarState) => {
8
9
  document.documentElement.removeAttribute(IS_SIDEBAR_COLLAPSING);
9
10
  callback(leftSidebarState);
10
11
  };
11
-
12
+ const leftSidebarSelector = getPageLayoutSlotCSSSelector('left-sidebar');
12
13
  // eslint-disable-next-line @repo/internal/react/require-jsdoc
13
14
  export const SidebarResizeController = ({
14
15
  children,
@@ -27,36 +28,37 @@ export const SidebarResizeController = ({
27
28
  const {
28
29
  isLeftSidebarCollapsed
29
30
  } = leftSidebarState;
30
- const leftSidebarSelector = getPageLayoutSlotCSSSelector('left-sidebar');
31
- const transitionEventHandler = useCallback(event => {
32
- if (event.propertyName === 'width' && event.target && event.target.matches(leftSidebarSelector)) {
33
- const $leftSidebarResizeController = document.querySelector(`[${GRAB_AREA_SELECTOR}]`);
34
- const isCollapsed = !!$leftSidebarResizeController && $leftSidebarResizeController.hasAttribute('disabled');
35
- handleDataAttributesAndCb(isCollapsed ? onCollapse : onExpand, isCollapsed, leftSidebarState);
36
31
 
37
- // Make sure multiple event handlers do not get attached
38
- // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
39
- document.querySelector(leftSidebarSelector).removeEventListener('transitionend', transitionEventHandler);
40
- }
41
- // eslint-disable-next-line react-hooks/exhaustive-deps
42
- }, []);
32
+ // We put the latest callbacks into a ref so we can always have the latest
33
+ // functions in our transitionend listeners
34
+ const stableRef = useRef({
35
+ onExpand,
36
+ onCollapse
37
+ });
43
38
  useEffect(() => {
44
- const $leftSidebar = document.querySelector(leftSidebarSelector);
45
- if ($leftSidebar && !isReducedMotion()) {
46
- // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
47
- $leftSidebar.addEventListener('transitionend', transitionEventHandler);
48
- }
49
- }, [isLeftSidebarCollapsed, leftSidebarSelector, leftSidebarState, onCollapse, onExpand, transitionEventHandler]);
39
+ stableRef.current = {
40
+ onExpand,
41
+ onCollapse
42
+ };
43
+ });
44
+ const transition = useRef(null);
50
45
  const expandLeftSidebar = useCallback(() => {
46
+ var _transition$current, _transition$current2;
51
47
  const {
52
48
  lastLeftSidebarWidth,
53
49
  isResizing,
54
50
  flyoutLockCount,
55
- isFixed
51
+ isFixed,
52
+ isLeftSidebarCollapsed
56
53
  } = leftSidebarState;
57
- if (isResizing) {
54
+ if (isResizing || !isLeftSidebarCollapsed ||
55
+ // already expanding
56
+ ((_transition$current = transition.current) === null || _transition$current === void 0 ? void 0 : _transition$current.action) === 'expand') {
58
57
  return;
59
58
  }
59
+
60
+ // flush existing transition
61
+ (_transition$current2 = transition.current) === null || _transition$current2 === void 0 ? void 0 : _transition$current2.complete();
60
62
  const width = Math.max(lastLeftSidebarWidth, DEFAULT_LEFT_SIDEBAR_WIDTH);
61
63
  const updatedLeftSidebarState = {
62
64
  isLeftSidebarCollapsed: false,
@@ -68,13 +70,41 @@ export const SidebarResizeController = ({
68
70
  isFixed
69
71
  };
70
72
  setLeftSidebarState(updatedLeftSidebarState);
71
-
73
+ function finish() {
74
+ handleDataAttributesAndCb(stableRef.current.onExpand, false,
75
+ // isCollapsed
76
+ updatedLeftSidebarState);
77
+ }
78
+ const sidebar = document.querySelector(leftSidebarSelector);
72
79
  // onTransitionEnd isn't triggered when a user prefers reduced motion
73
- if (isReducedMotion()) {
74
- handleDataAttributesAndCb(onExpand, false, updatedLeftSidebarState);
80
+ if (isReducedMotion() || !sidebar) {
81
+ finish();
82
+ return;
75
83
  }
76
- }, [leftSidebarState, onExpand]);
84
+ const unbindEvent = bind(sidebar, {
85
+ type: 'transitionend',
86
+ listener(event) {
87
+ if (event.target === sidebar && event.propertyName === 'width') {
88
+ var _transition$current3;
89
+ (_transition$current3 = transition.current) === null || _transition$current3 === void 0 ? void 0 : _transition$current3.complete();
90
+ }
91
+ }
92
+ });
93
+ const value = {
94
+ action: 'expand',
95
+ complete: () => {
96
+ value.abort();
97
+ finish();
98
+ },
99
+ abort: () => {
100
+ unbindEvent();
101
+ transition.current = null;
102
+ }
103
+ };
104
+ transition.current = value;
105
+ }, [leftSidebarState]);
77
106
  const collapseLeftSidebar = useCallback((event, collapseWithoutTransition) => {
107
+ var _transition$current4, _transition$current5;
78
108
  const {
79
109
  leftSidebarWidth,
80
110
  isResizing,
@@ -82,10 +112,15 @@ export const SidebarResizeController = ({
82
112
  isFixed,
83
113
  isLeftSidebarCollapsed
84
114
  } = leftSidebarState;
85
- if (isResizing || isLeftSidebarCollapsed) {
115
+ if (isResizing || isLeftSidebarCollapsed ||
116
+ // already collapsing
117
+ ((_transition$current4 = transition.current) === null || _transition$current4 === void 0 ? void 0 : _transition$current4.action) === 'collapse') {
86
118
  return;
87
119
  }
88
120
 
121
+ // flush existing transition
122
+ (_transition$current5 = transition.current) === null || _transition$current5 === void 0 ? void 0 : _transition$current5.complete();
123
+
89
124
  // data-attribute is used as a CSS selector to sync the hiding/showing
90
125
  // of the nav contents with expand/collapse animation
91
126
  document.documentElement.setAttribute(IS_SIDEBAR_COLLAPSING, 'true');
@@ -99,12 +134,46 @@ export const SidebarResizeController = ({
99
134
  isFixed
100
135
  };
101
136
  setLeftSidebarState(updatedLeftSidebarState);
137
+ function finish() {
138
+ handleDataAttributesAndCb(stableRef.current.onCollapse, true, updatedLeftSidebarState);
139
+ }
140
+ const sidebar = document.querySelector(leftSidebarSelector);
102
141
 
103
142
  // onTransitionEnd isn't triggered when a user prefers reduced motion
104
- if (collapseWithoutTransition || isReducedMotion()) {
105
- handleDataAttributesAndCb(onCollapse, true, updatedLeftSidebarState);
143
+ if (collapseWithoutTransition || isReducedMotion() || !sidebar) {
144
+ finish();
145
+ return;
106
146
  }
107
- }, [leftSidebarState, onCollapse]);
147
+ const unbindEvent = bind(sidebar, {
148
+ type: 'transitionend',
149
+ listener(event) {
150
+ if (sidebar === event.target && event.propertyName === 'width') {
151
+ var _transition$current6;
152
+ (_transition$current6 = transition.current) === null || _transition$current6 === void 0 ? void 0 : _transition$current6.complete();
153
+ }
154
+ }
155
+ });
156
+ const value = {
157
+ action: 'collapse',
158
+ complete: () => {
159
+ value.abort();
160
+ finish();
161
+ },
162
+ abort: () => {
163
+ unbindEvent();
164
+ transition.current = null;
165
+ }
166
+ };
167
+ transition.current = value;
168
+ }, [leftSidebarState]);
169
+
170
+ // Make sure we finish any lingering transitions when unmounting
171
+ useEffect(function mount() {
172
+ return function unmount() {
173
+ var _transition$current7;
174
+ (_transition$current7 = transition.current) === null || _transition$current7 === void 0 ? void 0 : _transition$current7.abort();
175
+ };
176
+ }, []);
108
177
  const context = useMemo(() => ({
109
178
  isLeftSidebarCollapsed,
110
179
  expandLeftSidebar,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/page-layout",
3
- "version": "1.3.10",
3
+ "version": "1.5.0",
4
4
  "sideEffects": false
5
5
  }
@@ -43,7 +43,7 @@ var ResizeControl = function ResizeControl(_ref) {
43
43
  setLeftSidebarState = _useContext.setLeftSidebarState;
44
44
  var isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed,
45
45
  isResizing = leftSidebarState.isResizing;
46
- var x = useRef(leftSidebarState[VAR_LEFT_SIDEBAR_WIDTH]);
46
+ var sidebarWidth = useRef(leftSidebarState[VAR_LEFT_SIDEBAR_WIDTH]);
47
47
  // Distance of mouse from left sidebar onMouseDown
48
48
  var offset = useRef(0);
49
49
  var keyboardEventTimeout = useRef();
@@ -73,13 +73,67 @@ var ResizeControl = function ResizeControl(_ref) {
73
73
  if (isLeftSidebarCollapsed) {
74
74
  return;
75
75
  }
76
+
77
+ // TODO: should only a primary pointer be able to start a resize?
78
+ // Keeping as is for now, but worth considering
79
+
80
+ // It is possible for a mousedown to fire during a resize
81
+ // Example: the user presses another pointer button while dragging
82
+ if (leftSidebarState.isResizing) {
83
+ // the resize will be cancelled by our global event listeners
84
+ return;
85
+ }
76
86
  offset.current = event.clientX - leftSidebarState[VAR_LEFT_SIDEBAR_WIDTH] - getLeftPanelWidth();
77
- unbindEvents.current = bindAll(document, [{
87
+ unbindEvents.current = bindAll(window, [{
78
88
  type: 'mousemove',
79
- listener: onMouseMove
89
+ listener: onUpdateResize
80
90
  }, {
81
91
  type: 'mouseup',
82
- listener: onMouseUp
92
+ listener: onFinishResizing
93
+ }, {
94
+ type: 'mousedown',
95
+ // this mousedown event listener is being added in the bubble phase
96
+ // on a higher event target than the resize handle.
97
+ // This means that the original mousedown event that triggers a resize
98
+ // can hit this mousedown handler. To get around that, we only call
99
+ // `onFinishResizing` after an animation frame so we don't pick up the original event
100
+ // Alternatives:
101
+ // 1. Add the window 'mousedown' event listener in the capture phase
102
+ // 👎 A 'mousedown' during a resize would trigger a new resize to start
103
+ // 2. Do 1. and call `event.preventDefault()`, then check for `event.defaultPrevented` inside
104
+ // the grab handle `onMouseDown`
105
+ // 👎 Not ideal to cancel events if we don't have to
106
+ listener: function () {
107
+ var hasFramePassed = false;
108
+ requestAnimationFrame(function () {
109
+ hasFramePassed = true;
110
+ });
111
+ return function listener() {
112
+ if (hasFramePassed) {
113
+ onFinishResizing();
114
+ }
115
+ };
116
+ }()
117
+ }, {
118
+ type: 'visibilitychange',
119
+ listener: onFinishResizing
120
+ },
121
+ // A 'click' event should never be hit as the 'mouseup' will come first and cause
122
+ // these event listeners to be unbound. I just added 'click' for extreme safety (paranoia)
123
+ {
124
+ type: 'click',
125
+ listener: onFinishResizing
126
+ }, {
127
+ type: 'keydown',
128
+ listener: function listener(event) {
129
+ // Can cancel resizing by pressing "Escape"
130
+ // Will return sidebar to the same size it was before the resizing started
131
+ if (event.key === 'Escape') {
132
+ sidebarWidth.current = Math.max(leftSidebarState.lastLeftSidebarWidth, COLLAPSED_LEFT_SIDEBAR_WIDTH);
133
+ document.documentElement.style.setProperty("--".concat(VAR_LEFT_SIDEBAR_WIDTH), "".concat(sidebarWidth.current, "px"));
134
+ onFinishResizing();
135
+ }
136
+ }
83
137
  }]);
84
138
  document.documentElement.setAttribute(IS_SIDEBAR_DRAGGING, 'true');
85
139
  var newLeftbarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, {
@@ -88,37 +142,38 @@ var ResizeControl = function ResizeControl(_ref) {
88
142
  setLeftSidebarState(newLeftbarState);
89
143
  onResizeStart && onResizeStart(newLeftbarState);
90
144
  };
91
- var cancelDrag = function cancelDrag(shouldCollapse) {
145
+ var onResizeOffLeftOfScreen = function onResizeOffLeftOfScreen() {
92
146
  var _unbindEvents$current;
93
- onMouseMove.cancel();
147
+ onUpdateResize.cancel();
94
148
  (_unbindEvents$current = unbindEvents.current) === null || _unbindEvents$current === void 0 ? void 0 : _unbindEvents$current.call(unbindEvents);
95
149
  unbindEvents.current = null;
96
150
  document.documentElement.removeAttribute(IS_SIDEBAR_DRAGGING);
97
151
  offset.current = 0;
98
152
  collapseLeftSidebar(undefined, true);
99
153
  };
100
- var onMouseMove = rafSchd(function (event) {
154
+ var onUpdateResize = rafSchd(function (event) {
101
155
  // Allow the sidebar to be 50% of the available page width
102
156
  var maxWidth = Math.round(window.innerWidth / 2);
103
157
  var leftPanelWidth = getLeftPanelWidth();
104
158
  var leftSidebarWidth = leftSidebarState.leftSidebarWidth;
105
- var invalidDrag = event.clientX < 0;
106
- if (invalidDrag) {
107
- cancelDrag();
159
+ var hasResizedOffLeftOfScreen = event.clientX < 0;
160
+ if (hasResizedOffLeftOfScreen) {
161
+ onResizeOffLeftOfScreen();
162
+ return;
108
163
  }
109
164
  var delta = Math.max(Math.min(event.clientX - leftSidebarWidth - leftPanelWidth, maxWidth - leftSidebarWidth - leftPanelWidth), COLLAPSED_LEFT_SIDEBAR_WIDTH - leftSidebarWidth - leftPanelWidth);
110
- x.current = Math.max(leftSidebarWidth + delta - offset.current, COLLAPSED_LEFT_SIDEBAR_WIDTH);
111
- document.documentElement.style.setProperty("--".concat(VAR_LEFT_SIDEBAR_WIDTH), "".concat(x.current, "px"));
165
+ sidebarWidth.current = Math.max(leftSidebarWidth + delta - offset.current, COLLAPSED_LEFT_SIDEBAR_WIDTH);
166
+ document.documentElement.style.setProperty("--".concat(VAR_LEFT_SIDEBAR_WIDTH), "".concat(sidebarWidth.current, "px"));
112
167
  });
113
168
  var cleanupAfterResize = function cleanupAfterResize() {
114
169
  var _unbindEvents$current2;
115
- x.current = 0;
170
+ sidebarWidth.current = 0;
116
171
  offset.current = 0;
117
172
  (_unbindEvents$current2 = unbindEvents.current) === null || _unbindEvents$current2 === void 0 ? void 0 : _unbindEvents$current2.call(unbindEvents);
118
173
  unbindEvents.current = null;
119
174
  };
120
175
  var updatedLeftSidebarState = {};
121
- var onMouseUp = function onMouseUp(event) {
176
+ var onFinishResizing = function onFinishResizing() {
122
177
  if (isLeftSidebarCollapsed) {
123
178
  return;
124
179
  }
@@ -126,14 +181,14 @@ var ResizeControl = function ResizeControl(_ref) {
126
181
 
127
182
  // If it is dragged to below the threshold,
128
183
  // collapse the navigation
129
- if (x.current < MIN_LEFT_SIDEBAR_DRAG_THRESHOLD) {
184
+ if (sidebarWidth.current < MIN_LEFT_SIDEBAR_DRAG_THRESHOLD) {
130
185
  document.documentElement.style.setProperty("--".concat(VAR_LEFT_SIDEBAR_WIDTH), "".concat(COLLAPSED_LEFT_SIDEBAR_WIDTH, "px"));
131
186
  collapseLeftSidebar(undefined, true);
132
187
  }
133
188
  // If it is dragged to position in between the
134
189
  // min threshold and default width
135
190
  // expand the nav to the default width
136
- else if (x.current > MIN_LEFT_SIDEBAR_DRAG_THRESHOLD && x.current < DEFAULT_LEFT_SIDEBAR_WIDTH) {
191
+ else if (sidebarWidth.current > MIN_LEFT_SIDEBAR_DRAG_THRESHOLD && sidebarWidth.current < DEFAULT_LEFT_SIDEBAR_WIDTH) {
137
192
  var _objectSpread2;
138
193
  document.documentElement.style.setProperty("--".concat(VAR_LEFT_SIDEBAR_WIDTH), "".concat(DEFAULT_LEFT_SIDEBAR_WIDTH, "px"));
139
194
  updatedLeftSidebarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, (_objectSpread2 = {
@@ -145,11 +200,11 @@ var ResizeControl = function ResizeControl(_ref) {
145
200
  // otherwise resize it to the desired width
146
201
  updatedLeftSidebarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, (_objectSpread3 = {
147
202
  isResizing: false
148
- }, _defineProperty(_objectSpread3, VAR_LEFT_SIDEBAR_WIDTH, x.current), _defineProperty(_objectSpread3, "lastLeftSidebarWidth", x.current), _objectSpread3));
203
+ }, _defineProperty(_objectSpread3, VAR_LEFT_SIDEBAR_WIDTH, sidebarWidth.current), _defineProperty(_objectSpread3, "lastLeftSidebarWidth", sidebarWidth.current), _objectSpread3));
149
204
  setLeftSidebarState(updatedLeftSidebarState);
150
205
  }
151
206
  requestAnimationFrame(function () {
152
- onMouseMove.cancel();
207
+ onUpdateResize.cancel();
153
208
  setIsGrabAreaFocused(false);
154
209
  onResizeEnd && onResizeEnd(updatedLeftSidebarState);
155
210
  cleanupAfterResize();
@@ -1,8 +1,9 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { bind } from 'bind-event-listener';
3
4
  import noop from '@atlaskit/ds-lib/noop';
4
5
  import { isReducedMotion } from '@atlaskit/motion';
5
- import { COLLAPSED_LEFT_SIDEBAR_WIDTH, DEFAULT_LEFT_SIDEBAR_WIDTH, GRAB_AREA_SELECTOR, IS_SIDEBAR_COLLAPSING } from '../common/constants';
6
+ import { COLLAPSED_LEFT_SIDEBAR_WIDTH, DEFAULT_LEFT_SIDEBAR_WIDTH, IS_SIDEBAR_COLLAPSING } from '../common/constants';
6
7
  import { getPageLayoutSlotCSSSelector } from '../common/utils';
7
8
  import { SidebarResizeContext } from './sidebar-resize-context';
8
9
  var handleDataAttributesAndCb = function handleDataAttributesAndCb() {
@@ -12,7 +13,7 @@ var handleDataAttributesAndCb = function handleDataAttributesAndCb() {
12
13
  document.documentElement.removeAttribute(IS_SIDEBAR_COLLAPSING);
13
14
  callback(leftSidebarState);
14
15
  };
15
-
16
+ var leftSidebarSelector = getPageLayoutSlotCSSSelector('left-sidebar');
16
17
  // eslint-disable-next-line @repo/internal/react/require-jsdoc
17
18
  export var SidebarResizeController = function SidebarResizeController(_ref) {
18
19
  var children = _ref.children,
@@ -31,34 +32,35 @@ export var SidebarResizeController = function SidebarResizeController(_ref) {
31
32
  leftSidebarState = _useState2[0],
32
33
  setLeftSidebarState = _useState2[1];
33
34
  var isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed;
34
- var leftSidebarSelector = getPageLayoutSlotCSSSelector('left-sidebar');
35
- var transitionEventHandler = useCallback(function (event) {
36
- if (event.propertyName === 'width' && event.target && event.target.matches(leftSidebarSelector)) {
37
- var $leftSidebarResizeController = document.querySelector("[".concat(GRAB_AREA_SELECTOR, "]"));
38
- var isCollapsed = !!$leftSidebarResizeController && $leftSidebarResizeController.hasAttribute('disabled');
39
- handleDataAttributesAndCb(isCollapsed ? onCollapse : onExpand, isCollapsed, leftSidebarState);
40
35
 
41
- // Make sure multiple event handlers do not get attached
42
- // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
43
- document.querySelector(leftSidebarSelector).removeEventListener('transitionend', transitionEventHandler);
44
- }
45
- // eslint-disable-next-line react-hooks/exhaustive-deps
46
- }, []);
36
+ // We put the latest callbacks into a ref so we can always have the latest
37
+ // functions in our transitionend listeners
38
+ var stableRef = useRef({
39
+ onExpand: onExpand,
40
+ onCollapse: onCollapse
41
+ });
47
42
  useEffect(function () {
48
- var $leftSidebar = document.querySelector(leftSidebarSelector);
49
- if ($leftSidebar && !isReducedMotion()) {
50
- // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
51
- $leftSidebar.addEventListener('transitionend', transitionEventHandler);
52
- }
53
- }, [isLeftSidebarCollapsed, leftSidebarSelector, leftSidebarState, onCollapse, onExpand, transitionEventHandler]);
43
+ stableRef.current = {
44
+ onExpand: onExpand,
45
+ onCollapse: onCollapse
46
+ };
47
+ });
48
+ var transition = useRef(null);
54
49
  var expandLeftSidebar = useCallback(function () {
50
+ var _transition$current, _transition$current2;
55
51
  var lastLeftSidebarWidth = leftSidebarState.lastLeftSidebarWidth,
56
52
  isResizing = leftSidebarState.isResizing,
57
53
  flyoutLockCount = leftSidebarState.flyoutLockCount,
58
- isFixed = leftSidebarState.isFixed;
59
- if (isResizing) {
54
+ isFixed = leftSidebarState.isFixed,
55
+ isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed;
56
+ if (isResizing || !isLeftSidebarCollapsed ||
57
+ // already expanding
58
+ ((_transition$current = transition.current) === null || _transition$current === void 0 ? void 0 : _transition$current.action) === 'expand') {
60
59
  return;
61
60
  }
61
+
62
+ // flush existing transition
63
+ (_transition$current2 = transition.current) === null || _transition$current2 === void 0 ? void 0 : _transition$current2.complete();
62
64
  var width = Math.max(lastLeftSidebarWidth, DEFAULT_LEFT_SIDEBAR_WIDTH);
63
65
  var updatedLeftSidebarState = {
64
66
  isLeftSidebarCollapsed: false,
@@ -70,22 +72,55 @@ export var SidebarResizeController = function SidebarResizeController(_ref) {
70
72
  isFixed: isFixed
71
73
  };
72
74
  setLeftSidebarState(updatedLeftSidebarState);
73
-
75
+ function finish() {
76
+ handleDataAttributesAndCb(stableRef.current.onExpand, false,
77
+ // isCollapsed
78
+ updatedLeftSidebarState);
79
+ }
80
+ var sidebar = document.querySelector(leftSidebarSelector);
74
81
  // onTransitionEnd isn't triggered when a user prefers reduced motion
75
- if (isReducedMotion()) {
76
- handleDataAttributesAndCb(onExpand, false, updatedLeftSidebarState);
82
+ if (isReducedMotion() || !sidebar) {
83
+ finish();
84
+ return;
77
85
  }
78
- }, [leftSidebarState, onExpand]);
86
+ var unbindEvent = bind(sidebar, {
87
+ type: 'transitionend',
88
+ listener: function listener(event) {
89
+ if (event.target === sidebar && event.propertyName === 'width') {
90
+ var _transition$current3;
91
+ (_transition$current3 = transition.current) === null || _transition$current3 === void 0 ? void 0 : _transition$current3.complete();
92
+ }
93
+ }
94
+ });
95
+ var value = {
96
+ action: 'expand',
97
+ complete: function complete() {
98
+ value.abort();
99
+ finish();
100
+ },
101
+ abort: function abort() {
102
+ unbindEvent();
103
+ transition.current = null;
104
+ }
105
+ };
106
+ transition.current = value;
107
+ }, [leftSidebarState]);
79
108
  var collapseLeftSidebar = useCallback(function (event, collapseWithoutTransition) {
109
+ var _transition$current4, _transition$current5;
80
110
  var leftSidebarWidth = leftSidebarState.leftSidebarWidth,
81
111
  isResizing = leftSidebarState.isResizing,
82
112
  flyoutLockCount = leftSidebarState.flyoutLockCount,
83
113
  isFixed = leftSidebarState.isFixed,
84
114
  isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed;
85
- if (isResizing || isLeftSidebarCollapsed) {
115
+ if (isResizing || isLeftSidebarCollapsed ||
116
+ // already collapsing
117
+ ((_transition$current4 = transition.current) === null || _transition$current4 === void 0 ? void 0 : _transition$current4.action) === 'collapse') {
86
118
  return;
87
119
  }
88
120
 
121
+ // flush existing transition
122
+ (_transition$current5 = transition.current) === null || _transition$current5 === void 0 ? void 0 : _transition$current5.complete();
123
+
89
124
  // data-attribute is used as a CSS selector to sync the hiding/showing
90
125
  // of the nav contents with expand/collapse animation
91
126
  document.documentElement.setAttribute(IS_SIDEBAR_COLLAPSING, 'true');
@@ -99,12 +134,46 @@ export var SidebarResizeController = function SidebarResizeController(_ref) {
99
134
  isFixed: isFixed
100
135
  };
101
136
  setLeftSidebarState(updatedLeftSidebarState);
137
+ function finish() {
138
+ handleDataAttributesAndCb(stableRef.current.onCollapse, true, updatedLeftSidebarState);
139
+ }
140
+ var sidebar = document.querySelector(leftSidebarSelector);
102
141
 
103
142
  // onTransitionEnd isn't triggered when a user prefers reduced motion
104
- if (collapseWithoutTransition || isReducedMotion()) {
105
- handleDataAttributesAndCb(onCollapse, true, updatedLeftSidebarState);
143
+ if (collapseWithoutTransition || isReducedMotion() || !sidebar) {
144
+ finish();
145
+ return;
106
146
  }
107
- }, [leftSidebarState, onCollapse]);
147
+ var unbindEvent = bind(sidebar, {
148
+ type: 'transitionend',
149
+ listener: function listener(event) {
150
+ if (sidebar === event.target && event.propertyName === 'width') {
151
+ var _transition$current6;
152
+ (_transition$current6 = transition.current) === null || _transition$current6 === void 0 ? void 0 : _transition$current6.complete();
153
+ }
154
+ }
155
+ });
156
+ var value = {
157
+ action: 'collapse',
158
+ complete: function complete() {
159
+ value.abort();
160
+ finish();
161
+ },
162
+ abort: function abort() {
163
+ unbindEvent();
164
+ transition.current = null;
165
+ }
166
+ };
167
+ transition.current = value;
168
+ }, [leftSidebarState]);
169
+
170
+ // Make sure we finish any lingering transitions when unmounting
171
+ useEffect(function mount() {
172
+ return function unmount() {
173
+ var _transition$current7;
174
+ (_transition$current7 = transition.current) === null || _transition$current7 === void 0 ? void 0 : _transition$current7.abort();
175
+ };
176
+ }, []);
108
177
  var context = useMemo(function () {
109
178
  return {
110
179
  isLeftSidebarCollapsed: isLeftSidebarCollapsed,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/page-layout",
3
- "version": "1.3.10",
3
+ "version": "1.5.0",
4
4
  "sideEffects": false
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/page-layout",
3
- "version": "1.3.10",
3
+ "version": "1.5.0",
4
4
  "description": "A collection of components which let you compose an application's page layout.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -28,7 +28,7 @@
28
28
  "@atlaskit/ds-lib": "^2.1.0",
29
29
  "@atlaskit/icon": "^21.11.0",
30
30
  "@atlaskit/motion": "^1.3.0",
31
- "@atlaskit/theme": "^12.3.0",
31
+ "@atlaskit/theme": "^12.4.0",
32
32
  "@atlaskit/tokens": "^1.2.0",
33
33
  "@babel/runtime": "^7.0.0",
34
34
  "@emotion/react": "^11.7.1",
@@ -40,17 +40,17 @@
40
40
  "react-dom": "^16.8.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@atlaskit/atlassian-navigation": "^2.3.0",
43
+ "@atlaskit/atlassian-navigation": "^2.4.0",
44
44
  "@atlaskit/atlassian-notifications": "^0.3.0",
45
- "@atlaskit/button": "^16.5.0",
45
+ "@atlaskit/button": "^16.6.0",
46
46
  "@atlaskit/docs": "*",
47
47
  "@atlaskit/drawer": "^7.4.0",
48
48
  "@atlaskit/icon": "*",
49
- "@atlaskit/logo": "^13.11.0",
49
+ "@atlaskit/logo": "^13.13.0",
50
50
  "@atlaskit/menu": "^1.5.0",
51
51
  "@atlaskit/notification-indicator": "^9.0.0",
52
52
  "@atlaskit/notification-log-client": "^6.0.0",
53
- "@atlaskit/onboarding": "^10.6.0",
53
+ "@atlaskit/onboarding": "^10.7.0",
54
54
  "@atlaskit/popup": "^1.5.0",
55
55
  "@atlaskit/section-message": "^6.3.0",
56
56
  "@atlaskit/side-navigation": "^1.6.0",