UNPKG

@atlaskit/page-layout

Version:

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

336 lines (327 loc) 15.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = require("react"); var _react2 = require("@emotion/react"); var _useCloseOnEscapePress = _interopRequireDefault(require("@atlaskit/ds-lib/use-close-on-escape-press")); var _motion = require("@atlaskit/motion"); var _responsive = require("@atlaskit/primitives/responsive"); var _colors = require("@atlaskit/theme/colors"); var _constants = require("../../common/constants"); var _utils = require("../../common/utils"); var _controllers = require("../../controllers"); var _resizeControl = _interopRequireDefault(require("../resize-control")); var _leftSidebarInner = _interopRequireDefault(require("./internal/left-sidebar-inner")); var _leftSidebarOuter = _interopRequireDefault(require("./internal/left-sidebar-outer")); var _resizableChildrenWrapper = _interopRequireDefault(require("./internal/resizable-children-wrapper")); var _slotDimensions = _interopRequireDefault(require("./slot-dimensions")); 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; } /* eslint-disable @repo/internal/dom-events/no-unsafe-event-listeners */ /** * @jsxRuntime classic * @jsx jsx */ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 var openBackdropStyles = (0, _react2.css)({ width: '100%', height: '100%', position: 'absolute', background: "var(--ds-blanket, ".concat(_colors.N100A, ")"), opacity: 1 }); var hiddenBackdropStyles = (0, _react2.css)({ opacity: 0, // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766 transition: "opacity ".concat(_constants.TRANSITION_DURATION, "ms ").concat(_motion.easeOut, " 0s") }); /** * __Left sidebar__ * * Provides a slot for a left sidebar within the PageLayout. * * On smaller viewports, the left sidebar can no longer be expanded. Instead, expanding it will * put it into our "flyout mode" to lay overtop (which in desktop is explicitly a hover state). * This ensures the contents behind do not reflow oddly and allows for a better experience * resizing between mobile and desktop. * * - [Examples](https://atlassian.design/components/page-layout/examples) * - [Code](https://atlassian.design/components/page-layout/code) */ var LeftSidebar = function LeftSidebar(props) { var children = props.children, width = props.width, _props$isFixed = props.isFixed, isFixed = _props$isFixed === void 0 ? true : _props$isFixed, valueTextLabel = props.valueTextLabel, resizeButtonLabel = props.resizeButtonLabel, resizeGrabAreaLabel = props.resizeGrabAreaLabel, overrides = props.overrides, onResizeStart = props.onResizeStart, onResizeEnd = props.onResizeEnd, onFlyoutExpand = props.onFlyoutExpand, onFlyoutCollapse = props.onFlyoutCollapse, testId = props.testId, id = props.id, skipLinkTitle = props.skipLinkTitle, collapsedState = props.collapsedState; var flyoutTimerRef = (0, _react.useRef)(); var mouseOverEventRef = (0, _react.useRef)(); var leftSideBarRef = (0, _react.useRef)(null); var _useContext = (0, _react.useContext)(_controllers.SidebarResizeContext), leftSidebarState = _useContext.leftSidebarState, setLeftSidebarState = _useContext.setLeftSidebarState; var isFlyoutOpen = leftSidebarState.isFlyoutOpen, isResizing = leftSidebarState.isResizing, flyoutLockCount = leftSidebarState.flyoutLockCount, isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed, leftSidebarWidth = leftSidebarState.leftSidebarWidth, lastLeftSidebarWidth = leftSidebarState.lastLeftSidebarWidth, hasInit = leftSidebarState.hasInit; var isLocked = flyoutLockCount > 0; var isLockedRef = (0, _react.useRef)(isLocked); var mouseXRef = (0, _react.useRef)(0); var handlerRef = (0, _react.useRef)(null); (0, _react.useEffect)(function () { isLockedRef.current = isLocked; // I tried a one-time `mousemove` handler that gets attached // when the lock releases. This is not robust because // the mouse can stay still after release (e.g. using keyboard) // and the sidebar will erroneously stay open. // // The following solution is likely less performant but more robust. if (isLocked && !handlerRef.current) { handlerRef.current = function (event) { mouseXRef.current = event.clientX; }; document.addEventListener('mousemove', handlerRef.current); } if (!isLocked && handlerRef.current) { if (mouseXRef.current >= lastLeftSidebarWidth) { setLeftSidebarState(function (current) { return _objectSpread(_objectSpread({}, current), {}, { isFlyoutOpen: false }); }); } document.removeEventListener('mousemove', handlerRef.current); handlerRef.current = null; } return function () { if (handlerRef.current) { document.removeEventListener('mousemove', handlerRef.current); } }; }, [isLocked, lastLeftSidebarWidth, setLeftSidebarState]); var _width = Math.max(width || 0, _constants.DEFAULT_LEFT_SIDEBAR_WIDTH); var collapsedStateOverrideOpen = collapsedState === 'expanded'; var leftSidebarWidthOnMount; if (collapsedStateOverrideOpen) { leftSidebarWidthOnMount = (0, _utils.resolveDimension)(_constants.VAR_LEFT_SIDEBAR_FLYOUT, _width, !width); } else if (isLeftSidebarCollapsed || collapsedState === 'collapsed') { leftSidebarWidthOnMount = _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH; } else { leftSidebarWidthOnMount = (0, _utils.resolveDimension)(_constants.VAR_LEFT_SIDEBAR_WIDTH, _width, !width || !collapsedStateOverrideOpen && (0, _utils.getGridStateFromStorage)('isLeftSidebarCollapsed')); } // Update state from cache on mount (0, _react.useEffect)(function () { var cachedCollapsedState = !collapsedStateOverrideOpen && (collapsedState === 'collapsed' || (0, _utils.getGridStateFromStorage)('isLeftSidebarCollapsed') || false); var cachedGridState = (0, _utils.getGridStateFromStorage)('gridState') || {}; var leftSidebarWidth = !width && cachedGridState[_constants.VAR_LEFT_SIDEBAR_FLYOUT] ? Math.max(cachedGridState[_constants.VAR_LEFT_SIDEBAR_FLYOUT], _constants.DEFAULT_LEFT_SIDEBAR_WIDTH) : _width; var lastLeftSidebarWidth = !width && cachedGridState[_constants.VAR_LEFT_SIDEBAR_FLYOUT] ? Math.max(cachedGridState[_constants.VAR_LEFT_SIDEBAR_FLYOUT], _constants.DEFAULT_LEFT_SIDEBAR_WIDTH) : _width; if (cachedCollapsedState) { leftSidebarWidth = _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH; } setLeftSidebarState({ isFlyoutOpen: false, isResizing: isResizing, isLeftSidebarCollapsed: cachedCollapsedState, leftSidebarWidth: leftSidebarWidth, lastLeftSidebarWidth: lastLeftSidebarWidth, flyoutLockCount: 0, isFixed: isFixed, hasInit: true }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Every time other than mount, // update the local storage and css variables. var notFirstRun = (0, _react.useRef)(false); (0, _react.useEffect)(function () { if (notFirstRun.current) { (0, _controllers.publishGridState)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, _constants.VAR_LEFT_SIDEBAR_WIDTH, leftSidebarWidth || leftSidebarWidthOnMount), _constants.VAR_LEFT_SIDEBAR_FLYOUT, lastLeftSidebarWidth)); (0, _utils.mergeGridStateIntoStorage)('isLeftSidebarCollapsed', isLeftSidebarCollapsed); } if (!notFirstRun.current) { notFirstRun.current = true; } return function () { removeMouseOverListener(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLeftSidebarCollapsed, leftSidebarWidth, id]); var onMouseOver = function onMouseOver(event) { var isMouseOnResizeButton = event.target.matches("[".concat(_constants.RESIZE_BUTTON_SELECTOR, "]")) || event.target.matches("[".concat(_constants.RESIZE_BUTTON_SELECTOR, "] *")); if (isFlyoutOpen || isMouseOnResizeButton || !isLeftSidebarCollapsed) { return; } event.persist(); flyoutTimerRef.current && clearTimeout(flyoutTimerRef.current); if (!mouseOverEventRef.current) { mouseOverEventRef.current = function (event) { var leftSidebar = leftSideBarRef.current; if (leftSidebar === null || isLockedRef.current) { return; } if (!leftSidebar.contains(event.target)) { flyoutTimerRef.current && clearTimeout(flyoutTimerRef.current); onFlyoutCollapse && onFlyoutCollapse(); setLeftSidebarState(function (current) { return _objectSpread(_objectSpread({}, current), {}, { isFlyoutOpen: false }); }); removeMouseOverListener(); } }; } document.addEventListener('mouseover', mouseOverEventRef.current, { capture: true, passive: true }); flyoutTimerRef.current = setTimeout(function () { setLeftSidebarState(function (current) { return _objectSpread(_objectSpread({}, current), {}, { isFlyoutOpen: true }); }); onFlyoutExpand && onFlyoutExpand(); }, _constants.FLYOUT_DELAY); }; var removeMouseOverListener = function removeMouseOverListener() { mouseOverEventRef.current && document.removeEventListener('mouseover', mouseOverEventRef.current, { capture: true, passive: true }); }; (0, _controllers.useSkipLink)(id, skipLinkTitle); var onMouseLeave = function onMouseLeave(event) { var isMouseOnResizeButton = event.target.matches("[".concat(_constants.RESIZE_BUTTON_SELECTOR, "]")) || event.target.matches("[".concat(_constants.RESIZE_BUTTON_SELECTOR, "] *")); if (isMouseOnResizeButton || !isLeftSidebarCollapsed || flyoutLockCount > 0) { return; } onFlyoutCollapse && onFlyoutCollapse(); setTimeout(function () { setLeftSidebarState(function (current) { return _objectSpread(_objectSpread({}, current), {}, { isFlyoutOpen: false }); }); }, _constants.FLYOUT_DELAY); }; var mobileMediaQuery = (0, _responsive.UNSAFE_useMediaQuery)('below.sm'); var openMobileFlyout = (0, _react.useCallback)(function () { if (!(mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches)) { return; } setLeftSidebarState(function (current) { if (current.isFlyoutOpen) { return current; } return _objectSpread(_objectSpread({}, current), {}, { isFlyoutOpen: true }); }); }, [setLeftSidebarState, mobileMediaQuery]); var closeMobileFlyout = (0, _react.useCallback)(function () { if (!(mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches)) { return; } setLeftSidebarState(function (current) { if (!current.isFlyoutOpen) { return current; } return _objectSpread(_objectSpread({}, current), {}, { isFlyoutOpen: false }); }); }, [setLeftSidebarState, mobileMediaQuery]); (0, _responsive.UNSAFE_useMediaQuery)('below.sm', function (event) { setLeftSidebarState(function (current) { if (event.matches && !current.isLeftSidebarCollapsed) { // Sidebar was previously open when resizing downwards, convert the sidebar being open to a flyout being open return _objectSpread(_objectSpread({}, current), {}, { isResizing: false, isLeftSidebarCollapsed: true, leftSidebarWidth: _constants.COLLAPSED_LEFT_SIDEBAR_WIDTH, lastLeftSidebarWidth: current.leftSidebarWidth, isFlyoutOpen: true }); } else if (!event.matches && current.isFlyoutOpen) { // The user is resizing "upwards", eg. going from mobile to desktop. // Flyout was previously open, let's keep it open by moving to the un-collapsed sidebar instead return _objectSpread(_objectSpread({}, current), {}, { isResizing: false, isLeftSidebarCollapsed: false, leftSidebarWidth: Math.max(current.lastLeftSidebarWidth, _constants.DEFAULT_LEFT_SIDEBAR_WIDTH), isFlyoutOpen: false }); } return current; }); }); // Close the flyout when the "escape" key is pressed. (0, _useCloseOnEscapePress.default)({ onClose: closeMobileFlyout, isDisabled: !isFlyoutOpen }); /** * We use both the state and our effect-based ref to protect animation until initialized fully */ var isReady = hasInit && notFirstRun.current; return (0, _react2.jsx)(_react.Fragment, null, (mobileMediaQuery === null || mobileMediaQuery === void 0 ? void 0 : mobileMediaQuery.matches) && /** * On desktop, the `onClick` handlers controls the temporary flyout behavior. * This is an intentionally mouse-only experience, it may even be disruptive with keyboard navigation. * * On mobile, the `onClick` handler controls the toggled flyout behaviour. * This is not intended to be how you use this with a keyboard, there is a ResizeButton for this intentionally instead. */ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions, @atlassian/a11y/interactive-element-not-keyboard-focusable (0, _react2.jsx)("div", { css: [hiddenBackdropStyles, isFlyoutOpen && openBackdropStyles], onClick: closeMobileFlyout }), (0, _react2.jsx)(_leftSidebarOuter.default, { ref: leftSideBarRef, testId: testId, onMouseOver: !(mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches) ? onMouseOver : undefined, onMouseLeave: !(mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches) ? onMouseLeave : undefined, onClick: mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches ? openMobileFlyout : undefined, id: id, isFixed: isFixed }, (0, _react2.jsx)(_slotDimensions.default, { variableName: _constants.VAR_LEFT_SIDEBAR_WIDTH, value: isReady ? leftSidebarWidth : leftSidebarWidthOnMount, mobileValue: _constants.MOBILE_COLLAPSED_LEFT_SIDEBAR_WIDTH }), (0, _react2.jsx)(_leftSidebarInner.default, { isFixed: isFixed, isFlyoutOpen: isFlyoutOpen }, (0, _react2.jsx)(_resizableChildrenWrapper.default, { isFlyoutOpen: isFlyoutOpen, isLeftSidebarCollapsed: isLeftSidebarCollapsed, hasCollapsedState: !isReady && collapsedState === 'collapsed', testId: testId && "".concat(testId, "-resize-children-wrapper") }, children), (0, _react2.jsx)(_resizeControl.default, { testId: testId, valueTextLabel: valueTextLabel, resizeGrabAreaLabel: resizeGrabAreaLabel, resizeButtonLabel: resizeButtonLabel // eslint-disable-next-line @repo/internal/react/no-unsafe-overrides , overrides: overrides, onResizeStart: onResizeStart, onResizeEnd: onResizeEnd })))); }; var _default = exports.default = LeftSidebar;