@atlaskit/page-layout
Version:
A collection of components which let you compose an application's page layout.
248 lines (237 loc) • 9.44 kB
JavaScript
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';
const handleDataAttributesAndCb = (callback = noop, leftSidebarState) => {
document.documentElement.removeAttribute(IS_SIDEBAR_COLLAPSING);
callback(leftSidebarState);
};
const leftSidebarSelector = getPageLayoutSlotCSSSelector('left-sidebar');
// eslint-disable-next-line @repo/internal/react/require-jsdoc
export const SidebarResizeController = ({
children,
onLeftSidebarExpand: onExpand,
onLeftSidebarCollapse: onCollapse
}) => {
const [leftSidebarState, setLeftSidebarState] = useState({
isFlyoutOpen: false,
isResizing: false,
isLeftSidebarCollapsed: false,
leftSidebarWidth: 0,
lastLeftSidebarWidth: 0,
flyoutLockCount: 0,
isFixed: true,
hasInit: false
});
const {
leftSidebarWidth,
lastLeftSidebarWidth,
isResizing,
flyoutLockCount,
isFixed,
isLeftSidebarCollapsed,
isFlyoutOpen,
hasInit
} = leftSidebarState;
// We put the latest callbacks into a ref so we can always have the latest
// functions in our transitionend listeners
const stableRef = useRef({
onExpand,
onCollapse
});
useEffect(() => {
stableRef.current = {
onExpand,
onCollapse
};
}, [onExpand, onCollapse]);
const transition = useRef(null);
const mobileMediaQuery = useMediaQuery('below.sm');
const isOpen = mobileMediaQuery !== null && mobileMediaQuery !== void 0 && mobileMediaQuery.matches ? isFlyoutOpen : !isLeftSidebarCollapsed;
const expandLeftSidebar = useCallback(() => {
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;
const flyoutOpenSidebarState = {
isResizing: false,
isLeftSidebarCollapsed: true,
leftSidebarWidth: COLLAPSED_LEFT_SIDEBAR_WIDTH,
lastLeftSidebarWidth: leftSidebarWidth,
isFlyoutOpen: true,
flyoutLockCount: 0,
isFixed,
hasInit
};
setLeftSidebarState(flyoutOpenSidebarState);
// Flush the desktop transitions, cleanup, and call the `onExpand` still
(_transition$current = transition.current) === null || _transition$current === void 0 ? 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 ? void 0 : _transition$current3.complete();
const width = Math.max(lastLeftSidebarWidth, DEFAULT_LEFT_SIDEBAR_WIDTH);
const updatedLeftSidebarState = {
isLeftSidebarCollapsed: false,
isFlyoutOpen: false,
leftSidebarWidth: width,
lastLeftSidebarWidth,
isResizing,
flyoutLockCount,
isFixed,
hasInit
};
setLeftSidebarState(updatedLeftSidebarState);
function finish() {
handleDataAttributesAndCb(stableRef.current.onExpand, updatedLeftSidebarState);
}
const sidebar = document.querySelector(leftSidebarSelector);
// onTransitionEnd isn't triggered when a user prefers reduced motion
if (isReducedMotion() || !sidebar) {
finish();
return;
}
const unbindEvent = bind(sidebar, {
type: 'transitionend',
listener(event) {
if (event.target === sidebar && event.propertyName === 'width') {
var _transition$current4;
(_transition$current4 = transition.current) === null || _transition$current4 === void 0 ? void 0 : _transition$current4.complete();
}
}
});
const value = {
action: 'expand',
complete: () => {
value.abort();
finish();
},
abort: () => {
unbindEvent();
transition.current = null;
}
};
transition.current = value;
}, [isOpen, mobileMediaQuery, isResizing, isLeftSidebarCollapsed, lastLeftSidebarWidth, flyoutLockCount, isFixed, leftSidebarWidth, hasInit]);
const collapseLeftSidebar = useCallback((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;
const flyoutCloseSidebarState = {
isResizing: false,
isLeftSidebarCollapsed: true,
leftSidebarWidth: COLLAPSED_LEFT_SIDEBAR_WIDTH,
lastLeftSidebarWidth,
isFlyoutOpen: false,
flyoutLockCount: 0,
isFixed,
hasInit
};
setLeftSidebarState(flyoutCloseSidebarState);
// Flush the desktop transitions, cleanup, and call the `onCollapse` still
(_transition$current5 = transition.current) === null || _transition$current5 === void 0 ? 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 ? 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');
const updatedLeftSidebarState = {
isLeftSidebarCollapsed: true,
isFlyoutOpen: false,
leftSidebarWidth: COLLAPSED_LEFT_SIDEBAR_WIDTH,
lastLeftSidebarWidth: leftSidebarWidth,
isResizing,
flyoutLockCount,
isFixed,
hasInit
};
setLeftSidebarState(updatedLeftSidebarState);
function finish() {
handleDataAttributesAndCb(stableRef.current.onCollapse, updatedLeftSidebarState);
}
const sidebar = document.querySelector(leftSidebarSelector);
// onTransitionEnd isn't triggered when a user prefers reduced motion
if (collapseWithoutTransition || isReducedMotion() || !sidebar) {
finish();
return;
}
const unbindEvent = bind(sidebar, {
type: 'transitionend',
listener(event) {
if (sidebar === event.target && event.propertyName === 'width') {
var _transition$current8;
(_transition$current8 = transition.current) === null || _transition$current8 === void 0 ? void 0 : _transition$current8.complete();
}
}
});
const value = {
action: 'collapse',
complete: () => {
value.abort();
finish();
},
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.
*/
const toggleLeftSidebar = useCallback((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 ? void 0 : _transition$current9.abort();
};
}, []);
const context = useMemo(() => ({
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,
collapseLeftSidebar,
leftSidebarState,
setLeftSidebarState,
toggleLeftSidebar
}), [isOpen, expandLeftSidebar, collapseLeftSidebar, leftSidebarState, toggleLeftSidebar]);
return /*#__PURE__*/React.createElement(SidebarResizeContext.Provider, {
value: context
}, children);
};