@atlaskit/page-layout
Version:
A collection of components which let you compose an application's page layout.
253 lines (242 loc) • 10.4 kB
JavaScript
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { bind } from 'bind-event-listener';
import noop from '@atlaskit/ds-lib/noop';
import { isReducedMotion } from '@atlaskit/motion';
import { UNSAFE_useMediaQuery as useMediaQuery } from '@atlaskit/primitives/responsive';
import { COLLAPSED_LEFT_SIDEBAR_WIDTH, DEFAULT_LEFT_SIDEBAR_WIDTH, IS_SIDEBAR_COLLAPSING } from '../common/constants';
import { getPageLayoutSlotCSSSelector } from '../common/utils';
import { SidebarResizeContext } from './sidebar-resize-context';
var handleDataAttributesAndCb = function handleDataAttributesAndCb() {
var callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : noop;
var leftSidebarState = arguments.length > 1 ? arguments[1] : undefined;
document.documentElement.removeAttribute(IS_SIDEBAR_COLLAPSING);
callback(leftSidebarState);
};
var leftSidebarSelector = getPageLayoutSlotCSSSelector('left-sidebar');
// eslint-disable-next-line @repo/internal/react/require-jsdoc
export var SidebarResizeController = function SidebarResizeController(_ref) {
var children = _ref.children,
onExpand = _ref.onLeftSidebarExpand,
onCollapse = _ref.onLeftSidebarCollapse;
var _useState = useState({
isFlyoutOpen: false,
isResizing: false,
isLeftSidebarCollapsed: false,
leftSidebarWidth: 0,
lastLeftSidebarWidth: 0,
flyoutLockCount: 0,
isFixed: true,
hasInit: false
}),
_useState2 = _slicedToArray(_useState, 2),
leftSidebarState = _useState2[0],
setLeftSidebarState = _useState2[1];
var leftSidebarWidth = leftSidebarState.leftSidebarWidth,
lastLeftSidebarWidth = leftSidebarState.lastLeftSidebarWidth,
isResizing = leftSidebarState.isResizing,
flyoutLockCount = leftSidebarState.flyoutLockCount,
isFixed = leftSidebarState.isFixed,
isLeftSidebarCollapsed = leftSidebarState.isLeftSidebarCollapsed,
isFlyoutOpen = leftSidebarState.isFlyoutOpen,
hasInit = leftSidebarState.hasInit;
// We put the latest callbacks into a ref so we can always have the latest
// functions in our transitionend listeners
var stableRef = useRef({
onExpand: onExpand,
onCollapse: onCollapse
});
useEffect(function () {
stableRef.current = {
onExpand: onExpand,
onCollapse: onCollapse
};
}, [onExpand, onCollapse]);
var transition = useRef(null);
var mobileMediaQuery = useMediaQuery('below.sm');
var isOpen = mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches ? isFlyoutOpen : !isLeftSidebarCollapsed;
var expandLeftSidebar = useCallback(function () {
var _transition$current2, _transition$current3;
if (isOpen) {
return;
}
// If the user is at a mobile viewport when this runs, we handle it differently
// We don't expand at mobile widths; instead we use a flyout which is treated the same otherwise
if (mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches) {
var _transition$current;
var flyoutOpenSidebarState = {
isResizing: false,
isLeftSidebarCollapsed: true,
leftSidebarWidth: COLLAPSED_LEFT_SIDEBAR_WIDTH,
lastLeftSidebarWidth: leftSidebarWidth,
isFlyoutOpen: true,
flyoutLockCount: 0,
isFixed: isFixed,
hasInit: hasInit
};
setLeftSidebarState(flyoutOpenSidebarState);
// Flush the desktop transitions, cleanup, and call the `onExpand` still
(_transition$current = transition.current) === null || _transition$current === void 0 || _transition$current.complete();
handleDataAttributesAndCb(stableRef.current.onExpand, flyoutOpenSidebarState);
return;
}
if (isResizing || !isLeftSidebarCollapsed ||
// already expanding
((_transition$current2 = transition.current) === null || _transition$current2 === void 0 ? void 0 : _transition$current2.action) === 'expand') {
return;
}
// flush existing transition
(_transition$current3 = transition.current) === null || _transition$current3 === void 0 || _transition$current3.complete();
var width = Math.max(lastLeftSidebarWidth, DEFAULT_LEFT_SIDEBAR_WIDTH);
var updatedLeftSidebarState = {
isLeftSidebarCollapsed: false,
isFlyoutOpen: false,
leftSidebarWidth: width,
lastLeftSidebarWidth: lastLeftSidebarWidth,
isResizing: isResizing,
flyoutLockCount: flyoutLockCount,
isFixed: isFixed,
hasInit: hasInit
};
setLeftSidebarState(updatedLeftSidebarState);
function finish() {
handleDataAttributesAndCb(stableRef.current.onExpand, updatedLeftSidebarState);
}
var sidebar = document.querySelector(leftSidebarSelector);
// onTransitionEnd isn't triggered when a user prefers reduced motion
if (isReducedMotion() || !sidebar) {
finish();
return;
}
var unbindEvent = bind(sidebar, {
type: 'transitionend',
listener: function listener(event) {
if (event.target === sidebar && event.propertyName === 'width') {
var _transition$current4;
(_transition$current4 = transition.current) === null || _transition$current4 === void 0 || _transition$current4.complete();
}
}
});
var value = {
action: 'expand',
complete: function complete() {
value.abort();
finish();
},
abort: function abort() {
unbindEvent();
transition.current = null;
}
};
transition.current = value;
}, [isOpen, mobileMediaQuery, isResizing, isLeftSidebarCollapsed, lastLeftSidebarWidth, flyoutLockCount, isFixed, leftSidebarWidth, hasInit]);
var collapseLeftSidebar = useCallback(function (event, collapseWithoutTransition) {
var _transition$current6, _transition$current7;
if (!isOpen) {
return;
}
// If the user is at a mobile viewport when this runs, we handle it differently
// We don't collapse at mobile widths; instead we close the flyout.
if (mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches) {
var _transition$current5;
var flyoutCloseSidebarState = {
isResizing: false,
isLeftSidebarCollapsed: true,
leftSidebarWidth: COLLAPSED_LEFT_SIDEBAR_WIDTH,
lastLeftSidebarWidth: lastLeftSidebarWidth,
isFlyoutOpen: false,
flyoutLockCount: 0,
isFixed: isFixed,
hasInit: hasInit
};
setLeftSidebarState(flyoutCloseSidebarState);
// Flush the desktop transitions, cleanup, and call the `onCollapse` still
(_transition$current5 = transition.current) === null || _transition$current5 === void 0 || _transition$current5.complete();
handleDataAttributesAndCb(stableRef.current.onCollapse, flyoutCloseSidebarState);
return;
}
if (isResizing || isLeftSidebarCollapsed ||
// already collapsing
((_transition$current6 = transition.current) === null || _transition$current6 === void 0 ? void 0 : _transition$current6.action) === 'collapse') {
return;
}
// flush existing transition
(_transition$current7 = transition.current) === null || _transition$current7 === void 0 || _transition$current7.complete();
// data-attribute is used as a CSS selector to sync the hiding/showing
// of the nav contents with expand/collapse animation
document.documentElement.setAttribute(IS_SIDEBAR_COLLAPSING, 'true');
var updatedLeftSidebarState = {
isLeftSidebarCollapsed: true,
isFlyoutOpen: false,
leftSidebarWidth: COLLAPSED_LEFT_SIDEBAR_WIDTH,
lastLeftSidebarWidth: leftSidebarWidth,
isResizing: isResizing,
flyoutLockCount: flyoutLockCount,
isFixed: isFixed,
hasInit: hasInit
};
setLeftSidebarState(updatedLeftSidebarState);
function finish() {
handleDataAttributesAndCb(stableRef.current.onCollapse, updatedLeftSidebarState);
}
var sidebar = document.querySelector(leftSidebarSelector);
// onTransitionEnd isn't triggered when a user prefers reduced motion
if (collapseWithoutTransition || isReducedMotion() || !sidebar) {
finish();
return;
}
var unbindEvent = bind(sidebar, {
type: 'transitionend',
listener: function listener(event) {
if (sidebar === event.target && event.propertyName === 'width') {
var _transition$current8;
(_transition$current8 = transition.current) === null || _transition$current8 === void 0 || _transition$current8.complete();
}
}
});
var value = {
action: 'collapse',
complete: function complete() {
value.abort();
finish();
},
abort: function abort() {
unbindEvent();
transition.current = null;
}
};
transition.current = value;
}, [isOpen, mobileMediaQuery, isResizing, isLeftSidebarCollapsed, leftSidebarWidth, flyoutLockCount, isFixed, lastLeftSidebarWidth, hasInit]);
/**
* Conditionally toggle the expanding or collapsing the sidebars.
* This supports our mobile flyout mode as well.
*/
var toggleLeftSidebar = useCallback(function (event, collapseWithoutTransition) {
if (isOpen) {
collapseLeftSidebar(event, collapseWithoutTransition);
} else {
expandLeftSidebar();
}
}, [isOpen, expandLeftSidebar, collapseLeftSidebar]);
// Make sure we finish any lingering transitions when unmounting
useEffect(function mount() {
return function unmount() {
var _transition$current9;
(_transition$current9 = transition.current) === null || _transition$current9 === void 0 || _transition$current9.abort();
};
}, []);
var context = useMemo(function () {
return {
isLeftSidebarCollapsed: !isOpen,
// Technically this isn't quite true, but with mobile it's a bit safer if products are using this to roll their own collapse/expand
expandLeftSidebar: expandLeftSidebar,
collapseLeftSidebar: collapseLeftSidebar,
leftSidebarState: leftSidebarState,
setLeftSidebarState: setLeftSidebarState,
toggleLeftSidebar: toggleLeftSidebar
};
}, [isOpen, expandLeftSidebar, collapseLeftSidebar, leftSidebarState, toggleLeftSidebar]);
return /*#__PURE__*/React.createElement(SidebarResizeContext.Provider, {
value: context
}, children);
};