@atlaskit/navigation-system
Version:
The latest navigation system for Atlassian apps.
224 lines (222 loc) • 8.89 kB
JavaScript
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
}));
}