@atlaskit/navigation-system
Version:
The latest navigation system for Atlassian apps.
460 lines (458 loc) • 23.9 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { bind } from 'bind-event-listener';
import { calculateLayoutAreaStates } from './calculate-layout-area-states';
import { defaultMainMinWidth } from './constants';
import { LayoutAreaSizingContext } from './layout-area-sizing-context';
import { layoutAreaStateContexts } from './layout-area-state-contexts';
import { resolveLayoutWidth } from './resolve-layout-width';
import { useOccupiedLayoutWidth } from './use-occupied-layout-width';
function useStableAreaState(state) {
var _ref = state !== null && state !== void 0 ? state : {},
mode = _ref.mode,
width = _ref.width,
minWidth = _ref.minWidth;
return useMemo(function () {
return mode !== undefined && width !== undefined && minWidth !== undefined ? {
mode: mode,
width: width,
minWidth: minWidth
} : undefined;
}, [mode, width, minWidth]);
}
export function LayoutAreaSizingRuntime(_ref2) {
var children = _ref2.children,
layoutRef = _ref2.layoutRef,
onReady = _ref2.onReady,
onUpdate = _ref2.onUpdate;
var occupiedWidth = useOccupiedLayoutWidth(layoutRef);
var _useState = useState(null),
_useState2 = _slicedToArray(_useState, 2),
dimensions = _useState2[0],
setDimensions = _useState2[1];
var _useState3 = useState(defaultMainMinWidth),
_useState4 = _slicedToArray(_useState3, 2),
mainMinWidth = _useState4[0],
setMainMinWidth = _useState4[1];
var _useState5 = useState({}),
_useState6 = _slicedToArray(_useState5, 2),
areas = _useState6[0],
setAreas = _useState6[1];
var _useState7 = useState(null),
_useState8 = _slicedToArray(_useState7, 2),
resizeSession = _useState8[0],
setResizeSession = _useState8[1];
var areaStatesRef = React.useRef({});
var configsRef = React.useRef({});
var _useState9 = useState(false),
_useState0 = _slicedToArray(_useState9, 2),
hasInteraction = _useState0[0],
setHasInteraction = _useState0[1];
var adoptedConstraints = React.useRef(null);
// Preserve the CSS handoff only while its available space is unchanged.
var isPreservingInitialLayout = !hasInteraction && (!adoptedConstraints.current || adoptedConstraints.current.occupiedWidth === occupiedWidth && adoptedConstraints.current.mainMinWidth === mainMinWidth);
var _useState1 = useState({}),
_useState10 = _slicedToArray(_useState1, 2),
initialStates = _useState10[0],
setInitialStates = _useState10[1];
var inputsRef = React.useRef({
dimensions: dimensions,
occupiedWidth: occupiedWidth,
mainMinWidth: mainMinWidth,
areas: areas,
resizeSession: resizeSession
});
useEffect(function () {
inputsRef.current = {
dimensions: dimensions,
occupiedWidth: occupiedWidth,
mainMinWidth: mainMinWidth,
areas: areas,
resizeSession: resizeSession
};
}, [dimensions, occupiedWidth, mainMinWidth, areas, resizeSession]);
useEffect(function () {
if (adoptedConstraints.current && (adoptedConstraints.current.occupiedWidth !== occupiedWidth || adoptedConstraints.current.mainMinWidth !== mainMinWidth)) {
setHasInteraction(true);
}
setResizeSession(null);
}, [mainMinWidth, occupiedWidth]);
useEffect(function () {
var root = layoutRef === null || layoutRef === void 0 ? void 0 : layoutRef.current;
var previous = null;
// Match the actual rem-based grid breakpoint, including browser font settings.
var inlineChatQuery = window.matchMedia('(min-width: 40rem)');
var inlineSideNavQuery = window.matchMedia('(min-width: 64rem)');
var updateDimensions = function updateDimensions(contentWidth) {
var _style$width, _style$paddingLeft, _style$paddingRight, _style$borderLeftWidt, _style$borderRightWid, _previous;
var style = root && contentWidth === undefined ? getComputedStyle(root) : null;
var computedWidth = Number.parseFloat((_style$width = style === null || style === void 0 ? void 0 : style.width) !== null && _style$width !== void 0 ? _style$width : '');
var padding = (parseFloat((_style$paddingLeft = style === null || style === void 0 ? void 0 : style.paddingLeft) !== null && _style$paddingLeft !== void 0 ? _style$paddingLeft : '') || 0) + (parseFloat((_style$paddingRight = style === null || style === void 0 ? void 0 : style.paddingRight) !== null && _style$paddingRight !== void 0 ? _style$paddingRight : '') || 0);
var border = (parseFloat((_style$borderLeftWidt = style === null || style === void 0 ? void 0 : style.borderLeftWidth) !== null && _style$borderLeftWidt !== void 0 ? _style$borderLeftWidt : '') || 0) + (parseFloat((_style$borderRightWid = style === null || style === void 0 ? void 0 : style.borderRightWidth) !== null && _style$borderRightWid !== void 0 ? _style$borderRightWid : '') || 0);
var layoutWidth = contentWidth !== null && contentWidth !== void 0 ? contentWidth : root ?
// Keep fractional CSS pixels, just as ResizeObserver does. Otherwise
// its initial delivery could be mistaken for a viewport resize.
Math.max(0, Number.isFinite(computedWidth) ? computedWidth - ((style === null || style === void 0 ? void 0 : style.boxSizing) === 'border-box' ? padding + border : 0) : root.clientWidth - padding) : window.innerWidth;
var next = {
viewportWidth: window.innerWidth,
layoutWidth: layoutWidth,
isChatPanelOverlay: !inlineChatQuery.matches,
isSideNavOverlay: !inlineSideNavQuery.matches
};
if (((_previous = previous) === null || _previous === void 0 ? void 0 : _previous.viewportWidth) === next.viewportWidth && previous.layoutWidth === next.layoutWidth && previous.isChatPanelOverlay === next.isChatPanelOverlay && previous.isSideNavOverlay === next.isSideNavOverlay) {
return;
}
if (previous) {
setHasInteraction(true);
}
previous = next;
setDimensions(next);
setResizeSession(null);
};
updateDimensions();
var observer = new ResizeObserver(function (entries) {
var entry = entries.find(function (entry) {
return entry.target === root;
});
if (entry) {
updateDimensions(entry.contentRect.width);
}
});
if (root) {
observer.observe(root);
}
var unbindResize = bind(window, {
type: 'resize',
listener: function listener() {
return updateDimensions();
}
});
var unbindQuery = bind(inlineChatQuery, {
type: 'change',
listener: function listener() {
return updateDimensions();
}
});
var unbindSideNavQuery = bind(inlineSideNavQuery, {
type: 'change',
listener: function listener() {
return updateDimensions();
}
});
return function () {
observer.disconnect();
unbindResize();
unbindQuery();
unbindSideNavQuery();
};
}, [layoutRef]);
var registerArea = useCallback(function (area, config) {
var previous = configsRef.current[area];
if (adoptedConstraints.current && (!previous || previous.isOpen !== config.isOpen || previous.requestedWidth !== config.requestedWidth || previous.minWidth !== config.minWidth || previous.maxWidth !== config.maxWidth || previous.defaultWidth !== config.defaultWidth || previous.compactDefaultWidth !== config.compactDefaultWidth)) {
setHasInteraction(true);
}
configsRef.current[area] = config;
setResizeSession(function (current) {
// A changed layout/configuration needs a fresh allocation. A controlled
// acknowledgement for the resized area keeps its siblings fixed.
return previous && (previous.isOpen === config.isOpen || area === 'side-nav' && !config.isOpen && (current === null || current === void 0 ? void 0 : current.area) !== 'side-nav') && previous.minWidth === config.minWidth && previous.maxWidth === config.maxWidth && previous.defaultWidth === config.defaultWidth && previous.compactDefaultWidth === config.compactDefaultWidth && (previous.requestedWidth === config.requestedWidth || (current === null || current === void 0 ? void 0 : current.area) === area && current.completedWidth === config.requestedWidth || area === 'side-nav' && !config.isOpen && (current === null || current === void 0 ? void 0 : current.area) !== 'side-nav') ? current : null;
});
setAreas(function (current) {
var existing = current[area];
return _objectSpread(_objectSpread({}, current), {}, _defineProperty({}, area, _objectSpread(_objectSpread(_objectSpread({}, existing), config), {}, {
liveResize: config.isOpen ? existing === null || existing === void 0 ? void 0 : existing.liveResize : undefined
})));
});
}, []);
var unregisterArea = useCallback(function (area) {
if (adoptedConstraints.current) {
setHasInteraction(true);
}
delete configsRef.current[area];
setResizeSession(null);
setAreas(function (current) {
if (!current[area]) {
return current;
}
// Closing uses isOpen: false and retains preferences. Unregistering means
// the owning component unmounted; a replacement must start with its own defaults.
var next = _objectSpread({}, current);
delete next[area];
return next;
});
}, []);
var captureSiblingWidths = useCallback(function (area) {
var mode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'inline';
var baselineWidths = {};
var overlayModes = {};
var _iterator = _createForOfIteratorHelper(['side-nav', 'panel', 'chat-panel']),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var areaName = _step.value;
var state = areaStatesRef.current[areaName];
if (state) {
overlayModes[areaName] = state.mode;
}
if ((state === null || state === void 0 ? void 0 : state.mode) === 'inline') {
baselineWidths[areaName] = state.width;
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return {
area: area,
baselineWidths: baselineWidths,
overlayModes: mode === 'overlay' ? overlayModes : undefined
};
}, []);
var startResize = useCallback(function (area, mode) {
setHasInteraction(true);
setResizeSession(captureSiblingWidths(area, mode));
// Start from the effective width, which may already be compressed below the preferred width.
// Reapplying the preferred width here would move the splitter away from the pointer.
var renderedState = areaStatesRef.current[area];
if ((renderedState === null || renderedState === void 0 ? void 0 : renderedState.mode) === mode) {
setAreas(function (current) {
var existing = current[area];
return existing ? _objectSpread(_objectSpread({}, current), {}, _defineProperty({}, area, _objectSpread(_objectSpread({}, existing), {}, {
liveResize: {
mode: mode,
width: renderedState.width
}
}))) : current;
});
}
}, [captureSiblingWidths]);
var resize = useCallback(function (area, mode, width) {
setAreas(function (current) {
var _existing$liveResize;
var existing = current[area];
if ((existing === null || existing === void 0 || (_existing$liveResize = existing.liveResize) === null || _existing$liveResize === void 0 ? void 0 : _existing$liveResize.mode) === mode && existing.liveResize.width === width) {
return current;
}
return existing ? _objectSpread(_objectSpread({}, current), {}, _defineProperty({}, area, _objectSpread(_objectSpread({}, existing), {}, {
liveResize: {
mode: mode,
width: width
}
}))) : current;
});
}, []);
var completeResize = useCallback(function (area, mode, width) {
setHasInteraction(true);
if (mode === 'overlay') {
var session = inputsRef.current.resizeSession;
setResizeSession(_objectSpread(_objectSpread({}, (session === null || session === void 0 ? void 0 : session.area) === area && session.overlayModes ? session : captureSiblingWidths(area, mode)), {}, {
completedWidth: width
}));
}
if (mode === 'inline') {
var _inputs$resizeSession, _inputs$areas$area;
var inputs = inputsRef.current;
var _session = ((_inputs$resizeSession = inputs.resizeSession) === null || _inputs$resizeSession === void 0 ? void 0 : _inputs$resizeSession.area) === area && (_inputs$areas$area = inputs.areas[area]) !== null && _inputs$areas$area !== void 0 && _inputs$areas$area.liveResize ? inputs.resizeSession : captureSiblingWidths(area);
var existing = inputs.areas[area];
if (inputs.dimensions && existing) {
// Keyboard completion has no preview; calculate it using the accepted
// request, not the parent's previous controlled width. Pointer completion
// keeps below-minimum cascade results even though its payload is clamped.
var completed = existing.liveResize ? areaStatesRef.current : calculateLayoutAreaStates(_objectSpread(_objectSpread({}, inputs.dimensions), {}, {
occupiedWidth: inputs.occupiedWidth,
mainMinWidth: inputs.mainMinWidth,
areas: _objectSpread(_objectSpread({}, inputs.areas), {}, _defineProperty({}, area, _objectSpread(_objectSpread({}, existing), {}, {
liveResize: {
mode: mode,
width: width
}
}))),
resizeSession: _session
}));
var completedWidths = {};
var _iterator2 = _createForOfIteratorHelper(['side-nav', 'panel', 'chat-panel']),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var _completed$name;
var name = _step2.value;
if (((_completed$name = completed[name]) === null || _completed$name === void 0 ? void 0 : _completed$name.mode) === 'inline') {
completedWidths[name] = completed[name].width;
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
setResizeSession(_objectSpread(_objectSpread({}, _session), {}, {
completedWidth: width,
completedWidths: completedWidths
}));
}
}
setAreas(function (current) {
var existing = current[area];
if (!existing) {
return current;
}
if (mode === 'overlay' && area !== 'panel') {
return _objectSpread(_objectSpread({}, current), {}, _defineProperty({}, area, _objectSpread(_objectSpread({}, existing), {}, {
liveResize: undefined,
preferredOverlayWidth: width
})));
}
return _objectSpread(_objectSpread({}, current), {}, _defineProperty({}, area, _objectSpread(_objectSpread({}, existing), {}, {
liveResize: undefined,
// Local panels have one preference, regardless of the resized mode.
preferredWidth: width
})));
});
}, [captureSiblingWidths]);
useEffect(function () {
// Loading the interaction engine is not a layout event. Adopt the CSS layout
// without changing widths or modes; subsequent changes to configuration or
// available space may redistribute it. Content stays mounted during the handoff.
var root = layoutRef === null || layoutRef === void 0 ? void 0 : layoutRef.current;
if (onUpdate && root && dimensions && isPreservingInitialLayout) {
var measured = {};
var selectors = {
'side-nav': ':scope > nav[data-layout-slot]',
panel: '[data-layout-with-panel-slot]',
'chat-panel': ':scope > [data-layout-chat-panel]'
};
var _iterator3 = _createForOfIteratorHelper(['side-nav', 'panel', 'chat-panel']),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var name = _step3.value;
var element = root.querySelector(selectors[name]);
var config = areas[name];
if (!element || !(config !== null && config !== void 0 && config.isOpen)) {
continue;
}
var style = getComputedStyle(element);
var mode = style.gridArea.split('/')[0].trim() === name ? 'inline' : 'overlay';
measured[name] = {
mode: mode,
width: element.getBoundingClientRect().width,
minWidth: resolveLayoutWidth(config.minWidth, dimensions.viewportWidth)
};
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
setInitialStates(measured);
}
}, [areas, dimensions, isPreservingInitialLayout, layoutRef, onUpdate]);
var areaStates = useMemo(function () {
var _initialStates$sideN, _initialStates$panel, _initialStates$chatP;
if (!dimensions) {
return {};
}
var allocated = calculateLayoutAreaStates(_objectSpread(_objectSpread({}, dimensions), {}, {
occupiedWidth: occupiedWidth,
mainMinWidth: mainMinWidth,
areas: areas,
resizeSession: resizeSession,
initialModes: onUpdate && layoutRef !== null && layoutRef !== void 0 && layoutRef.current && isPreservingInitialLayout ? {
'side-nav': (_initialStates$sideN = initialStates['side-nav']) === null || _initialStates$sideN === void 0 ? void 0 : _initialStates$sideN.mode,
panel: (_initialStates$panel = initialStates.panel) === null || _initialStates$panel === void 0 ? void 0 : _initialStates$panel.mode,
'chat-panel': (_initialStates$chatP = initialStates['chat-panel']) === null || _initialStates$chatP === void 0 ? void 0 : _initialStates$chatP.mode
} : undefined
}));
if (onUpdate && layoutRef !== null && layoutRef !== void 0 && layoutRef.current && isPreservingInitialLayout) {
var _iterator4 = _createForOfIteratorHelper(['side-nav', 'panel', 'chat-panel']),
_step4;
try {
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
var name = _step4.value;
if (initialStates[name] && allocated[name]) {
allocated[name] = _objectSpread(_objectSpread({}, allocated[name]), initialStates[name]);
} else {
delete allocated[name];
}
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
}
return allocated;
}, [areas, mainMinWidth, resizeSession, dimensions, occupiedWidth, isPreservingInitialLayout, initialStates, layoutRef, onUpdate]);
useEffect(function () {
var _areas$sideNav;
if (hasInteraction && (resizeSession === null || resizeSession === void 0 ? void 0 : resizeSession.area) !== 'side-nav' && (_areas$sideNav = areas['side-nav']) !== null && _areas$sideNav !== void 0 && _areas$sideNav.isOpen && !areaStates['side-nav'] && dimensions) {
var _areas$sideNav$onReq, _areas$sideNav2;
(_areas$sideNav$onReq = (_areas$sideNav2 = areas['side-nav']).onRequestClose) === null || _areas$sideNav$onReq === void 0 || _areas$sideNav$onReq.call(_areas$sideNav2);
}
}, [areaStates, areas, dimensions, hasInteraction, resizeSession]);
useEffect(function () {
areaStatesRef.current = areaStates;
}, [areaStates]);
// Read bounds lazily: changing a sibling's capacity must not publish new render
// state or recreate drag listeners for a region whose own width did not change.
var getResizeBounds = useCallback(function (area) {
var _areaStatesRef$curren;
var bounds = (_areaStatesRef$curren = areaStatesRef.current[area]) === null || _areaStatesRef$curren === void 0 ? void 0 : _areaStatesRef$curren.resizeBounds;
return bounds ? {
min: "".concat(bounds.min, "px"),
max: "".concat(bounds.max, "px")
} : undefined;
}, []);
var value = useMemo(function () {
return {
getResizeBounds: getResizeBounds,
registerArea: registerArea,
unregisterArea: unregisterArea,
startResize: startResize,
resize: resize,
completeResize: completeResize,
setMainMinWidth: setMainMinWidth
};
}, [completeResize, getResizeBounds, registerArea, resize, startResize, unregisterArea]);
var sideNavState = useStableAreaState(areaStates['side-nav']);
var panelState = useStableAreaState(areaStates.panel);
var chatPanelState = useStableAreaState(areaStates['chat-panel']);
var SideNavStateProvider = layoutAreaStateContexts['side-nav'].Provider;
var PanelStateProvider = layoutAreaStateContexts.panel.Provider;
var ChatPanelStateProvider = layoutAreaStateContexts['chat-panel'].Provider;
useEffect(function () {
onReady === null || onReady === void 0 || onReady(value);
return function () {
return onReady === null || onReady === void 0 ? void 0 : onReady(null);
};
}, [onReady, value]);
useEffect(function () {
if (!adoptedConstraints.current && (sideNavState || panelState || chatPanelState)) {
adoptedConstraints.current = {
occupiedWidth: occupiedWidth,
mainMinWidth: mainMinWidth
};
}
onUpdate === null || onUpdate === void 0 || onUpdate({
'side-nav': sideNavState,
panel: panelState,
'chat-panel': chatPanelState
});
}, [onUpdate, sideNavState, panelState, chatPanelState, occupiedWidth, mainMinWidth]);
return /*#__PURE__*/React.createElement(LayoutAreaSizingContext.Provider, {
value: value
}, /*#__PURE__*/React.createElement(SideNavStateProvider, {
value: sideNavState
}, /*#__PURE__*/React.createElement(PanelStateProvider, {
value: panelState
}, /*#__PURE__*/React.createElement(ChatPanelStateProvider, {
value: chatPanelState
}, children))));
}