@atlaskit/page-layout
Version:
A collection of components which let you compose an application's page layout.
336 lines (327 loc) • 15.9 kB
JavaScript
;
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;