UNPKG

@atlaskit/navigation-system

Version:

The latest navigation system for Atlassian apps.

224 lines (222 loc) • 8.89 kB
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { bind, bindAll } from 'bind-event-listener'; import { defaultMainMinWidth } from './constants'; import { LayoutAreaSizingContext } from './layout-area-sizing-context'; import { layoutAreaStateContexts } from './layout-area-state-contexts'; import { useCssLayoutAreaStates } from './use-css-layout-area-states'; /** * Stable, synchronous boundary for SSR content. The drag/cascade implementation * is a sibling, not a replacement wrapper, so loading it cannot remount the app. * CSS handles initial and responsive geometry; only splitter interaction requests * the asynchronous chunk. The eager observer reads modes without changing widths. */ export function LayoutAreaSizingProvider({ children, layoutRef }) { var _states$sideNav$mode, _states$sideNav, _states$chatPanel$mo, _states$chatPanel; const [Runtime, setRuntime] = useState(null); const [states, setStates] = useState({}); const actions = useRef(null); const configs = useRef({}); const [lastOpenedOverlay, setLastOpenedOverlay] = useState(null); const [configRevision, setConfigRevision] = useState(0); const cssModes = useCssLayoutAreaStates(layoutRef, configRevision, Runtime === null); const sideNavMode = (_states$sideNav$mode = (_states$sideNav = states['side-nav']) === null || _states$sideNav === void 0 ? void 0 : _states$sideNav.mode) !== null && _states$sideNav$mode !== void 0 ? _states$sideNav$mode : cssModes['side-nav']; const chatPanelMode = (_states$chatPanel$mo = (_states$chatPanel = states['chat-panel']) === null || _states$chatPanel === void 0 ? void 0 : _states$chatPanel.mode) !== null && _states$chatPanel$mo !== void 0 ? _states$chatPanel$mo : cssModes['chat-panel']; // Overlay dismissal is available before the sizing chunk loads. Keep the open // order here too: replaying registrations into the runtime loses that order. useEffect(() => { const inlineChatQuery = window.matchMedia('(min-width: 40rem)'); const dismissOlderOverlay = () => { var _olderOverlay$onReque; const sideNav = configs.current['side-nav']; const chatPanel = configs.current['chat-panel']; if (!(sideNav !== null && sideNav !== void 0 && sideNav.isOpen) || !(chatPanel !== null && chatPanel !== void 0 && chatPanel.isOpen) || !lastOpenedOverlay) { return; } // Before allocation, CSS puts both areas in overlay below 40rem. Once // sizing is active, also honour overlays caused by custom widths/slots. if (inlineChatQuery.matches && (sideNavMode !== 'overlay' || chatPanelMode !== 'overlay')) { return; } const olderOverlay = lastOpenedOverlay === 'side-nav' ? chatPanel : sideNav; (_olderOverlay$onReque = olderOverlay.onRequestClose) === null || _olderOverlay$onReque === void 0 ? void 0 : _olderOverlay$onReque.call(olderOverlay); }; dismissOlderOverlay(); return bind(inlineChatQuery, { type: 'change', listener: dismissOlderOverlay }); }, [chatPanelMode, lastOpenedOverlay, sideNavMode]); const splitters = useRef(new Set()); const registerSplitter = useCallback(element => { splitters.current.add(element); return () => { splitters.current.delete(element); }; }, []); const mainMinWidth = useRef(defaultMainMinWidth); const loadRef = useRef(() => {}); const ready = useRef(false); const waiting = useRef([]); const pending = useRef([]); const prepareResize = useCallback(() => { if (ready.current) { return Promise.resolve(true); } return new Promise(resolve => { waiting.current.push(resolve); loadRef.current(); }); }, []); useEffect(() => { const root = layoutRef === null || layoutRef === void 0 ? void 0 : layoutRef.current; if (!root) { return; } let cancelled = false; let loading = false; const waitingForRuntime = waiting.current; const load = () => { if (loading) { return; } loading = true; void import( /* webpackChunkName: "@atlaskit-internal_navigation-layout-sizing" */'./layout-area-sizing-runtime').then(({ LayoutAreaSizingRuntime }) => { if (!cancelled) { setRuntime(() => LayoutAreaSizingRuntime); } }, () => { loading = false; pending.current = []; for (const resolve of waitingForRuntime.splice(0)) { resolve(false); } }); }; loadRef.current = load; const loadForSplitter = event => { if (!(event.target instanceof Node)) { return; } for (const splitter of splitters.current) { if (splitter.contains(event.target)) { load(); return; } } }; const unbindRoot = bindAll(root, [{ type: 'pointerover', listener: loadForSplitter, options: { passive: true } }, { type: 'focusin', listener: loadForSplitter }, { type: 'pointerdown', listener: loadForSplitter, options: { passive: true } }]); return () => { cancelled = true; ready.current = false; pending.current = []; for (const resolve of waitingForRuntime.splice(0)) { resolve(false); } loadRef.current = () => {}; unbindRoot(); }; }, [layoutRef]); const onReady = useCallback(runtime => { actions.current = runtime; if (runtime) { runtime.setMainMinWidth(mainMinWidth.current); for (const area of ['side-nav', 'panel', 'chat-panel']) { const config = configs.current[area]; if (config) { runtime.registerArea(area, config); } } } }, []); const onUpdate = useCallback(next => { setStates(next); const runtime = actions.current; if (runtime && Object.values(next).some(Boolean)) { ready.current = true; for (const action of pending.current.splice(0)) { action(runtime); } for (const resolve of waiting.current.splice(0)) { resolve(true); } } }, []); const dispatch = useCallback(action => { if (ready.current && actions.current) { action(actions.current); } else { pending.current.push(action); loadRef.current(); } }, []); const value = useMemo(() => ({ prepareResize, registerSplitter, registerArea: (area, config) => { var _configs$current$area, _actions$current; if (area !== 'panel' && config.isOpen && !((_configs$current$area = configs.current[area]) !== null && _configs$current$area !== void 0 && _configs$current$area.isOpen)) { setLastOpenedOverlay(area); } configs.current[area] = config; setConfigRevision(revision => revision + 1); (_actions$current = actions.current) === null || _actions$current === void 0 ? void 0 : _actions$current.registerArea(area, config); }, unregisterArea: area => { var _actions$current2; delete configs.current[area]; setConfigRevision(revision => revision + 1); (_actions$current2 = actions.current) === null || _actions$current2 === void 0 ? void 0 : _actions$current2.unregisterArea(area); }, setMainMinWidth: width => { var _actions$current3; if (mainMinWidth.current !== width) { setConfigRevision(revision => revision + 1); } mainMinWidth.current = width; (_actions$current3 = actions.current) === null || _actions$current3 === void 0 ? void 0 : _actions$current3.setMainMinWidth(width); }, getResizeBounds: area => { var _actions$current4; return (_actions$current4 = actions.current) === null || _actions$current4 === void 0 ? void 0 : _actions$current4.getResizeBounds(area); }, startResize: (area, mode) => dispatch(runtime => runtime.startResize(area, mode)), resize: (area, mode, width) => dispatch(runtime => runtime.resize(area, mode, width)), completeResize: (area, mode, width) => dispatch(runtime => runtime.completeResize(area, mode, width)) }), [dispatch, prepareResize, registerSplitter]); const SideNavStateProvider = layoutAreaStateContexts['side-nav'].Provider; const PanelStateProvider = layoutAreaStateContexts.panel.Provider; const ChatPanelStateProvider = layoutAreaStateContexts['chat-panel'].Provider; return /*#__PURE__*/React.createElement(LayoutAreaSizingContext.Provider, { value: value }, /*#__PURE__*/React.createElement(SideNavStateProvider, { value: states['side-nav'] }, /*#__PURE__*/React.createElement(PanelStateProvider, { value: states.panel }, /*#__PURE__*/React.createElement(ChatPanelStateProvider, { value: states['chat-panel'] }, children))), Runtime && /*#__PURE__*/React.createElement(Runtime, { layoutRef: layoutRef, onReady: onReady, onUpdate: onUpdate })); }