|
@@ -1,44 +1,22 @@
|
|
|
-import {DOMAttributes, ReactNode, useCallback, useRef, useState} from 'react';
|
|
|
+import {DOMAttributes, ReactNode, useCallback} from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
+import debounce from 'lodash/debounce';
|
|
|
|
|
|
import {IconGrabbable} from 'sentry/icons';
|
|
|
import {space} from 'sentry/styles/space';
|
|
|
-import useMouseTracking from 'sentry/utils/replays/hooks/useMouseTracking';
|
|
|
import useSplitPanelTracking from 'sentry/utils/replays/hooks/useSplitPanelTracking';
|
|
|
-import useTimeout from 'sentry/utils/useTimeout';
|
|
|
-
|
|
|
-const BAR_THICKNESS = 16;
|
|
|
-const HALF_BAR = BAR_THICKNESS / 2;
|
|
|
-
|
|
|
-const MOUSE_RELEASE_TIMEOUT_MS = 750;
|
|
|
-
|
|
|
-type CSSValuePX = `${number}px`;
|
|
|
-type CSSValuePct = `${number}%`;
|
|
|
-type CSSValueFR = '1fr';
|
|
|
-type CSSValue = CSSValuePX | CSSValuePct | CSSValueFR;
|
|
|
-type LimitValue =
|
|
|
- | {
|
|
|
- /**
|
|
|
- * Percent, as a value from `0` to `1.0`
|
|
|
- */
|
|
|
- pct: number;
|
|
|
- }
|
|
|
- | {
|
|
|
- /**
|
|
|
- * CSS pixels
|
|
|
- */
|
|
|
- px: number;
|
|
|
- };
|
|
|
+import {useResizableDrawer} from 'sentry/utils/useResizableDrawer';
|
|
|
|
|
|
type Side = {
|
|
|
content: ReactNode;
|
|
|
- default?: CSSValuePct | CSSValueFR;
|
|
|
- max?: LimitValue;
|
|
|
- min?: LimitValue;
|
|
|
+ default: number;
|
|
|
+ max: number;
|
|
|
+ min: number;
|
|
|
};
|
|
|
|
|
|
type Props =
|
|
|
| {
|
|
|
+ availableSize: number;
|
|
|
/**
|
|
|
* Content on the right side of the split
|
|
|
*/
|
|
@@ -46,178 +24,76 @@ type Props =
|
|
|
/**
|
|
|
* Content on the left side of the split
|
|
|
*/
|
|
|
- right: Side;
|
|
|
+ right: ReactNode;
|
|
|
}
|
|
|
| {
|
|
|
+ availableSize: number;
|
|
|
/**
|
|
|
* Content below the split
|
|
|
*/
|
|
|
- bottom: Side;
|
|
|
+ bottom: ReactNode;
|
|
|
/**
|
|
|
* Content above of the split
|
|
|
*/
|
|
|
top: Side;
|
|
|
};
|
|
|
|
|
|
-function getValFromSide<Field extends keyof Side>(side: Side, field: Field) {
|
|
|
- return side && typeof side === 'object' && field in side ? side[field] : undefined;
|
|
|
-}
|
|
|
-
|
|
|
-function getSplitDefault(props: Props) {
|
|
|
- if ('left' in props) {
|
|
|
- const a = getValFromSide(props.left, 'default');
|
|
|
- if (a) {
|
|
|
- return {a};
|
|
|
- }
|
|
|
- const b = getValFromSide(props.right, 'default');
|
|
|
- if (b) {
|
|
|
- return {b};
|
|
|
- }
|
|
|
- return {a: '50%' as CSSValuePct};
|
|
|
- }
|
|
|
- const a = getValFromSide(props.top, 'default');
|
|
|
- if (a) {
|
|
|
- return {a};
|
|
|
- }
|
|
|
- const b = getValFromSide(props.bottom, 'default');
|
|
|
- if (b) {
|
|
|
- return {b};
|
|
|
- }
|
|
|
- return {a: '50%' as CSSValuePct};
|
|
|
-}
|
|
|
-
|
|
|
-function getMinMax(side: Side): {
|
|
|
- max: {pct: number; px: number};
|
|
|
- min: {pct: number; px: number};
|
|
|
-} {
|
|
|
- const ONE = {px: Number.MAX_SAFE_INTEGER, pct: 1.0};
|
|
|
- const ZERO = {px: 0, pct: 0};
|
|
|
- if (!side || typeof side !== 'object') {
|
|
|
- return {
|
|
|
- max: ONE,
|
|
|
- min: ZERO,
|
|
|
- };
|
|
|
- }
|
|
|
- return {
|
|
|
- max: 'max' in side ? {...ONE, ...side.max} : ONE,
|
|
|
- min: 'min' in side ? {...ZERO, ...side.min} : ZERO,
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
function SplitPanel(props: Props) {
|
|
|
- const [isMousedown, setIsMousedown] = useState(false);
|
|
|
- const [sizeCSS, setSizeCSS] = useState(getSplitDefault(props));
|
|
|
- const sizeCSSRef = useRef<undefined | CSSValuePct | CSSValueFR>();
|
|
|
- sizeCSSRef.current = sizeCSS.a;
|
|
|
+ const isLeftRight = 'left' in props;
|
|
|
+ const initialSize = isLeftRight ? props.left.default : props.top.default;
|
|
|
+ const min = isLeftRight ? props.left.min : props.top.min;
|
|
|
+ const max = isLeftRight ? props.left.max : props.top.max;
|
|
|
|
|
|
const {setStartPosition, logEndPosition} = useSplitPanelTracking({
|
|
|
- slideDirection: 'left' in props ? 'leftright' : 'updown',
|
|
|
- });
|
|
|
-
|
|
|
- const onTimeout = useCallback(() => {
|
|
|
- setIsMousedown(false);
|
|
|
- logEndPosition(sizeCSSRef.current);
|
|
|
- }, [logEndPosition]);
|
|
|
- const {start: startMouseIdleTimer, cancel: cancelMouseIdleTimer} = useTimeout({
|
|
|
- timeMs: MOUSE_RELEASE_TIMEOUT_MS,
|
|
|
- onTimeout,
|
|
|
+ slideDirection: isLeftRight ? 'leftright' : 'updown',
|
|
|
});
|
|
|
|
|
|
- const handleMouseDown = useCallback(() => {
|
|
|
- setIsMousedown(true);
|
|
|
- setStartPosition(sizeCSSRef.current);
|
|
|
-
|
|
|
- document.addEventListener(
|
|
|
- 'mouseup',
|
|
|
- () => {
|
|
|
- setIsMousedown(false);
|
|
|
- cancelMouseIdleTimer();
|
|
|
- logEndPosition(sizeCSSRef.current);
|
|
|
- },
|
|
|
- {once: true}
|
|
|
- );
|
|
|
-
|
|
|
- startMouseIdleTimer();
|
|
|
- }, [cancelMouseIdleTimer, logEndPosition, setStartPosition, startMouseIdleTimer]);
|
|
|
-
|
|
|
- const handlePositionChange = useCallback(
|
|
|
- params => {
|
|
|
- if (params) {
|
|
|
- startMouseIdleTimer();
|
|
|
- const {left, top, width, height} = params;
|
|
|
-
|
|
|
- if ('left' in props) {
|
|
|
- const priPx = left - HALF_BAR;
|
|
|
- const priPct = priPx / width;
|
|
|
- const secPx = width - priPx;
|
|
|
- const secPct = 1 - priPct;
|
|
|
- const priLim = getMinMax(props.left);
|
|
|
- const secLim = getMinMax(props.right);
|
|
|
- if (
|
|
|
- priPx < priLim.min.px ||
|
|
|
- priPx > priLim.max.px ||
|
|
|
- priPct < priLim.min.pct ||
|
|
|
- priPct > priLim.max.pct ||
|
|
|
- secPx < secLim.min.px ||
|
|
|
- secPx > secLim.max.px ||
|
|
|
- secPct < secLim.min.pct ||
|
|
|
- secPct > secLim.max.pct
|
|
|
- ) {
|
|
|
- return;
|
|
|
- }
|
|
|
- setSizeCSS({a: `${priPct * 100}%`});
|
|
|
- } else {
|
|
|
- const priPx = top - HALF_BAR;
|
|
|
- const priPct = priPx / height;
|
|
|
- const secPx = height - priPx;
|
|
|
- const secPct = 1 - priPct;
|
|
|
- const priLim = getMinMax(props.top);
|
|
|
- const secLim = getMinMax(props.bottom);
|
|
|
- if (
|
|
|
- priPx < priLim.min.px ||
|
|
|
- priPx > priLim.max.px ||
|
|
|
- priPct < priLim.min.pct ||
|
|
|
- priPct > priLim.max.pct ||
|
|
|
- secPx < secLim.min.px ||
|
|
|
- secPx > secLim.max.px ||
|
|
|
- secPct < secLim.min.pct ||
|
|
|
- secPct > secLim.max.pct
|
|
|
- ) {
|
|
|
- return;
|
|
|
- }
|
|
|
- setSizeCSS({a: `${priPct * 100}%`});
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- [props, startMouseIdleTimer]
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ const onResize = useCallback(
|
|
|
+ debounce(newSize => logEndPosition(`${(newSize / props.availableSize) * 100}%`), 750),
|
|
|
+ [debounce, logEndPosition, props.availableSize]
|
|
|
);
|
|
|
|
|
|
- const elem = useRef<HTMLDivElement>(null);
|
|
|
- const mouseTrackingProps = useMouseTracking<HTMLDivElement>({
|
|
|
- elem,
|
|
|
- onPositionChange: handlePositionChange,
|
|
|
+ const {
|
|
|
+ isHeld,
|
|
|
+ onDoubleClick,
|
|
|
+ onMouseDown: onDragStart,
|
|
|
+ size: containerSize,
|
|
|
+ } = useResizableDrawer({
|
|
|
+ direction: isLeftRight ? 'left' : 'down',
|
|
|
+ initialSize,
|
|
|
+ min,
|
|
|
+ onResize,
|
|
|
});
|
|
|
|
|
|
- const activeTrackingProps = isMousedown ? mouseTrackingProps : {};
|
|
|
+ const sizePct = `${
|
|
|
+ (Math.min(containerSize, max) / props.availableSize) * 100
|
|
|
+ }%` as `${number}%`;
|
|
|
+ const onMouseDown = useCallback(
|
|
|
+ event => {
|
|
|
+ setStartPosition(sizePct);
|
|
|
+ onDragStart(event);
|
|
|
+ },
|
|
|
+ [setStartPosition, onDragStart, sizePct]
|
|
|
+ );
|
|
|
|
|
|
- if ('left' in props) {
|
|
|
+ if (isLeftRight) {
|
|
|
const {left: a, right: b} = props;
|
|
|
|
|
|
return (
|
|
|
<SplitPanelContainer
|
|
|
+ className={isHeld ? 'disable-iframe-pointer' : undefined}
|
|
|
orientation="columns"
|
|
|
- size={sizeCSS}
|
|
|
- ref={elem}
|
|
|
- {...activeTrackingProps}
|
|
|
+ size={sizePct}
|
|
|
>
|
|
|
- <Panel>{getValFromSide(a, 'content') || a}</Panel>
|
|
|
+ <Panel>{a.content}</Panel>
|
|
|
<Divider
|
|
|
+ isHeld={isHeld}
|
|
|
+ onDoubleClick={onDoubleClick}
|
|
|
+ onMouseDown={onMouseDown}
|
|
|
slideDirection="leftright"
|
|
|
- isMousedown={isMousedown}
|
|
|
- onMouseDown={handleMouseDown}
|
|
|
/>
|
|
|
- <Panel>{getValFromSide(b, 'content') || b}</Panel>
|
|
|
- {isMousedown ? <HoverMouseDiv /> : null}
|
|
|
+ <Panel>{b}</Panel>
|
|
|
</SplitPanelContainer>
|
|
|
);
|
|
|
}
|
|
@@ -226,25 +102,24 @@ function SplitPanel(props: Props) {
|
|
|
return (
|
|
|
<SplitPanelContainer
|
|
|
orientation="rows"
|
|
|
- size={sizeCSS}
|
|
|
- ref={elem}
|
|
|
- {...activeTrackingProps}
|
|
|
+ size={sizePct}
|
|
|
+ className={isHeld ? 'disable-iframe-pointer' : undefined}
|
|
|
>
|
|
|
- <Panel>{getValFromSide(a, 'content') || a}</Panel>
|
|
|
+ <Panel>{a.content}</Panel>
|
|
|
<Divider
|
|
|
+ isHeld={isHeld}
|
|
|
+ onDoubleClick={onDoubleClick}
|
|
|
+ onMouseDown={onMouseDown}
|
|
|
slideDirection="updown"
|
|
|
- isMousedown={isMousedown}
|
|
|
- onMouseDown={handleMouseDown}
|
|
|
/>
|
|
|
- <Panel>{getValFromSide(b, 'content') || b}</Panel>
|
|
|
- {isMousedown ? <HoverMouseDiv /> : null}
|
|
|
+ <Panel>{b}</Panel>
|
|
|
</SplitPanelContainer>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
const SplitPanelContainer = styled('div')<{
|
|
|
orientation: 'rows' | 'columns';
|
|
|
- size: {a: CSSValue} | {b: CSSValue};
|
|
|
+ size: `${number}px` | `${number}%`;
|
|
|
}>`
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
@@ -252,28 +127,21 @@ const SplitPanelContainer = styled('div')<{
|
|
|
position: relative;
|
|
|
display: grid;
|
|
|
overflow: auto;
|
|
|
- grid-template-${p => p.orientation}:
|
|
|
- ${p => ('a' in p.size ? p.size.a : '1fr')}
|
|
|
- auto
|
|
|
- ${p => ('a' in p.size ? '1fr' : p.size.b)};
|
|
|
+ grid-template-${p => p.orientation}: ${p => p.size} auto 1fr;
|
|
|
+
|
|
|
+ &.disable-iframe-pointer iframe {
|
|
|
+ pointer-events: none !important;
|
|
|
+ }
|
|
|
`;
|
|
|
|
|
|
const Panel = styled('div')`
|
|
|
overflow: hidden;
|
|
|
`;
|
|
|
|
|
|
-const HoverMouseDiv = styled('div')`
|
|
|
- position: absolute;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
-`;
|
|
|
-
|
|
|
-type DividerProps = {isMousedown: boolean; slideDirection: 'leftright' | 'updown'};
|
|
|
+type DividerProps = {isHeld: boolean; slideDirection: 'leftright' | 'updown'};
|
|
|
const Divider = styled(
|
|
|
({
|
|
|
- isMousedown: _a,
|
|
|
+ isHeld: _a,
|
|
|
slideDirection: _b,
|
|
|
...props
|
|
|
}: DividerProps & DOMAttributes<HTMLDivElement>) => (
|
|
@@ -287,7 +155,7 @@ const Divider = styled(
|
|
|
height: 100%;
|
|
|
width: 100%;
|
|
|
|
|
|
- ${p => (p.isMousedown ? 'user-select: none;' : '')}
|
|
|
+ ${p => (p.isHeld ? 'user-select: none;' : '')}
|
|
|
|
|
|
:hover {
|
|
|
background: ${p => p.theme.hover};
|