mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat: vertical resizer (#21605)
This commit is contained in:
@@ -114,7 +114,7 @@ export function SidePanel(): JSX.Element | null {
|
||||
},
|
||||
}
|
||||
|
||||
const { desiredWidth, isResizeInProgress } = useValues(resizerLogic(resizerLogicProps))
|
||||
const { desiredSize, isResizeInProgress } = useValues(resizerLogic(resizerLogicProps))
|
||||
|
||||
useEffect(() => {
|
||||
setSidePanelAvailable(true)
|
||||
@@ -171,7 +171,7 @@ export function SidePanel(): JSX.Element | null {
|
||||
ref={ref}
|
||||
// eslint-disable-next-line react/forbid-dom-props
|
||||
style={{
|
||||
width: sidePanelOpenAndAvailable ? desiredWidth ?? DEFAULT_WIDTH : undefined,
|
||||
width: sidePanelOpenAndAvailable ? desiredSize ?? DEFAULT_WIDTH : undefined,
|
||||
...(theme?.sidebarStyle ?? {}),
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,35 +1,62 @@
|
||||
@mixin orientation-mapping($width, $top, $bottom, $left) {
|
||||
#{$width}: var(--resizer-thickness);
|
||||
#{$top}: 0;
|
||||
#{$bottom}: 0;
|
||||
|
||||
.Resizer__handle {
|
||||
#{$top}: 0;
|
||||
#{$bottom}: 0;
|
||||
#{$width}: 1px;
|
||||
#{$left}: calc(var(--resizer-thickness) / 2);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
#{$top}: 0;
|
||||
#{$bottom}: 0;
|
||||
#{$width}: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Resizer {
|
||||
--resizer-width: 8px;
|
||||
--resizer-thickness: 8px;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: var(
|
||||
--z-notifications-popover
|
||||
); // A bit above navbar for a nicer slider experience when the sidebar is closed
|
||||
|
||||
width: var(--resizer-width);
|
||||
cursor: col-resize;
|
||||
user-select: none; // Fixes inadvertent selection of scene text when resizing
|
||||
|
||||
&--left,
|
||||
&--right {
|
||||
--scale: scaleX(3);
|
||||
|
||||
cursor: col-resize;
|
||||
|
||||
@include orientation-mapping(width, top, bottom, left);
|
||||
}
|
||||
|
||||
&--top,
|
||||
&--bottom {
|
||||
--scale: scaleY(3);
|
||||
|
||||
cursor: row-resize;
|
||||
|
||||
@include orientation-mapping(height, left, right, top);
|
||||
}
|
||||
|
||||
.Resizer[aria-hidden='true'] & {
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.Resizer__handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: calc(var(--resizer-width) / 2);
|
||||
width: 1px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
box-sizing: content-box;
|
||||
width: 1px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
@@ -47,12 +74,22 @@
|
||||
|
||||
&--left {
|
||||
left: 0;
|
||||
transform: translateX(calc(var(--resizer-width) / 2 * -1));
|
||||
transform: translateX(calc(var(--resizer-thickness) / 2 * -1));
|
||||
}
|
||||
|
||||
&--right {
|
||||
right: 0;
|
||||
transform: translateX(calc(var(--resizer-width) / 2 * 1));
|
||||
transform: translateX(calc(var(--resizer-thickness) / 2 * 1));
|
||||
}
|
||||
|
||||
&--top {
|
||||
top: 0;
|
||||
transform: translateY(calc(var(--resizer-thickness) / 2 * -1));
|
||||
}
|
||||
|
||||
&--bottom {
|
||||
bottom: 0;
|
||||
transform: translateY(calc(var(--resizer-thickness) / 2 * 1));
|
||||
}
|
||||
|
||||
&:hover .Resizer__handle::after,
|
||||
@@ -62,6 +99,6 @@
|
||||
|
||||
&--resizing .Resizer__handle::before,
|
||||
&--resizing .Resizer__handle::after {
|
||||
transform: scaleX(3);
|
||||
transform: var(--scale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export type ResizerProps = ResizerLogicProps & {
|
||||
|
||||
export function Resizer(props: ResizerProps): JSX.Element {
|
||||
const logic = resizerLogic(props)
|
||||
const { isResizeInProgress } = useValues(logic)
|
||||
const { isResizeInProgress, isVertical } = useValues(logic)
|
||||
const { beginResize } = useActions(logic)
|
||||
|
||||
// The same logic can be used by multiple resizers
|
||||
@@ -38,7 +38,7 @@ export function Resizer(props: ResizerProps): JSX.Element {
|
||||
onMouseDown={(e) => {
|
||||
if (e.button === 0) {
|
||||
setIsSelected(true)
|
||||
beginResize(e.pageX)
|
||||
beginResize(isVertical ? e.pageX : e.pageY)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -4,20 +4,15 @@ import posthog from 'posthog-js'
|
||||
import type { resizerLogicType } from './resizerLogicType'
|
||||
|
||||
export type ResizerEvent = {
|
||||
originX: number
|
||||
desiredX: number
|
||||
finished: boolean
|
||||
originWidth: number
|
||||
desiredWidth: number
|
||||
desiredSize: number
|
||||
}
|
||||
|
||||
export type ResizerLogicProps = {
|
||||
logicKey: string
|
||||
persistent?: boolean
|
||||
placement: 'left' | 'right'
|
||||
placement: 'left' | 'right' | 'top' | 'bottom'
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
onResize?: (event: ResizerEvent) => void
|
||||
/** At what width, should this rather be considered a "close" event */
|
||||
/** At what size, should this rather be considered a "close" event */
|
||||
closeThreshold?: number
|
||||
/** Fired when the "closeThreshold" is crossed */
|
||||
onToggleClosed?: (closed: boolean) => void
|
||||
@@ -38,11 +33,11 @@ export const resizerLogic = kea<resizerLogicType>([
|
||||
key((props) => props.logicKey),
|
||||
path((key) => ['components', 'resizer', 'resizerLogic', key]),
|
||||
actions({
|
||||
beginResize: (startX: number) => ({ startX }),
|
||||
beginResize: (startXOrY: number) => ({ startXOrY }),
|
||||
endResize: true,
|
||||
setResizingWidth: (width: number | null) => ({ width }),
|
||||
setDesiredWidth: (width: number | null) => ({ width }),
|
||||
resetDesiredWidth: true,
|
||||
setResizingSize: (size: number | null) => ({ size }),
|
||||
setDesiredSize: (size: number | null) => ({ size }),
|
||||
resetDesiredSize: true,
|
||||
}),
|
||||
reducers(({ props }) => ({
|
||||
isResizeInProgress: [
|
||||
@@ -52,33 +47,35 @@ export const resizerLogic = kea<resizerLogicType>([
|
||||
endResize: () => false,
|
||||
},
|
||||
],
|
||||
width: [
|
||||
size: [
|
||||
null as number | null,
|
||||
{ persist: props.persistent },
|
||||
{
|
||||
setDesiredWidth: (_, { width }) => width,
|
||||
resetDesiredWidth: () => null,
|
||||
setDesiredSize: (_, { size }) => size,
|
||||
resetDesiredSize: () => null,
|
||||
},
|
||||
],
|
||||
resizingWidth: [
|
||||
resizingSize: [
|
||||
null as number | null,
|
||||
{
|
||||
setResizingWidth: (_, { width }) => width,
|
||||
setResizingSize: (_, { size }) => size,
|
||||
beginResize: () => null,
|
||||
endResize: () => null,
|
||||
},
|
||||
],
|
||||
})),
|
||||
selectors({
|
||||
desiredWidth: [
|
||||
(s) => [s.width, s.resizingWidth, s.isResizeInProgress],
|
||||
(width, resizingWidth, isResizeInProgress) => {
|
||||
return isResizeInProgress ? resizingWidth ?? width : width
|
||||
desiredSize: [
|
||||
(s) => [s.size, s.resizingSize, s.isResizeInProgress],
|
||||
(size, resizingSize, isResizeInProgress) => {
|
||||
return isResizeInProgress ? resizingSize ?? size : size
|
||||
},
|
||||
],
|
||||
isVertical: [(_, p) => [p.placement], (placement) => ['left', 'right'].includes(placement)],
|
||||
isStart: [(_, p) => [p.placement], (placement) => ['left', 'top'].includes(placement)],
|
||||
}),
|
||||
listeners(({ cache, props, actions, values }) => ({
|
||||
beginResize: ({ startX }) => {
|
||||
beginResize: ({ startXOrY }) => {
|
||||
if (!props.containerRef.current) {
|
||||
return
|
||||
}
|
||||
@@ -87,37 +84,33 @@ export const resizerLogic = kea<resizerLogicType>([
|
||||
cache.firstClickTimestamp = Date.now()
|
||||
|
||||
const originContainerBounds = props.containerRef.current.getBoundingClientRect()
|
||||
const originContainerBoundsSize = values.isVertical
|
||||
? originContainerBounds.width
|
||||
: originContainerBounds.height
|
||||
|
||||
let isClosed = props.closeThreshold ? originContainerBounds.width < props.closeThreshold : false
|
||||
let isClosed = props.closeThreshold ? originContainerBoundsSize < props.closeThreshold : false
|
||||
|
||||
removeAllListeners(cache)
|
||||
cache.originX = startX
|
||||
cache.originXOrY = startXOrY
|
||||
|
||||
const calculateEvent = (e: MouseEvent, finished: boolean): ResizerEvent => {
|
||||
const calculateEvent = (e: MouseEvent): ResizerEvent => {
|
||||
// desired width is based on the change relative to the original bounds
|
||||
// The resizer could be on the left or the right, so we need to account for this
|
||||
const eventSize = values.isVertical ? e.pageX : e.pageY
|
||||
const difference = eventSize - cache.originXOrY
|
||||
const desiredSize = values.isStart
|
||||
? originContainerBoundsSize - difference
|
||||
: originContainerBoundsSize + difference
|
||||
|
||||
const desiredWidth =
|
||||
props.placement === 'left'
|
||||
? originContainerBounds.width - (e.pageX - cache.originX)
|
||||
: originContainerBounds.width + (e.pageX - cache.originX)
|
||||
|
||||
return {
|
||||
originX: cache.originX,
|
||||
desiredX: e.pageX,
|
||||
originWidth: originContainerBounds.width,
|
||||
desiredWidth,
|
||||
finished,
|
||||
}
|
||||
return { desiredSize }
|
||||
}
|
||||
|
||||
cache.onMouseMove = (e: MouseEvent): void => {
|
||||
const event = calculateEvent(e, false)
|
||||
props.onResize?.(event)
|
||||
actions.setResizingWidth(event.desiredWidth)
|
||||
const event = calculateEvent(e)
|
||||
actions.setResizingSize(event.desiredSize)
|
||||
isDoubleClick = false
|
||||
|
||||
const newIsClosed = props.closeThreshold ? event.desiredWidth < props.closeThreshold : false
|
||||
const newIsClosed = props.closeThreshold ? event.desiredSize < props.closeThreshold : false
|
||||
|
||||
if (newIsClosed !== isClosed) {
|
||||
props.onToggleClosed?.(newIsClosed)
|
||||
@@ -127,25 +120,23 @@ export const resizerLogic = kea<resizerLogicType>([
|
||||
}
|
||||
cache.onMouseUp = (e: MouseEvent): void => {
|
||||
if (e.button === 0) {
|
||||
const event = calculateEvent(e, false)
|
||||
const event = calculateEvent(e)
|
||||
|
||||
if (isDoubleClick) {
|
||||
// Double click - reset to original width
|
||||
actions.resetDesiredWidth()
|
||||
actions.resetDesiredSize()
|
||||
cache.firstClickTimestamp = null
|
||||
|
||||
props.onDoubleClick?.()
|
||||
} else if (event.desiredWidth !== values.width) {
|
||||
} else if (event.desiredSize !== values.size) {
|
||||
if (!isClosed) {
|
||||
// We only want to persist the value if it is open
|
||||
actions.setDesiredWidth(event.desiredWidth)
|
||||
actions.setDesiredSize(event.desiredSize)
|
||||
}
|
||||
|
||||
props.onResize?.(event)
|
||||
|
||||
posthog.capture('element resized', {
|
||||
key: props.logicKey,
|
||||
newWidth: event.desiredWidth,
|
||||
newWidth: event.desiredSize,
|
||||
originalWidth: originContainerBounds.width,
|
||||
isClosed,
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ export function PlayerInspector({
|
||||
onToggleClosed: (shouldBeClosed) => setInspectorExpanded(!shouldBeClosed),
|
||||
}
|
||||
|
||||
const { desiredWidth } = useValues(resizerLogic(resizerLogicProps))
|
||||
const { desiredSize } = useValues(resizerLogic(resizerLogicProps))
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -37,7 +37,7 @@ export function PlayerInspector({
|
||||
ref={ref}
|
||||
// eslint-disable-next-line react/forbid-dom-props
|
||||
style={{
|
||||
width: inspectorExpanded ? desiredWidth ?? 'var(--inspector-width)' : undefined,
|
||||
width: inspectorExpanded ? desiredSize ?? 'var(--inspector-width)' : undefined,
|
||||
}}
|
||||
>
|
||||
<Resizer logicKey="player-inspector" placement="left" containerRef={ref} closeThreshold={100} />
|
||||
|
||||
Reference in New Issue
Block a user