UNPKG

@atlaskit/page-layout

Version:

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

248 lines (237 loc) 9.46 kB
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/get-page-layout-slot-css-selector'; 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 apps 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); };