@atlaskit/navigation-system
Version:
The latest navigation system for Atlassian apps.
803 lines (781 loc) • 40.3 kB
JavaScript
/* panel-splitter.tsx generated by @compiled/babel-plugin v3.0.2 */
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PanelSplitter = void 0;
require("./panel-splitter.compiled.css");
var _react = _interopRequireWildcard(require("react"));
var React = _react;
var _runtime = require("@compiled/react/runtime");
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _reactDom = require("react-dom");
var _bindEventListener = require("bind-event-listener");
var _tinyInvariant = _interopRequireDefault(require("tiny-invariant"));
var _useId = require("@atlaskit/ds-lib/use-id");
var _useStableRef = _interopRequireDefault(require("@atlaskit/ds-lib/use-stable-ref"));
var _useOpenLayerObserver = require("@atlaskit/layering/use-open-layer-observer");
var _fg = require("@atlaskit/platform-feature-flags/fg");
var _elementAdapter = require("@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter");
var _blockDraggingToIframes = require("@atlaskit/pragmatic-drag-and-drop/utils/block-dragging-to-iframes");
var _combine = require("@atlaskit/pragmatic-drag-and-drop/utils/combine");
var _disableNativeDragPreview = require("@atlaskit/pragmatic-drag-and-drop/utils/disable-native-drag-preview");
var _preventUnhandled = require("@atlaskit/pragmatic-drag-and-drop/utils/prevent-unhandled");
var _Tooltip = _interopRequireDefault(require("@atlaskit/tooltip/Tooltip"));
var _TooltipContainer = _interopRequireDefault(require("@atlaskit/tooltip/TooltipContainer"));
var _requestClosePopovers = require("@atlaskit/top-layer/page-surface/request-close-popovers");
var _visuallyHidden = _interopRequireDefault(require("@atlaskit/visually-hidden/visually-hidden"));
var _constants = require("../constants");
var _layoutAreaSizingContext = require("../layout-area-sizing-context");
var _convertResizeBoundToPixels = require("./convert-resize-bound-to-pixels");
var _getPanelGrowthSide = require("./get-panel-growth-side");
var _getPercentageWithinPixelBounds = require("./get-percentage-within-pixel-bounds");
var _getPixelWidth = require("./get-pixel-width");
var _getWidthFromDragLocation = require("./get-width-from-drag-location");
var _isPanelSplitterDragData = require("./is-panel-splitter-drag-data");
var _keyboardResizeManager = require("./keyboard-resize-manager");
var _onDoubleClickContext = require("./on-double-click-context");
var _panelSplitterContext = require("./panel-splitter-context");
var _panelSplitterDragSymbol = require("./panel-splitter-drag-symbol");
var _excluded = ["children", "className"];
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t6 in e) "default" !== _t6 && {}.hasOwnProperty.call(e, _t6) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t6)) && (i.get || i.set) ? o(f, _t6, i) : f[_t6] = e[_t6]); return f; })(e, t); }
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) { (0, _defineProperty2.default)(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; }
function signPanelSplitterDragData(data) {
return _objectSpread(_objectSpread({}, data), {}, (0, _defineProperty2.default)({}, _panelSplitterDragSymbol.panelSplitterDragDataSymbol, true));
}
var containerStyles = {
root: "_12ji1r31 _1qu2glyw _12y31o36 _1e0cglyw _kqswstnw _u7coidpf _152tidpf _1pbykb7n _181n1ule",
positionEnd: "_rjxpftgi",
positionStart: "_1e02zeo2"
};
var grabAreaStyles = {
root: "_1bsbl52n _4t3i1osq _kqswstnw _1q51ze3t _y4tize3t _85i5ze3t _bozgze3t _syaz1j28 _bfhk1j28 _1oec6ebc _1lww188d _6fl41ytf _k8m01ylx _d0aluf7j _vp7g6x5g _30l36x5g _1gglglyw _9h8h12zz",
rootFinesse: "_vp7g1ihb _30l31ihb _9h8hq98m"
};
var lineStyles = {
root: "_kqswstnw _1e0c1ule _1bsb1l7b _4t3i1osq _syaz1kw7 _bfhk1r31 _1e021v6z"
};
var tooltipStyles = {
root: "_ahbq196n _1bsb1ris",
placedLeft: "_ahbqze3t _12l2196n"
};
var PanelSplitterTooltip = /*#__PURE__*/(0, _react.forwardRef)(function (_ref, ref) {
var children = _ref.children,
className = _ref.className,
props = (0, _objectWithoutProperties2.default)(_ref, _excluded);
var style = (0, _react.useMemo)(function () {
if (!props.style || !props.style.transform) {
return props.style;
}
// We cannot just match against integers safely, as browsers may introduce non-integer values due to scaling
// In the future we can probably use `CSSStyleValue.parse()` to get the browser to handle parsing for us,
// but there's no Firefox support yet.
var _props$style$transfor = props.style.transform.matchAll(/(?:-|\+)?\d*\.?\d+px/g),
_props$style$transfor2 = (0, _slicedToArray2.default)(_props$style$transfor, 2),
translateX = _props$style$transfor2[0],
translateY = _props$style$transfor2[1];
if (!translateY) {
// If we can't extract the translateY value we bail out and return the original style
return props.style;
}
/**
* Adjusts the translate Y to keep the tooltip within the main content area,
* so that it does not appear over the banner or top navigation.
*/
var newTranslateY = "max(calc(".concat(_constants.contentInsetBlockStart, " + ", "var(--ds-space-100, 8px)", "), ").concat(translateY, ")");
var newTransform = "translate3d(".concat(translateX, ", ").concat(newTranslateY, ", 0)");
return _objectSpread(_objectSpread({}, props.style), {}, {
transform: newTransform
});
}, [props.style]);
return /*#__PURE__*/React.createElement(_TooltipContainer.default, (0, _extends2.default)({}, props, {
ref: ref
// Must be statically passed
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop, @atlaskit/design-system/no-unsafe-style-overrides
,
className: (0, _runtime.ax)([tooltipStyles.root, props.placement === 'left' && tooltipStyles.placedLeft, className])
// eslint-disable-next-line @atlaskit/design-system/no-unsafe-style-overrides
,
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
style: style
}), children);
});
/**
* A wrapper component that renders a tooltip if the tooltipContent or shortcut is provided.
*/
var MaybeTooltip = function MaybeTooltip(_ref2) {
var tooltipContent = _ref2.tooltipContent,
shortcut = _ref2.shortcut,
mousePosition = _ref2.mousePosition,
children = _ref2.children,
testId = _ref2.testId;
if (tooltipContent) {
return /*#__PURE__*/React.createElement(_Tooltip.default, {
testId: testId,
content: tooltipContent,
shortcut: shortcut,
position: "mouse-y",
mousePosition: mousePosition,
isScreenReaderAnnouncementDisabled: true,
component: PanelSplitterTooltip,
shouldAlwaysFadeIn: true,
shouldRenderToParent: true
}, children);
}
return children;
};
// Browsers increase an RTL range input's value on ArrowLeft, so the arrow key pointing towards the
// panel's growth side widens it.
var rangeInputDirs = {
left: 'rtl',
right: 'ltr'
};
function getTextDirection(element) {
var _window$getComputedSt = window.getComputedStyle(element),
direction = _window$getComputedSt.direction;
return direction === 'rtl' ? 'rtl' : 'ltr';
}
var PortaledPanelSplitter = function PortaledPanelSplitter(_ref3) {
var _useContext;
var label = _ref3.label,
onResizeStart = _ref3.onResizeStart,
onResizeEnd = _ref3.onResizeEnd,
testId = _ref3.testId,
panelId = _ref3.panelId,
panelWidth = _ref3.panelWidth,
onCompleteResize = _ref3.onCompleteResize,
onResizeStartInternal = _ref3.onResizeStartInternal,
onResizeInternal = _ref3.onResizeInternal,
prepareResize = _ref3.prepareResize,
getResizeBounds = _ref3.getResizeBounds,
panel = _ref3.panel,
portal = _ref3.portal,
resizingCssVar = _ref3.resizingCssVar,
resizingElementRef = _ref3.resizingElementRef,
position = _ref3.position,
tooltipContent = _ref3.tooltipContent,
shortcut = _ref3.shortcut;
var splitterRef = (0, _react.useRef)(null);
var registerSplitter = (_useContext = (0, _react.useContext)(_layoutAreaSizingContext.LayoutAreaSizingContext)) === null || _useContext === void 0 ? void 0 : _useContext.registerSplitter;
var unregisterSplitter = (0, _react.useRef)(undefined);
var setSplitterRef = (0, _react.useCallback)(function (element) {
var _unregisterSplitter$c;
(_unregisterSplitter$c = unregisterSplitter.current) === null || _unregisterSplitter$c === void 0 || _unregisterSplitter$c.call(unregisterSplitter);
splitterRef.current = element;
unregisterSplitter.current = element && prepareResize ? registerSplitter === null || registerSplitter === void 0 ? void 0 : registerSplitter(element) : undefined;
}, [prepareResize, registerSplitter]);
var labelId = (0, _useId.useId)();
var panelGrowthSide = (0, _react.useMemo)(function () {
return (0, _fg.fg)('platform-dst-panel-splitter-resize-direction') ? (0, _getPanelGrowthSide.getPanelGrowthSide)({
position: position,
textDirection: getTextDirection(panel)
}) : undefined;
}, [position, panel]);
// Separate state used for the input range width to remove the UI's dependency on the "persisted" layout state value being updated
var _useState = (0, _react.useState)(panelWidth),
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
rangeInputValue = _useState2[0],
setRangeInputValue = _useState2[1];
/**
* We are using state to store the resize bounds _converted into pixel units_, so that we can update them when the window is resized.
* Otherwise they can become out of sync with the actual viewport size, and cause a broken experience (inaccurate width percentage)
* when the user tries to resize using the keyboard.
*/
var _useState3 = (0, _react.useState)({
// Using placeholder values here for the initial render. These will be calculated and updated when the range input is focused.
min: 200,
max: 500
}),
_useState4 = (0, _slicedToArray2.default)(_useState3, 2),
rangeInputBounds = _useState4[0],
setRangeInputBounds = _useState4[1];
var openLayerObserver = (0, _useOpenLayerObserver.useOpenLayerObserver)({
isEnabled: !(0, _fg.fg)('platform-dst-top-layer')
});
/**
* This is a temporary workaround to enable the `SideNavPanelSplitter` component
* to collapse the side nav on double click, without exposing the `onDoubleClick` prop on `PanelSplitter`.
* Once `PanelSplitter` supports an `onDoubleClick` prop directly, this context can be removed.
*/
var onDoubleClick = (0, _react.useContext)(_onDoubleClickContext.OnDoubleClickContext);
// Storing the initial `clientX` on `mousedown` events, to workaround a bug caused by some browser extensions
// where the `dragstart` event incorrectly returns `0` for the `clientX` location.
var initialClientXRef = (0, _react.useRef)(null);
// Live allocation changes panelWidth on every frame. Only the legacy path should
// rebind the drag listeners when this value changes.
var legacyPanelWidth = (0, _fg.fg)('platform-dst-chat-panel-layout') || onResizeInternal ? undefined : panelWidth;
(0, _react.useEffect)(function () {
var splitter = splitterRef.current;
(0, _tinyInvariant.default)(splitter, 'Splitter ref must be set');
var lastManagedResizeWidth;
return (0, _combine.combine)((0, _blockDraggingToIframes.blockDraggingToIFrames)({
element: splitter
}),
/**
* Capturing the initial `clientX` from the mousedown, before the drag starts.
*
* ⚠️ Note: We are not using pragmatic-drag-and-drop's `onDragStart` for this as some browser
* extensions can cause the client location (e.g. clientX) to incorrectly return 0 during the `dragstart` event.
*
* We are also not using pragmatic-drag-and-drop's `onDrag` here as it is throttled, which means
* fast mouse movements can cause the real first drag event to be missed, causing a different clientX
* to be captured.
*
* I also tried only binding an event listener inside pragmatic-drag-and-drop's `onDragStart`, which seemd to work
* but did not feel as robust, and might have timing issues as it happens slightly later.
*/
(0, _bindEventListener.bind)(splitter, {
type: 'mousedown',
listener: function listener(event) {
initialClientXRef.current = event.clientX;
}
}), (0, _elementAdapter.draggable)({
element: splitter,
onGenerateDragPreview: function onGenerateDragPreview(_ref4) {
var nativeSetDragImage = _ref4.nativeSetDragImage;
// We will be moving the line to indicate a drag. We can disable the native drag preview
(0, _disableNativeDragPreview.disableNativeDragPreview)({
nativeSetDragImage: nativeSetDragImage
});
// We don't want any native drop animation for when the user does not drop on a drop target. We want the drag to finish immediately
_preventUnhandled.preventUnhandled.start();
},
getInitialData: function getInitialData() {
var initialWidth = (0, _getPixelWidth.getPixelWidth)(panel);
/**
* The drag calculations require the actual computed width of the element
* For example, if the panel has loaded with a width of 2000px, but the max bound is 1000px and is visually 1000px (due to the CSS `clamp()`),
* the drag calculations should use the actual width of 1000px, not the width in state of 2000px.
*/
return signPanelSplitterDragData({
panelId: panelId,
initialWidth: initialWidth,
resizingWidth: "".concat(initialWidth, "px"),
resizeBounds: getResizeBounds(),
// Only computing text direction when we need it, just as the drag is starting.
// Recomputing text direction on each new drag in case the text direction
// has changed. This is unlikely, but being safe.
direction: getTextDirection(panel)
});
},
onDragStart: function onDragStart(_ref5) {
var source = _ref5.source;
/**
* ⚠️ Note: We are not using the client locations (e.g. clientX) during `onDragStart`
* because some browser extensions can cause the event properties to incorrectly return 0.
*/
(0, _tinyInvariant.default)((0, _isPanelSplitterDragData.isPanelSplitterDragData)(source.data));
var resizeStartData = {
initialWidth: source.data.initialWidth
};
// The layout allocation system needs to know which area is actively being
// resized before the first drag update. Committing this synchronously keeps
// the actively dragged area protected from automatic compression.
if (onResizeStartInternal) {
(0, _reactDom.flushSync)(function () {
return onResizeStartInternal(resizeStartData);
});
}
onResizeStart === null || onResizeStart === void 0 || onResizeStart(resizeStartData);
// Close any open layers when the user starts resizing
// Not gated. Top-layer tooltips (`platform-dst-top-layer-tooltip`) are not known to the observer.
(0, _requestClosePopovers.requestClosePopovers)();
openLayerObserver === null || openLayerObserver === void 0 || openLayerObserver.closeLayers();
},
onDrag: function onDrag(_ref6) {
var _initialClientXRef$cu;
var location = _ref6.location,
source = _ref6.source;
/**
* ⚠️ Note: We are not using the location.initial.input client locations because some browser extensions
* can cause the client locations (e.g. clientX) in the `dragstart` event to incorrectly return 0.
*/
(0, _tinyInvariant.default)((0, _isPanelSplitterDragData.isPanelSplitterDragData)(source.data));
var _source$data = source.data,
initialWidth = _source$data.initialWidth,
resizeBounds = _source$data.resizeBounds,
direction = _source$data.direction;
(0, _tinyInvariant.default)(initialClientXRef.current !== null, 'initialClientX must be set');
/**
* How wide the element would be if there were no width constraints,
* based on the pointer's position.
*/
var targetWidth = (0, _getWidthFromDragLocation.getWidthFromDragLocation)({
initialWidth: initialWidth,
location: location,
// The fallback of 0 won't be used due to the invariant, however we require one to satisfy the type.
initialClientX: (_initialClientXRef$cu = initialClientXRef.current) !== null && _initialClientXRef$cu !== void 0 ? _initialClientXRef$cu : 0,
direction: direction,
position: position
});
var resizingWidth = "clamp(".concat(resizeBounds.min, ", ").concat(targetWidth, "px, ").concat(resizeBounds.max, ")");
if (onResizeInternal) {
// The allocator needs below-minimum requests for the local-to-chat cascade.
var currentBounds = getResizeBounds();
var width = Math.max(0, Math.min(targetWidth, prepareResize ? window.innerWidth : (0, _convertResizeBoundToPixels.convertResizeBoundToPixels)(currentBounds.max)));
if (width !== lastManagedResizeWidth) {
lastManagedResizeWidth = width;
onResizeInternal(width);
}
} else {
var _resizingElementRef$c;
panel.style.setProperty(resizingCssVar, resizingWidth);
resizingElementRef === null || resizingElementRef === void 0 || (_resizingElementRef$c = resizingElementRef.current) === null || _resizingElementRef$c === void 0 || _resizingElementRef$c.style.setProperty(resizingCssVar, resizingWidth);
}
source.data.resizingWidth = resizingWidth;
},
onDrop: function onDrop(_ref7) {
return (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
var _resizingElementRef$c2;
var source, width, finalWidth, _t;
return _regenerator.default.wrap(function (_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
source = _ref7.source;
(0, _tinyInvariant.default)((0, _isPanelSplitterDragData.isPanelSplitterDragData)(source.data));
_preventUnhandled.preventUnhandled.stop();
if (!prepareResize) {
_context.next = 3;
break;
}
_context.next = 1;
return prepareResize();
case 1:
_t = !_context.sent;
if (_t) {
_context.next = 2;
break;
}
_t = !panel.isConnected;
case 2:
if (!_t) {
_context.next = 3;
break;
}
return _context.abrupt("return");
case 3:
// PDD may deliver its final throttled drag update immediately before drop.
// Commit that update once before measuring; do not persist a previous frame.
if (onResizeInternal && lastManagedResizeWidth !== undefined) {
width = lastManagedResizeWidth;
(0, _reactDom.flushSync)(function () {
return onResizeInternal(width);
});
lastManagedResizeWidth = undefined;
}
finalWidth = (0, _getPixelWidth.getPixelWidth)(panel); // Persist the final preferred width before removing the imperative drag
// variable, so the rendered width is continuous across pointer release.
if ((0, _fg.fg)('platform-dst-chat-panel-layout') || onResizeInternal) {
(0, _reactDom.flushSync)(function () {
return onCompleteResize(finalWidth);
});
} else {
onCompleteResize(finalWidth);
}
onResizeEnd === null || onResizeEnd === void 0 || onResizeEnd({
initialWidth: source.data.initialWidth,
finalWidth: finalWidth
});
panel.style.removeProperty(resizingCssVar);
resizingElementRef === null || resizingElementRef === void 0 || (_resizingElementRef$c2 = resizingElementRef.current) === null || _resizingElementRef$c2 === void 0 || _resizingElementRef$c2.style.removeProperty(resizingCssVar);
case 4:
case "end":
return _context.stop();
}
}, _callee);
}))();
}
}));
}, [onCompleteResize, onResizeStartInternal, onResizeInternal, prepareResize, onResizeStart, onResizeEnd, panel, resizingCssVar, resizingElementRef, legacyPanelWidth, position, openLayerObserver, panelId, getResizeBounds]);
var onResizeStartStableRef = (0, _useStableRef.default)(onResizeStart);
var onResizeEndStableRef = (0, _useStableRef.default)(onResizeEnd);
var _useState5 = (0, _react.useState)(function () {
return (0, _keyboardResizeManager.createKeyboardResizeManager)({
onResizeStart: function onResizeStart() {
var _onResizeStartStableR;
// Close any open layers when the user starts resizing with keyboard
// Not gated. Top-layer tooltips (`platform-dst-top-layer-tooltip`) are not known to the observer.
(0, _requestClosePopovers.requestClosePopovers)();
openLayerObserver === null || openLayerObserver === void 0 || openLayerObserver.closeLayers();
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
(_onResizeStartStableR = onResizeStartStableRef.current) === null || _onResizeStartStableR === void 0 || _onResizeStartStableR.call.apply(_onResizeStartStableR, [onResizeStartStableRef].concat(args));
},
onResizeEnd: function onResizeEnd() {
var _onResizeEndStableRef;
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return (_onResizeEndStableRef = onResizeEndStableRef.current) === null || _onResizeEndStableRef === void 0 ? void 0 : _onResizeEndStableRef.call.apply(_onResizeEndStableRef, [onResizeEndStableRef].concat(args));
}
});
}),
_useState6 = (0, _slicedToArray2.default)(_useState5, 1),
keyboardResizeManager = _useState6[0];
var focusSequence = (0, _react.useRef)(0);
var keyboardBoundsReady = (0, _react.useRef)(false);
var pendingKeyboardWidth = (0, _react.useRef)(null);
var handleSliderInputChange = (0, _react.useCallback)( /*#__PURE__*/function () {
var _ref8 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(event, pendingKey) {
var isManagedResize, sequence, value, bounds, min, max, _pendingKeyboardWidth, current, doesArrowRightIncrease, increase, step, _t2, _t3;
return _regenerator.default.wrap(function (_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
isManagedResize = onResizeInternal;
sequence = focusSequence.current;
value = isManagedResize ? parseFloat(event.currentTarget.value) : parseInt(event.currentTarget.value);
if (!prepareResize) {
_context2.next = 4;
break;
}
_context2.next = 1;
return prepareResize();
case 1:
_t3 = !_context2.sent;
if (_t3) {
_context2.next = 2;
break;
}
_t3 = !panel.isConnected;
case 2:
_t2 = _t3;
if (_t2) {
_context2.next = 3;
break;
}
_t2 = sequence !== focusSequence.current;
case 3:
if (!_t2) {
_context2.next = 4;
break;
}
return _context2.abrupt("return");
case 4:
if (isManagedResize) {
// The available region may have changed since focus, including without a
// window resize. Use the allocator's current bounds for the public payload.
bounds = getResizeBounds();
min = (0, _convertResizeBoundToPixels.convertResizeBoundToPixels)(bounds.min);
max = (0, _convertResizeBoundToPixels.convertResizeBoundToPixels)(bounds.max);
if (pendingKey) {
// Native range inputs would apply these keys to the placeholder bounds.
// Preserve the intent until the chunk provides the real bounds instead.
current = (_pendingKeyboardWidth = pendingKeyboardWidth.current) !== null && _pendingKeyboardWidth !== void 0 ? _pendingKeyboardWidth : (0, _getPixelWidth.getPixelWidth)(panel);
doesArrowRightIncrease = panelGrowthSide ? panelGrowthSide === 'right' : getTextDirection(panel) === 'ltr';
increase = pendingKey === 'ArrowUp' || pendingKey === (doesArrowRightIncrease ? 'ArrowRight' : 'ArrowLeft') || pendingKey === 'PageUp';
step = pendingKey.startsWith('Page') ? Math.max(20, (max - min) / 10) : 20;
value = pendingKey === 'End' ? max : pendingKey === 'Home' ? min : current + (increase ? step : -step);
}
value = Math.max(min, Math.min(value, max));
setRangeInputBounds({
min: min,
max: max
});
}
pendingKeyboardWidth.current = value;
setRangeInputValue(value);
/**
* We are intentionally _not_ debouncing `onCompleteResize` as we want it to be called anytime the user changes the range input value.
* It is an internal-only callback - it's provided by `PanelSplitterProvider`, and used by layout slots to update their layout width state.
*/
onCompleteResize(value);
keyboardResizeManager.onResize({
initialWidth: panelWidth,
finalWidth: value
});
case 5:
case "end":
return _context2.stop();
}
}, _callee2);
}));
return function (_x, _x2) {
return _ref8.apply(this, arguments);
};
}(), [getResizeBounds, onCompleteResize, onResizeInternal, panelWidth, keyboardResizeManager, panel, prepareResize, panelGrowthSide]);
var handleSliderKeyDown = (0, _react.useCallback)(function (event) {
if (prepareResize && !keyboardBoundsReady.current && ['Home', 'End', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'].includes(event.key)) {
event.preventDefault();
void handleSliderInputChange(event, event.key);
}
}, [handleSliderInputChange, prepareResize]);
var resizeEventListenerCleanupFn = (0, _react.useRef)(null);
var handleSliderFocus = (0, _react.useCallback)( /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() {
var _resizeEventListenerC;
var sequence, width, resizeBounds, _t4, _t5;
return _regenerator.default.wrap(function (_context3) {
while (1) switch (_context3.prev = _context3.next) {
case 0:
sequence = ++focusSequence.current;
keyboardBoundsReady.current = false;
pendingKeyboardWidth.current = null;
if (!prepareResize) {
_context3.next = 4;
break;
}
_context3.next = 1;
return prepareResize();
case 1:
_t5 = !_context3.sent;
if (_t5) {
_context3.next = 2;
break;
}
_t5 = !panel.isConnected;
case 2:
_t4 = _t5;
if (_t4) {
_context3.next = 3;
break;
}
_t4 = sequence !== focusSequence.current;
case 3:
if (!_t4) {
_context3.next = 4;
break;
}
return _context3.abrupt("return");
case 4:
/**
* We are only updating the range input's properties when the element is focused, as a performance optimisation.
* It isn't necessary to recalculate these values until the element is going to be used.
*/
// Clean up any left over listeners in case `focus` was triggered again without `blur`.
(_resizeEventListenerC = resizeEventListenerCleanupFn.current) === null || _resizeEventListenerC === void 0 || _resizeEventListenerC.call(resizeEventListenerCleanupFn);
// Update the range input value to match the current panel width.
width = prepareResize ? (0, _getPixelWidth.getPixelWidth)(panel) : panelWidth;
pendingKeyboardWidth.current = width;
setRangeInputValue(width);
resizeBounds = getResizeBounds(); // Update the range input bounds to ensure they are accurate (in case the viewport was previously resized)
setRangeInputBounds({
min: (0, _convertResizeBoundToPixels.convertResizeBoundToPixels)(resizeBounds.min),
max: (0, _convertResizeBoundToPixels.convertResizeBoundToPixels)(resizeBounds.max)
});
keyboardBoundsReady.current = true;
/**
* _While the slider element is focused_, add a window resize event listener to update the range input bounds
* when the window is resized. This is needed to ensure the min and max widths accurately reflect the viewport
* size - in case they are provided in vw units.
*
* This listener is cleaned up when the element is blurred.
*/
resizeEventListenerCleanupFn.current = (0, _bindEventListener.bind)(window, {
type: 'resize',
listener: function updateState() {
var resizeBounds = getResizeBounds();
setRangeInputBounds({
min: (0, _convertResizeBoundToPixels.convertResizeBoundToPixels)(resizeBounds.min),
max: (0, _convertResizeBoundToPixels.convertResizeBoundToPixels)(resizeBounds.max)
});
}
});
case 5:
case "end":
return _context3.stop();
}
}, _callee3);
})), [panelWidth, getResizeBounds, panel, prepareResize]);
var handleSliderBlur = (0, _react.useCallback)(function () {
var _resizeEventListenerC2;
focusSequence.current++;
keyboardBoundsReady.current = false;
// Remove the resize event listener when the range input is blurred.
(_resizeEventListenerC2 = resizeEventListenerCleanupFn.current) === null || _resizeEventListenerC2 === void 0 || _resizeEventListenerC2.call(resizeEventListenerCleanupFn);
}, []);
(0, _react.useEffect)(function () {
return function cleanup() {
var _resizeEventListenerC3;
// Cleanup the resize event listener when the component is unmounted.
(_resizeEventListenerC3 = resizeEventListenerCleanupFn.current) === null || _resizeEventListenerC3 === void 0 || _resizeEventListenerC3.call(resizeEventListenerCleanupFn);
};
}, []);
var ariaValueText = (0, _react.useMemo)(function () {
return "".concat((0, _getPercentageWithinPixelBounds.getPercentageWithinPixelBounds)({
currentWidth: rangeInputValue,
resizeBounds: rangeInputBounds
}), "% width");
}, [rangeInputValue, rangeInputBounds]);
return /*#__PURE__*/(0, _reactDom.createPortal)( /*#__PURE__*/React.createElement("div", {
"data-testid": testId ? "".concat(testId, "-container") : undefined,
className: (0, _runtime.ax)([containerStyles.root, position === 'start' && containerStyles.positionStart, position === 'end' && containerStyles.positionEnd])
}, /*#__PURE__*/React.createElement(MaybeTooltip, {
tooltipContent: tooltipContent,
shortcut: shortcut,
mousePosition: panelGrowthSide !== null && panelGrowthSide !== void 0 ? panelGrowthSide : 'right',
testId: testId ? "".concat(testId, "-tooltip") : undefined
}, /*#__PURE__*/React.createElement("div", {
ref: setSplitterRef,
"data-testid": testId,
onDoubleClick: onDoubleClick,
className: (0, _runtime.ax)([grabAreaStyles.root, (0, _fg.fg)('platform-dst-tokens-finesse') && grabAreaStyles.rootFinesse])
}, /*#__PURE__*/React.createElement(_visuallyHidden.default, null, /*#__PURE__*/React.createElement("input", {
type: "range",
dir: panelGrowthSide ? rangeInputDirs[panelGrowthSide] : undefined,
value: rangeInputValue,
step: 20,
min: rangeInputBounds.min,
max: rangeInputBounds.max,
"aria-valuetext": ariaValueText,
"aria-labelledby": labelId,
onChange: handleSliderInputChange,
onKeyDown: prepareResize ? handleSliderKeyDown : undefined,
onFocus: handleSliderFocus,
onBlur: handleSliderBlur
}), /*#__PURE__*/React.createElement("span", {
id: labelId
}, label)), /*#__PURE__*/React.createElement("span", {
className: (0, _runtime.ax)([lineStyles.root])
})))), portal);
};
/**
* _PanelSplitter_
*
* A component that allows the user to resize a layout area.
* It can be used within layout areas like `Panel`, and `Aside`.
* For the `SideNav`, use the `SideNavPanelSplitter` component instead, as it provides additional functionality
* such as double clicking to collapse the side nav.
*
* Example usage:
* ```tsx
* <Aside>
* <!-- other side nav content -->
* <PanelSplitter label="Resize Side Nav" />
* </Aside>
* ```
*/
var PanelSplitter = exports.PanelSplitter = function PanelSplitter(_ref0) {
var label = _ref0.label,
onResizeStart = _ref0.onResizeStart,
onResizeEnd = _ref0.onResizeEnd,
testId = _ref0.testId,
tooltipContent = _ref0.tooltipContent;
var _useState7 = (0, _react.useState)(null),
_useState8 = (0, _slicedToArray2.default)(_useState7, 2),
panel = _useState8[0],
setPanel = _useState8[1];
var _useState9 = (0, _react.useState)(null),
_useState0 = (0, _slicedToArray2.default)(_useState9, 2),
portal = _useState0[0],
setPortal = _useState0[1];
var context = (0, _react.useContext)(_panelSplitterContext.PanelSplitterContext);
(0, _tinyInvariant.default)(context, 'Panel splitter context not set');
var panelRef = context.panelRef,
portalRef = context.portalRef,
isEnabled = context.isEnabled,
panelId = context.panelId,
panelWidth = context.panelWidth,
onCompleteResize = context.onCompleteResize,
onResizeStartInternal = context.onResizeStartInternal,
onResizeInternal = context.onResizeInternal,
getResizeBounds = context.getResizeBounds,
prepareResize = context.prepareResize,
resizingCssVar = context.resizingCssVar,
resizingElementRef = context.resizingElementRef,
position = context.position,
shortcut = context.shortcut;
/**
* **Explanation**
*
* _How React Suspense works_
*
* When a component is suspended:
*
* - it is _not_ "unmounted" and effects are _not_ cleaned up
* - refs to react managed elements are cleared
* - react adds `display:none !important` as an inline style to hide the element
* - state is preserved
*
* When a suspended component is resumed:
* - `display:none !important` inline style is removed
* - it will re-render (without the refs set)
* - refs will be set
* - `useEffect` will _not_ re-run
*
* [More details](https://x.com/alexandereardon/status/1944617494569992440)
*
* _What does this mean for the panel splitter?_
*
* - The panel splitter renders into content into a react managed element (eg managed by Aside)
* - When UI is suspended the portal ref is cleared
* - After a suspense, we will get a render with cleared refs
* - 🔥 This is problematic if we want to portal into that ref during the render
*
* _Approach: initial render_
*
* - We cannot portal the panel splitter until after the refs are initially set
* - Effects run after refs being set. In an effect we set the ref values (which are populated),
* trigger a re-render and then portal into the refs.
*
* _Approach: suspense_
*
* - We put the refs into react `state` after every render
* - The refs in `state` will not be cleared by suspending as effects are not run, or cleaned up,
* when components are suspended / resumed.
* - After the post suspense render, we can use the `portalRef` (which was captured in an effect)
* to continue to portal into.
*
* _Why not store `portalRef` in `state` in `<PanelSplitterProvider>`?_
*
* - The `portalRef` is set high in the tree, so a re-render will cause a
* large amount of components to re-render (eg the whole sidebar)
* - After a suspense, we would need to do _another_ large re-render to propagate the new refs
* - With our current approach, any changes to `portalRef` will cause a re-render low in the tree
* (just in the `<PanelSplitter>`)
*/
// We want this effect to run after each render (see above)
// eslint-disable-next-line react-hooks/exhaustive-deps
(0, _react.useEffect)(function () {
// Don't need to do anything
if (!isEnabled) {
return;
}
// It is safe to access refs in useEffect, even with Suspense
var panelEl = panelRef.current;
var portalEl = portalRef.current;
(0, _tinyInvariant.default)(panelEl, 'panelRef not set');
(0, _tinyInvariant.default)(portalEl, 'portal element not set');
// Doing our own check, as this results in fewer re-renders than react
// (which also does it's own checks to see if a re-render should occur)
if (panelEl === panel && portalEl === portal) {
return;
}
// If refs don't change, react won't cause infinite loops
setPanel(panelEl);
setPortal(portalEl);
});
if (!isEnabled) {
return null;
}
if (!portal || !panel) {
return null;
}
return /*#__PURE__*/React.createElement(PortaledPanelSplitter, {
label: label,
onResizeStart: onResizeStart,
onResizeEnd: onResizeEnd,
testId: testId,
panelId: panelId,
panel: panel,
portal: portal,
panelWidth: panelWidth,
onCompleteResize: onCompleteResize,
onResizeStartInternal: onResizeStartInternal,
onResizeInternal: onResizeInternal,
prepareResize: prepareResize,
getResizeBounds: getResizeBounds,
resizingCssVar: resizingCssVar,
resizingElementRef: resizingElementRef,
position: position,
tooltipContent: tooltipContent,
shortcut: shortcut
});
};