@atlaskit/page-layout 1.3.10 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @atlaskit/page-layout
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`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
8
+
9
+ [ux] **new**: if a user presses the `"Escape"` key while the sidebar is being resized, then the resizing operation will end
10
+
3
11
  ## 1.3.10
4
12
 
5
13
  ### 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
  *
@@ -42,12 +42,29 @@ var SidebarResizeController = function SidebarResizeController(_ref) {
42
42
  setLeftSidebarState = _useState2[1];
43
43
  var isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed;
44
44
  var leftSidebarSelector = (0, _utils.getPageLayoutSlotCSSSelector)('left-sidebar');
45
+
46
+ /**
47
+ * Bug: this function will cause `onExpand` / `onCollapse` when any
48
+ * `width` transition occurs (eg when cancelling a resizing)
49
+ * This
50
+ */
45
51
  var transitionEventHandler = (0, _react.useCallback)(function (event) {
46
52
  if (event.propertyName === 'width' && event.target && event.target.matches(leftSidebarSelector)) {
47
53
  var $leftSidebarResizeController = document.querySelector("[".concat(_constants.GRAB_AREA_SELECTOR, "]"));
48
54
  var isCollapsed = !!$leftSidebarResizeController && $leftSidebarResizeController.hasAttribute('disabled');
49
- handleDataAttributesAndCb(isCollapsed ? onCollapse : onExpand, isCollapsed, leftSidebarState);
55
+ handleDataAttributesAndCb(
56
+ /**
57
+ * Bug: `onCollapse` and `onExpand` are stale after the first render
58
+ */
59
+ isCollapsed ? onCollapse : onExpand, isCollapsed,
60
+ /**
61
+ * Bug: `leftSidebarState` is stale after the first render
62
+ */
63
+ leftSidebarState);
50
64
 
65
+ /**
66
+ * TODO: this appears smelly. Let's do better
67
+ */
51
68
  // Make sure multiple event handlers do not get attached
52
69
  // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
53
70
  document.querySelector(leftSidebarSelector).removeEventListener('transitionend', transitionEventHandler);
@@ -57,6 +74,11 @@ var SidebarResizeController = function SidebarResizeController(_ref) {
57
74
  (0, _react.useEffect)(function () {
58
75
  var $leftSidebar = document.querySelector(leftSidebarSelector);
59
76
  if ($leftSidebar && !(0, _motion.isReducedMotion)()) {
77
+ /**
78
+ * Note: This pattern relies on `transitionEventHandler` keeping a stable
79
+ * reference to continually adding event listeners.
80
+ * I think there should be a better way
81
+ */
60
82
  // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
61
83
  $leftSidebar.addEventListener('transitionend', transitionEventHandler);
62
84
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/page-layout",
3
- "version": "1.3.10",
3
+ "version": "1.4.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();
@@ -28,12 +28,29 @@ export const SidebarResizeController = ({
28
28
  isLeftSidebarCollapsed
29
29
  } = leftSidebarState;
30
30
  const leftSidebarSelector = getPageLayoutSlotCSSSelector('left-sidebar');
31
+
32
+ /**
33
+ * Bug: this function will cause `onExpand` / `onCollapse` when any
34
+ * `width` transition occurs (eg when cancelling a resizing)
35
+ * This
36
+ */
31
37
  const transitionEventHandler = useCallback(event => {
32
38
  if (event.propertyName === 'width' && event.target && event.target.matches(leftSidebarSelector)) {
33
39
  const $leftSidebarResizeController = document.querySelector(`[${GRAB_AREA_SELECTOR}]`);
34
40
  const isCollapsed = !!$leftSidebarResizeController && $leftSidebarResizeController.hasAttribute('disabled');
35
- handleDataAttributesAndCb(isCollapsed ? onCollapse : onExpand, isCollapsed, leftSidebarState);
41
+ handleDataAttributesAndCb(
42
+ /**
43
+ * Bug: `onCollapse` and `onExpand` are stale after the first render
44
+ */
45
+ isCollapsed ? onCollapse : onExpand, isCollapsed,
46
+ /**
47
+ * Bug: `leftSidebarState` is stale after the first render
48
+ */
49
+ leftSidebarState);
36
50
 
51
+ /**
52
+ * TODO: this appears smelly. Let's do better
53
+ */
37
54
  // Make sure multiple event handlers do not get attached
38
55
  // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
39
56
  document.querySelector(leftSidebarSelector).removeEventListener('transitionend', transitionEventHandler);
@@ -43,6 +60,11 @@ export const SidebarResizeController = ({
43
60
  useEffect(() => {
44
61
  const $leftSidebar = document.querySelector(leftSidebarSelector);
45
62
  if ($leftSidebar && !isReducedMotion()) {
63
+ /**
64
+ * Note: This pattern relies on `transitionEventHandler` keeping a stable
65
+ * reference to continually adding event listeners.
66
+ * I think there should be a better way
67
+ */
46
68
  // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
47
69
  $leftSidebar.addEventListener('transitionend', transitionEventHandler);
48
70
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/page-layout",
3
- "version": "1.3.10",
3
+ "version": "1.4.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();
@@ -32,12 +32,29 @@ export var SidebarResizeController = function SidebarResizeController(_ref) {
32
32
  setLeftSidebarState = _useState2[1];
33
33
  var isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed;
34
34
  var leftSidebarSelector = getPageLayoutSlotCSSSelector('left-sidebar');
35
+
36
+ /**
37
+ * Bug: this function will cause `onExpand` / `onCollapse` when any
38
+ * `width` transition occurs (eg when cancelling a resizing)
39
+ * This
40
+ */
35
41
  var transitionEventHandler = useCallback(function (event) {
36
42
  if (event.propertyName === 'width' && event.target && event.target.matches(leftSidebarSelector)) {
37
43
  var $leftSidebarResizeController = document.querySelector("[".concat(GRAB_AREA_SELECTOR, "]"));
38
44
  var isCollapsed = !!$leftSidebarResizeController && $leftSidebarResizeController.hasAttribute('disabled');
39
- handleDataAttributesAndCb(isCollapsed ? onCollapse : onExpand, isCollapsed, leftSidebarState);
45
+ handleDataAttributesAndCb(
46
+ /**
47
+ * Bug: `onCollapse` and `onExpand` are stale after the first render
48
+ */
49
+ isCollapsed ? onCollapse : onExpand, isCollapsed,
50
+ /**
51
+ * Bug: `leftSidebarState` is stale after the first render
52
+ */
53
+ leftSidebarState);
40
54
 
55
+ /**
56
+ * TODO: this appears smelly. Let's do better
57
+ */
41
58
  // Make sure multiple event handlers do not get attached
42
59
  // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
43
60
  document.querySelector(leftSidebarSelector).removeEventListener('transitionend', transitionEventHandler);
@@ -47,6 +64,11 @@ export var SidebarResizeController = function SidebarResizeController(_ref) {
47
64
  useEffect(function () {
48
65
  var $leftSidebar = document.querySelector(leftSidebarSelector);
49
66
  if ($leftSidebar && !isReducedMotion()) {
67
+ /**
68
+ * Note: This pattern relies on `transitionEventHandler` keeping a stable
69
+ * reference to continually adding event listeners.
70
+ * I think there should be a better way
71
+ */
50
72
  // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
51
73
  $leftSidebar.addEventListener('transitionend', transitionEventHandler);
52
74
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@atlaskit/page-layout",
3
- "version": "1.3.10",
3
+ "version": "1.4.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.4.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",