UNPKG

@atlaskit/navigation-system

Version:

The latest navigation system for Atlassian apps.

803 lines (781 loc) • 40.3 kB
/* 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 }); };