Skip to content

internal: allow studio panel to be draggable #31747

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions packages/app/src/runner/ResizablePanels.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { runnerConstants } from './runner-constants'
// default values
const defaultPanel1Width = runnerConstants.defaultSpecListWidth
const defaultPanel2Width = runnerConstants.defaultReporterWidth
const defaultPanel4Width = runnerConstants.defaultStudioWidth
const minPanel1Width = 100
const minPanel2Width = 100
const minPanel3Width = 500
const minPanel4Width = runnerConstants.absoluteStudioMinimum

// helpers
const assertWidth = (panel: ResizablePanelName, width: number) => {
Expand Down Expand Up @@ -38,9 +40,11 @@ describe('<ResizablePanels />', { viewportWidth: 1500, defaultCommandTimeout: 40
v-slots={slotContents}
initialPanel1Width={defaultPanel1Width}
initialPanel2Width={defaultPanel2Width}
initialPanel4Width={defaultPanel4Width}
minPanel1Width={minPanel1Width}
minPanel2Width={minPanel2Width}
minPanel3Width={minPanel3Width}
minPanel4Width={minPanel4Width}
/>
</div>))
})
Expand Down Expand Up @@ -106,6 +110,128 @@ describe('<ResizablePanels />', { viewportWidth: 1500, defaultCommandTimeout: 40
})
})

describe('when panel 4 is shown', () => {
beforeEach(() => {
cy.mount(() => (
<div class="flex">
<div class="h-screen">
<ResizablePanels
maxTotalWidth={2000}
v-slots={slotContents}
initialPanel1Width={defaultPanel1Width}
initialPanel2Width={defaultPanel2Width}
initialPanel4Width={defaultPanel4Width}
minPanel1Width={minPanel1Width}
minPanel2Width={minPanel2Width}
minPanel3Width={minPanel3Width}
minPanel4Width={minPanel4Width}
showPanel4={true}
/>
</div></div>))
})

it('the panels can be resized', () => {
assertWidth('panel1', defaultPanel1Width)
dragHandleToClientX('panel1', 500)
assertWidth('panel1', 500)
dragHandleToClientX('panel1', 400)
assertWidth('panel1', 400)

assertWidth('panel2', defaultPanel2Width)
dragHandleToClientX('panel2', 800)
assertWidth('panel2', 400)
dragHandleToClientX('panel2', 700)
assertWidth('panel2', 300)

assertWidth('panel4', defaultPanel4Width)
dragHandleToClientX('panel4', 1300)
assertWidth('panel4', 700)
dragHandleToClientX('panel4', 1500)
assertWidth('panel4', 500)
})

it('panel 1 can be resized between its minimum allowed width and maximum available space', () => {
// drag panel 1 to its minimum width and attempt to go below it
assertWidth('panel1', defaultPanel1Width)
dragHandleToClientX('panel1', 100)
dragHandleToClientX('panel1', 99)
assertWidth('panel1', minPanel1Width)
dragHandleToClientX('panel1', 50)
assertWidth('panel1', minPanel1Width)

// drag panel 1 to the maximum space available and attempt to go above it
dragHandleToClientX('panel1', 710)
dragHandleToClientX('panel1', 800)
assertWidth('panel1', 710)
dragHandleToClientX('panel1', 900)
assertWidth('panel1', 710)

// panel 2 was not reduced
assertWidth('panel2', defaultPanel2Width)

// panel 3 reached its minimum allowed size
assertWidth('panel3', 500)

// panel 4 was not reduced
assertWidth('panel4', defaultPanel4Width)
})

it('panel 2 can be resized between its minimum allowed width and maximum available space', () => {
// drag panel 2 to its minimum width and attempt to go below it
assertWidth('panel2', defaultPanel2Width)
dragHandleToClientX('panel2', 380)
dragHandleToClientX('panel2', 200)
assertWidth('panel2', minPanel2Width)
dragHandleToClientX('panel2', 180)
assertWidth('panel2', minPanel2Width)

// drag panel 2 to the maximum space available and attempt to go above it
dragHandleToClientX('panel2', 1160)
dragHandleToClientX('panel2', 1200)
assertWidth('panel2', 880)
dragHandleToClientX('panel2', 1300)
assertWidth('panel2', 880)

// panel 1 was not reduced
assertWidth('panel1', defaultPanel1Width)

// panel 3 reached its minimum allowed size
assertWidth('panel3', minPanel3Width)

// panel 4 was not reduced
assertWidth('panel4', defaultPanel4Width)
})

it('panel 4 can be resized between its minimum allowed width and maximum available space', () => {
// since its starting width is the same as its minimum width,
// drag panel 4 to a different width, then drag it to its minimum width and attempt to go below it
assertWidth('panel4', defaultPanel4Width)
dragHandleToClientX('panel4', 1400)
assertWidth('panel4', 600)
dragHandleToClientX('panel4', 1660)
dragHandleToClientX('panel4', 1800)
assertWidth('panel4', minPanel4Width)
dragHandleToClientX('panel4', 1900)
assertWidth('panel4', minPanel4Width)

// drag panel 4 to the maximum space available and attempt to go above it
dragHandleToClientX('panel4', 1230)
dragHandleToClientX('panel4', 1100)
assertWidth('panel4', 770)
dragHandleToClientX('panel4', 900)
assertWidth('panel4', 770)

// panel 1 was not reduced
assertWidth('panel1', defaultPanel1Width)

// panel 2 was not reduced
assertWidth('panel2', defaultPanel2Width)

// panel 3 reached its absolute minimum allowed size
assertWidth('panel3', minPanel3Width)
})
})

