UNPKG

@atlaskit/page-layout

Version:

A collection of components which let you compose an application's page layout.

359 lines (347 loc) 19.2 kB
"use strict"; 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;