@atlaskit/page-layout
Version:
A collection of components which let you compose an application's page layout.
359 lines (347 loc) • 19.2 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _react = require("react");
var _react2 = require("@emotion/react");
var _bindEventListener = require("bind-event-listener");
var _rafSchd = _interopRequireDefault(require("raf-schd"));
var _responsive = require("@atlaskit/primitives/responsive");
var _constants = require("../../common/constants");
var _utils = require("../../common/utils");
var _sidebarResizeContext = require("../../controllers/sidebar-resize-context");
var _grabArea = _interopRequireDefault(require("./grab-area"));
var _resizeButton = _interopRequireDefault(require("./resize-button"));
var _shadow = _interopRequireDefault(require("./shadow"));
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /**
* @jsxRuntime classic
* @jsx jsx
*/ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles, @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
/* import useUpdateCssVar from '../../controllers/use-update-css-vars'; */
var cssSelector = (0, _defineProperty2.default)({}, _constants.RESIZE_CONTROL_SELECTOR, true);
var resizeControlStyles = (0, _react2.css)({
position: 'absolute',
insetBlockEnd: 0,
insetBlockStart: 0,
insetInlineStart: '100%',
outline: 'none'
});
var showResizeButtonStyles = (0, _react2.css)({
'--ds--resize-button--opacity': 1
});
// @ts-expect-error adding `!important` to style rules is currently a type error
var globalResizingStyles = (0, _react2.css)({
// eslint-disable-next-line @atlaskit/design-system/no-nested-styles, @atlaskit/ui-styling-standard/no-nested-selectors -- Ignored via go/DSP-18766
'*': {
// Setting the cursor to be `ew-resize` on all elements so that even if the user
// pointer slips off the resize handle, the cursor will still be the resize cursor
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-important-styles -- Ignored via go/DSP-18766
cursor: 'ew-resize !important',
// Blocking selection while resizing
// Notes:
// - This prevents a user selection being caused by resizing
// - Safari + Firefox → all good
// - Chrome → This will undo the current selection while resizing (not ideal)
// - The current selection will resume after resizing
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-important-styles -- Ignored via go/DSP-18766
userSelect: 'none !important'
},
// eslint-disable-next-line @atlaskit/design-system/no-nested-styles, @atlaskit/ui-styling-standard/no-nested-selectors -- Ignored via go/DSP-18766
iframe: {
// Disabling pointer events on iframes when resizing
// as iframes will swallower user events when the user is over them
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-important-styles -- Ignored via go/DSP-18766
pointerEvents: 'none !important'
}
// Note: We _could_ also disable `pointer-events` on all elements during resizing.
// However, to minimize risk we are just disabling `pointer-events` on iframes
// as that change is actually needed to fix resizing with iframes
});
var ResizeControl = function ResizeControl(_ref) {
var testId = _ref.testId,
overrides = _ref.overrides,
_ref$resizeButtonLabe = _ref.resizeButtonLabel,
resizeButtonLabel = _ref$resizeButtonLabe === void 0 ? 'Current project sidebar' : _ref$resizeButtonLabe,
_ref$valueTextLabel = _ref.valueTextLabel,
valueTextLabel = _ref$valueTextLabel === void 0 ? 'Width' : _ref$valueTextLabel,
_ref$resizeGrabAreaLa = _ref.resizeGrabAreaLabel,
resizeGrabAreaLabel = _ref$resizeGrabAreaLa === void 0 ? 'Resize Current Project Sidebar' : _ref$resizeGrabAreaLa,
onResizeStart = _ref.onResizeStart,
onResizeEnd = _ref.onResizeEnd;
var _useContext = (0, _react.useContext)(_sidebarResizeContext.SidebarResizeContext),
toggleLeftSidebar = _useContext.toggleLeftSidebar,
collapseLeftSidebar = _useContext.collapseLeftSidebar,
leftSidebarState = _useContext.leftSidebarState,
setLeftSidebarState = _useContext.setLeftSidebarState;
var isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed;
var sidebarWidth = (0, _react.useRef)(leftSidebarState[_constants.VAR_LEFT_SIDEBAR_WIDTH]);
// Distance of mouse from left sidebar onMouseDown
var offset = (0, _react.useRef)(0);
var keyboardEventTimeout = (0, _react.useRef)();
var _useState = (0, _react.useState)(false),
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
isGrabAreaFocused = _useState2[0],
setIsGrabAreaFocused = _useState2[1];
var unbindEvents = (0, _react.useRef)(null);
var mobileMediaQuery = (0, _responsive.UNSAFE_useMediaQuery)('below.sm');
// Used in some cases to ensure function references don't have to change
// TODO: more functions could use `stableSidebarState` rather than `leftSidebarState`
var stableSidebarState = (0, _react.useRef)(leftSidebarState);
(0, _react.useEffect)(function () {
stableSidebarState.current = leftSidebarState;
}, [leftSidebarState]);
var toggleSideBar = (0, _react.useCallback)(function (event) {
// don't cascade down to the LeftSidebarOuter
event === null || event === void 0 || event.stopPropagation();
toggleLeftSidebar();
}, [toggleLeftSidebar]);
var onMouseDown = function onMouseDown(event) {
if (isLeftSidebarCollapsed) {
return;
}
// Only allow left (primary) clicks to trigger resize as we've received
// bug reports about right click unexpectedly beginning a resize.
if (event.button !== 0) {
return;
}
// It is possible for a mousedown to fire during a resize
// Example: the user presses another pointer button while dragging
if (leftSidebarState.isResizing) {
// the resize will be cancelled by our global event listeners
return;
}
offset.current = event.clientX - leftSidebarState[_constants.VAR_LEFT_SIDEBAR_WIDTH] - (0, _utils.getLeftPanelWidth)();
unbindEvents.current = (0, _bindEventListener.bindAll)(window, [{
type: 'mousemove',
listener: function listener(event) {
onUpdateResize({
clientX: event.clientX
});
}
}, {
type: 'mouseup',
listener: onFinishResizing
}, {
type: 'mousedown',
// this mousedown event listener is being added in the bubble phase
// on a higher event target than the resize handle.
// This means that the original mousedown event that triggers a resize
// can hit this mousedown handler. To get around that, we only call
// `onFinishResizing` after an animation frame so we don't pick up the original event
// Alternatives:
// 1. Add the window 'mousedown' event listener in the capture phase
// 👎 A 'mousedown' during a resize would trigger a new resize to start
// 2. Do 1. and call `event.preventDefault()`, then check for `event.defaultPrevented` inside
// the grab handle `onMouseDown`
// 👎 Not ideal to cancel events if we don't have to
listener: function () {
var hasFramePassed = false;
requestAnimationFrame(function () {
hasFramePassed = true;
});
return function listener() {
if (hasFramePassed) {
onFinishResizing();
}
};
}()
}, {
type: 'visibilitychange',
listener: onFinishResizing
},
// A 'click' event should never be hit as the 'mouseup' will come first and cause
// these event listeners to be unbound. I just added 'click' for extreme safety (paranoia)
{
type: 'click',
listener: onFinishResizing
}, {
type: 'keydown',
listener: function listener(event) {
// Can cancel resizing by pressing "Escape"
// Will return sidebar to the same size it was before the resizing started
if (event.key === 'Escape') {
sidebarWidth.current = Math.max(leftSidebarState.lastLeftSidebarWidth, _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH);
document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(sidebarWidth.current, "px"));
onFinishResizing();
}
}
}]);
document.documentElement.setAttribute(_constants.IS_SIDEBAR_DRAGGING, 'true');
var newLeftbarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, {
isResizing: true
});
setLeftSidebarState(newLeftbarState);
onResizeStart && onResizeStart(newLeftbarState);
};
var onResizeOffLeftOfScreen = function onResizeOffLeftOfScreen() {
var _unbindEvents$current;
onUpdateResize.cancel();
(_unbindEvents$current = unbindEvents.current) === null || _unbindEvents$current === void 0 || _unbindEvents$current.call(unbindEvents);
unbindEvents.current = null;
document.documentElement.removeAttribute(_constants.IS_SIDEBAR_DRAGGING);
offset.current = 0;
collapseLeftSidebar(undefined, true);
};
// It is important that `onUpdateResize` is a stable function reference, so that:
// 1. we ensure we are correctly throttling with `requestAnimationFrame`
// 2. that a `onUpdateResize` will cancel the one and only pending frame
// To help ensure `onUpdateResize` is stable, we are putting the last state into a ref
var _useState3 = (0, _react.useState)(function () {
return (0, _rafSchd.default)(function (_ref2) {
var clientX = _ref2.clientX;
// Allow the sidebar to be 50% of the available page width
var maxWidth = Math.round(window.innerWidth / 2);
var leftPanelWidth = (0, _utils.getLeftPanelWidth)();
var leftSidebarWidth = stableSidebarState.current.leftSidebarWidth;
var hasResizedOffLeftOfScreen = clientX < 0;
if (hasResizedOffLeftOfScreen) {
onResizeOffLeftOfScreen();
return;
}
var delta = Math.max(Math.min(clientX - leftSidebarWidth - leftPanelWidth, maxWidth - leftSidebarWidth - leftPanelWidth), _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH - leftSidebarWidth - leftPanelWidth);
sidebarWidth.current = Math.max(leftSidebarWidth + delta - offset.current, _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH);
document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(sidebarWidth.current, "px"));
});
}),
_useState4 = (0, _slicedToArray2.default)(_useState3, 1),
onUpdateResize = _useState4[0];
var onFinishResizing = function onFinishResizing() {
var _unbindEvents$current2;
if (isLeftSidebarCollapsed) {
return;
}
document.documentElement.removeAttribute(_constants.IS_SIDEBAR_DRAGGING);
// TODO: the control flow is pretty strange as the first codepath which calls `collapseLeftSidebar()`
// does not return an updated state snapshot.
var updatedLeftSidebarState = null;
// If it is dragged to below the threshold,
// collapse the navigation
if (sidebarWidth.current < _constants.MIN_LEFT_SIDEBAR_DRAG_THRESHOLD) {
// TODO: for this codepath, `onCollapse` occurs before `onResizeEnd` which seems wrong
document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(_constants.COLLAPSED_LEFT_SIDEBAR_WIDTH, "px"));
collapseLeftSidebar(undefined, true);
}
// If it is dragged to position in between the
// min threshold and default width
// expand the nav to the default width
else if (sidebarWidth.current > _constants.MIN_LEFT_SIDEBAR_DRAG_THRESHOLD && sidebarWidth.current < _constants.DEFAULT_LEFT_SIDEBAR_WIDTH) {
document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(_constants.DEFAULT_LEFT_SIDEBAR_WIDTH, "px"));
updatedLeftSidebarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, (0, _defineProperty2.default)((0, _defineProperty2.default)({
isResizing: false
}, _constants.VAR_LEFT_SIDEBAR_WIDTH, _constants.DEFAULT_LEFT_SIDEBAR_WIDTH), "lastLeftSidebarWidth", _constants.DEFAULT_LEFT_SIDEBAR_WIDTH));
setLeftSidebarState(updatedLeftSidebarState);
} else {
// otherwise resize it to the desired width
updatedLeftSidebarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, (0, _defineProperty2.default)((0, _defineProperty2.default)({
isResizing: false
}, _constants.VAR_LEFT_SIDEBAR_WIDTH, sidebarWidth.current), "lastLeftSidebarWidth", sidebarWidth.current));
setLeftSidebarState(updatedLeftSidebarState);
}
(_unbindEvents$current2 = unbindEvents.current) === null || _unbindEvents$current2 === void 0 || _unbindEvents$current2.call(unbindEvents);
unbindEvents.current = null;
onUpdateResize.cancel();
sidebarWidth.current = 0;
offset.current = 0;
// TODO: no idea why this is in an animation frame
requestAnimationFrame(function () {
setIsGrabAreaFocused(false);
// Note: the `collapseSidebar` codepath does not return state, so we need to pull it from the ref
onResizeEnd === null || onResizeEnd === void 0 || onResizeEnd(updatedLeftSidebarState !== null && updatedLeftSidebarState !== void 0 ? updatedLeftSidebarState : stableSidebarState.current);
});
};
var onKeyDown = function onKeyDown(event) {
if (isLeftSidebarCollapsed || !isGrabAreaFocused) {
return false;
}
var key = event.key;
var isLeftOrTopArrow = key === 'ArrowLeft' || key === 'ArrowUp' || key === 'Left' || key === 'Up';
var isRightOrBottomArrow = key === 'ArrowRight' || key === 'ArrowDown' || key === 'Right' || key === 'Down';
var isSpaceOrEnter = key === 'Enter' || key === 'Spacebar' || key === ' ';
if (isSpaceOrEnter) {
toggleSideBar(event);
event.preventDefault();
}
if (isLeftOrTopArrow || isRightOrBottomArrow) {
event.preventDefault(); // prevent content scroll
onResizeStart && onResizeStart(leftSidebarState);
var step = 10;
var stepValue = isLeftOrTopArrow ? -step : step;
var leftSidebarWidth = leftSidebarState.leftSidebarWidth;
var maxWidth = Math.round(window.innerWidth / 2) - (0, _utils.getLeftPanelWidth)();
var hasModifierKey = event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
var width = leftSidebarWidth + stepValue;
if (width <= _constants.DEFAULT_LEFT_SIDEBAR_WIDTH) {
width = _constants.DEFAULT_LEFT_SIDEBAR_WIDTH;
document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(_constants.DEFAULT_LEFT_SIDEBAR_WIDTH - 20, "px"));
} else if (width > maxWidth) {
width = maxWidth;
document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(maxWidth + 20, "px"));
} else if (hasModifierKey) {
width = isRightOrBottomArrow ? maxWidth : _constants.DEFAULT_LEFT_SIDEBAR_WIDTH;
}
// Nesting the setTimeout within requestAnimationFrame helps
// the browser schedule the setTimeout call in an efficient manner
requestAnimationFrame(function () {
keyboardEventTimeout.current = window.setTimeout(function () {
keyboardEventTimeout.current && clearTimeout(keyboardEventTimeout.current);
document.documentElement.style.setProperty("--".concat(_constants.VAR_LEFT_SIDEBAR_WIDTH), "".concat(width, "px"));
var updatedLeftSidebarState = _objectSpread(_objectSpread({}, leftSidebarState), {}, (0, _defineProperty2.default)((0, _defineProperty2.default)({}, _constants.VAR_LEFT_SIDEBAR_WIDTH, width), "lastLeftSidebarWidth", width));
setLeftSidebarState(updatedLeftSidebarState);
onResizeEnd && onResizeEnd(updatedLeftSidebarState);
}, 50);
});
}
};
var onFocus = (0, _react.useCallback)(function () {
setIsGrabAreaFocused(true);
}, []);
var onBlur = (0, _react.useCallback)(function () {
setIsGrabAreaFocused(false);
}, []);
var resizeButton = _objectSpread({
render: function render(Component, props) {
return (0, _react2.jsx)(Component, props);
}
}, overrides && overrides.ResizeButton);
// This width is calculated once only on mount.
// This means resizing the window will cause this value to be incorrect for screen reader users,
// however this comes with a substantial performance gain and so is considered acceptable.
var maxAriaWidth = (0, _react.useMemo)(function () {
var innerWidth = typeof window === 'undefined' ? 0 : window.innerWidth;
return Math.round(innerWidth / 2) - (0, _utils.getLeftPanelWidth)();
}, []);
var leftSidebarPercentageExpanded = (0, _utils.getLeftSidebarPercentage)(leftSidebarState.leftSidebarWidth, maxAriaWidth);
return (0, _react2.jsx)(_react.Fragment, null, (0, _react2.jsx)("div", (0, _extends2.default)({}, cssSelector, {
css: [resizeControlStyles, (isGrabAreaFocused || isLeftSidebarCollapsed) && showResizeButtonStyles]
}), (0, _react2.jsx)(_shadow.default, {
testId: testId && "".concat(testId, "-shadow")
}),
// Only show the GrabArea if we're not on the mobile viewport
!(mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches) && (0, _react2.jsx)(_grabArea.default, {
isDisabled: isLeftSidebarCollapsed,
isLeftSidebarCollapsed: isLeftSidebarCollapsed,
label: resizeGrabAreaLabel,
valueTextLabel: valueTextLabel,
leftSidebarPercentageExpanded: leftSidebarPercentageExpanded,
onBlur: onBlur,
onFocus: onFocus,
onKeyDown: onKeyDown,
onMouseDown: onMouseDown,
testId: testId && "".concat(testId, "-grab-area")
}), resizeButton.render(_resizeButton.default, {
isLeftSidebarCollapsed: mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches ? !leftSidebarState.isFlyoutOpen : isLeftSidebarCollapsed,
label: resizeButtonLabel,
onClick: toggleSideBar,
testId: testId && "".concat(testId, "-resize-button")
})), leftSidebarState.isResizing ? (0, _react2.jsx)(_react2.Global, {
styles: globalResizingStyles
}) : null);
};
// eslint-disable-next-line @repo/internal/react/require-jsdoc
var _default = exports.default = ResizeControl;