describe('when there is a side nav', () => {
it('handles being offset by some distance on the left', () => {
cy.mount(() => (
Expand Down
101 changes: 75 additions & 26 deletions packages/app/src/runner/ResizablePanels.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
id="resizable-panels-root"
class="flex"
:class="{
'select-none': panel1IsDragging || panel2IsDragging,
'select-none': panel1IsDragging || panel2IsDragging || panel4IsDragging,
}"
@mouseup="handleMouseup"
@mousemove="handleMousemove"
Expand All @@ -14,7 +14,7 @@
v-show="showPanel1"
data-cy="specs-list-panel"
class="h-full shrink-0 z-20 relative"
:style="{width: `${panel1Width}px`}"
:style="{ width: `${panel1Width}px` }"
>
<slot
name="panel1"
Expand All @@ -32,7 +32,7 @@
v-show="showPanel2"
data-cy="reporter-panel"
class="h-full shrink-0 z-10 relative"
:style="{width: `${panel2Width}px`}"
:style="{ width: `${panel2Width}px` }"
>
<slot name="panel2" />

Expand All @@ -46,7 +46,7 @@
<div
data-cy="aut-panel"
class="grow h-full bg-gray-100 relative"
:class="{'pointer-events-none':panel2IsDragging}"
:class="{ 'pointer-events-none': panel2IsDragging || panel4IsDragging }"
:style="{ width: `${panel3width}px` }"
>
<slot
Expand All @@ -58,12 +58,15 @@
<div
v-show="showPanel4"
data-cy="panel-4"
class="h-full bg-gray-100 relative"
:style="{width: `${panel4Width}px`}"
class="h-full shrink-0 z-10 bg-gray-100 relative"
:style="{ width: `${panel4Width}px` }"
>
<slot
name="panel4"
:width="panel4Width"
<slot name="panel4" />

<div
data-cy="panel4ResizeHandle"
class="cursor-ew-resize h-full top-0 left-[-6px] w-[10px] z-30 absolute"
@mousedown="handleMousedown('panel4', $event)"
/>
</div>
</div>
Expand All @@ -86,9 +89,11 @@ const props = withDefaults(defineProps<{
showPanel4?: boolean // studio in runner
initialPanel1Width?: number
initialPanel2Width?: number
initialPanel4Width?: number
minPanel1Width?: number
minPanel2Width?: number
minPanel3Width?: number
minPanel4Width?: number
maxTotalWidth?: number // windowWidth in runner
offsetLeft?: number
}>(), {
Expand All @@ -97,23 +102,28 @@ const props = withDefaults(defineProps<{
showPanel4: false,
initialPanel1Width: runnerConstants.defaultSpecListWidth,
initialPanel2Width: runnerConstants.defaultReporterWidth,
initialPanel4Width: runnerConstants.defaultStudioWidth,
minPanel1Width: 200,
minPanel2Width: 220,
minPanel3Width: 100,
minPanel4Width: 340,
maxTotalWidth: window.innerWidth,
offsetLeft: 0,
})

const emit = defineEmits<{
(e: 'resizeEnd', value: DraggablePanel): void
(e: 'panelWidthUpdated', value: {panel: DraggablePanel, width: number}): void
(e: 'panelWidthUpdated', value: { panel: DraggablePanel, width: number }): void
}>()

const panel1HandleX = ref(props.initialPanel1Width)
const panel2HandleX = ref(props.initialPanel2Width + props.initialPanel1Width)
const panel4HandleX = ref(props.initialPanel2Width + props.initialPanel1Width + props.initialPanel4Width)
const panel1IsDragging = ref(false)
const panel2IsDragging = ref(false)
const panel4IsDragging = ref(false)
const cachedPanel1Width = ref<number>(props.initialPanel1Width) // because panel 1 (the inline specs list) can be opened and closed in the UI, we cache the width
const cachedPanel4Width = ref(props.initialPanel4Width)
const panel2Width = ref(props.initialPanel2Width)

const handleMousedown = (panel: DraggablePanel, event: MouseEvent) => {
Expand All @@ -122,10 +132,13 @@ const handleMousedown = (panel: DraggablePanel, event: MouseEvent) => {
} else if (panel === 'panel2') {
panel2IsDragging.value = true
panel2HandleX.value = event.clientX
} else if (panel === 'panel4') {
panel4IsDragging.value = true
panel4HandleX.value = event.clientX
}
}
const handleMousemove = (event: MouseEvent) => {
if (!panel1IsDragging.value && !panel2IsDragging.value) {
if (!panel1IsDragging.value && !panel2IsDragging.value && !panel4IsDragging.value) {
// nothing is dragging, ignore mousemove

return
Expand All @@ -139,6 +152,15 @@ const handleMousemove = (event: MouseEvent) => {
panel2HandleX.value = event.clientX
panel2Width.value = event.clientX - props.offsetLeft - panel1Width.value
emit('panelWidthUpdated', { panel: 'panel2', width: panel2Width.value })
} else if (panel4IsDragging.value && isNewWidthAllowed(event.clientX, 'panel4')) {
panel4HandleX.value = event.clientX
// Calculate width from the right edge of the window
// so that when we drag the panel to the left, it grows
// and when we drag it to the right, it shrinks
const rightEdge = props.maxTotalWidth + props.offsetLeft

cachedPanel4Width.value = rightEdge - event.clientX
emit('panelWidthUpdated', { panel: 'panel4', width: panel4Width.value })
}
}
const handleMouseup = () => {
Expand All @@ -149,30 +171,37 @@ const handleMouseup = () => {
return
}

handleResizeEnd('panel2')
panel2IsDragging.value = false
if (panel2IsDragging.value) {
handleResizeEnd('panel2')
panel2IsDragging.value = false
}

if (panel4IsDragging.value) {
handleResizeEnd('panel4')
panel4IsDragging.value = false
}
}

const maxPanel1Width = computed(() => {
const unavailableWidth = panel2Width.value + props.minPanel3Width
const unavailableWidth = panel2Width.value + props.minPanel3Width + panel4Width.value

return props.maxTotalWidth - unavailableWidth
})

const panel4Width = computed(() => {
if (!props.showPanel4) {
const panel1Width = computed(() => {
if (!props.showPanel1) {
return 0
}

return runnerConstants.defaultStudioWidth
return cachedPanel1Width.value
})

const panel1Width = computed(() => {
if (!props.showPanel1) {
const panel4Width = computed(() => {
if (!props.showPanel4) {
return 0
}

return cachedPanel1Width.value
return cachedPanel4Width.value
})

const maxPanel2Width = computed(() => {
Expand All @@ -192,6 +221,12 @@ const panel3width = computed(() => {
return panel3SpaceAvailable < props.minPanel3Width ? minimumWithBuffer : panel3SpaceAvailable
})

const maxPanel4Width = computed(() => {
const unavailableWidth = panel1Width.value + panel2Width.value + props.minPanel3Width

return props.maxTotalWidth - unavailableWidth
})

function handleResizeEnd (panel: DraggablePanel) {
emit('resizeEnd', panel)
}
Expand All @@ -212,15 +247,29 @@ function isNewWidthAllowed (mouseClientX: number, panel: DraggablePanel) {
return result
}

const newWidth = mouseClientX - props.offsetLeft - panel1Width.value
if (panel === 'panel2') {
const newWidth = mouseClientX - props.offsetLeft - panel1Width.value

if (isMaxWidthSmall && newWidth > fallbackWidth) {
return true
}

return panel2IsDragging.value && newWidth >= props.minPanel2Width && newWidth <= maxPanel2Width.value
}

if (panel === 'panel4') {
const rightEdge = props.maxTotalWidth + props.offsetLeft
const newWidth = rightEdge - mouseClientX

if (isMaxWidthSmall && newWidth >= props.minPanel4Width) {
return true
}

if (isMaxWidthSmall && newWidth > fallbackWidth) {
return true
return panel4IsDragging.value && newWidth >= props.minPanel4Width && newWidth <= maxPanel4Width.value
}

return panel2IsDragging.value && newWidth >= props.minPanel2Width && newWidth <= maxPanel2Width.value
return false
}

watchEffect(() => {
if (!props.showPanel1) {
emit('panelWidthUpdated', { panel: 'panel1', width: 0 })
Expand All @@ -231,7 +280,7 @@ watchEffect(() => {
if (!props.showPanel4) {
emit('panelWidthUpdated', { panel: 'panel4', width: 0 })
} else if (props.showPanel4) {
emit('panelWidthUpdated', { panel: 'panel4', width: panel4Width.value })
emit('panelWidthUpdated', { panel: 'panel4', width: cachedPanel4Width.value })
}
})

Expand Down
Loading
Loading