feat: vertical resizer (#21605)

This commit is contained in:
David Newell
2024-04-17 13:54:50 +01:00
committed by GitHub
parent 606550c0b1
commit 2f0efba61c
5 changed files with 97 additions and 69 deletions

View File

@@ -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 ?? {}),
}}
>

View File

@@ -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);
}
}

View File

@@ -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)
}
}}
>

View File

@@ -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,
})

View File

@@ -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} />