Skip to content

Commit 95a4608

Browse files
authored
misc: make test name header sticky in studio and tests list (#32840)
* misc: make studio test name header sticky * update changelog * sticky header in all tests view * update changelog * only display shadow for test header
1 parent 0dd2427 commit 95a4608

File tree

7 files changed

+75
-7
lines changed

7 files changed

+75
-7
lines changed

cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ _Released 11/4/2025 (PENDING)_
2424
- Have cursor on hover of the AUT URL to show as pointer. Addresses [#32777](https://github.com/cypress-io/cypress/issues/32777). Addressed in [#32782](https://github.com/cypress-io/cypress/pull/32782).
2525
- WebKit now prefers a cookie's fully qualified `domain` when requesting a cookie value via [`cy.getCookie()`](https://docs.cypress.io/api/commands/getcookie). If none are found, the cookie's apex domain will be used as a fallback. Addresses [#29954](https://github.com/cypress-io/cypress/issues/29954), [#29973](https://github.com/cypress-io/cypress/issues/29973) and [#30392](https://github.com/cypress-io/cypress/issues/30392). Addressed in [#32852](https://github.com/cypress-io/cypress/pull/32852).
2626
- The 'Next' tooltip style was updated. Addressed in [#32866](https://github.com/cypress-io/cypress/pull/32866).
27+
- Make test name header sticky in studio mode and in the tests list. Addresses [#32591](https://github.com/cypress-io/cypress/issues/32591). Addressed in [#32840](https://github.com/cypress-io/cypress/pull/32840)
2728

2829
**Dependency Updates:**
2930

packages/reporter/src/collapsible/collapsible.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import "../lib/mixins.scss";
2+
13
.reporter {
24
.collapsible-indicator {
35
margin-right: 8px;
@@ -8,4 +10,8 @@
810
.is-open > .collapsible-header-wrapper > .collapsible-header > .collapsible-header-inner > .collapsible-indicator {
911
transform: rotate(0);
1012
}
13+
14+
.test > .is-open > .collapsible-header-wrapper {
15+
@include sticky-header-with-shadow;
16+
}
1117
}

packages/reporter/src/collapsible/collapsible.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import cs from 'classnames'
2-
import React, { CSSProperties, MouseEvent, ReactNode, RefObject, useCallback, useState } from 'react'
2+
import React, { CSSProperties, MouseEvent, ReactNode, RefObject, useCallback, useEffect, useRef, useState } from 'react'
33
import { onEnterOrSpace } from '../lib/util'
44
import DocumentBlankIcon from '@packages/frontend-shared/src/assets/icons/document-blank_x16.svg'
55
import { IconChevronDownSmall } from '@cypress-design/react-icon'
@@ -24,6 +24,8 @@ interface CollapsibleProps {
2424

2525
const Collapsible: React.FC<CollapsibleProps> = ({ isOpen: isOpenAsProp = false, header, headerClass = '', headerStyle = {}, headerExtras, contentClass = '', hideExpander = false, containerRef = null, onOpenStateChangeRequested, children, HeaderComponent }) => {
2626
const [isOpenState, setIsOpenState] = useState(isOpenAsProp)
27+
const headerRef = useRef<HTMLDivElement>(null)
28+
const fixedElementRef = useRef<HTMLDivElement>(null)
2729

2830
const toggleOpenState = useCallback((e?: MouseEvent) => {
2931
e?.stopPropagation()
@@ -36,9 +38,27 @@ const Collapsible: React.FC<CollapsibleProps> = ({ isOpen: isOpenAsProp = false,
3638

3739
const isOpen = onOpenStateChangeRequested ? isOpenAsProp : isOpenState
3840

41+
const toggleHeaderShadow = (entries) => {
42+
const [entry] = entries
43+
44+
headerRef.current?.classList.toggle('shadow-active', !entry.isIntersecting)
45+
}
46+
47+
useEffect(() => {
48+
if (!fixedElementRef?.current) return
49+
50+
const observer = new IntersectionObserver(toggleHeaderShadow)
51+
52+
observer.observe(fixedElementRef.current)
53+
54+
return () => observer.disconnect()
55+
}, [])
56+
3957
return (
4058
<div className={cs('collapsible', { 'is-open': isOpen })} ref={containerRef}>
41-
<div className={cs('collapsible-header-wrapper', headerClass)}>
59+
{/* This empty div acts as an intersection observer target to toggle the header shadow based on scroll position */}
60+
<div ref={fixedElementRef}/>
61+
<div className={cs('collapsible-header-wrapper', headerClass)} ref={headerRef}>
4262
<div
4363
aria-expanded={isOpen}
4464
className='collapsible-header'

packages/reporter/src/lib/mixins.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,22 @@
8787
right: 0;
8888
background: $linear-gradient;
8989
z-index: 0;
90+
}
91+
92+
@mixin sticky-header-with-shadow {
93+
position: sticky;
94+
top: 0;
95+
z-index: 1;
96+
background-color: $gray-1100;
97+
98+
&.shadow-active::after {
99+
content: "";
100+
position: absolute;
101+
top: 101%;
102+
left: 0;
103+
right: 0;
104+
height: 16px;
105+
background: linear-gradient(to bottom, $gray-1100 0%, rgba(22, 24, 39, 0.3) 100%);
106+
pointer-events: none;
107+
}
90108
}

packages/reporter/src/runnables/runnables.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ $dotted-line-left-padding: 19px;
108108
width: 100%;
109109
color: $gray-400;
110110
background-color: $gray-1100;
111-
overflow: auto;
112111
line-height: 18px;
113112

114113
.runnable-wrapper {

packages/reporter/src/studio/StudioTest.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import "../lib/mixins.scss";
2+
13
.studio-single-test-container {
24
display: flex;
35
flex-direction: column;
@@ -15,6 +17,7 @@
1517
font-size: 14px;
1618
line-height: 20px;
1719
flex-shrink: 0;
20+
@include sticky-header-with-shadow;
1821

1922
.state-icon {
2023
height: 32px;

packages/reporter/src/studio/StudioTest.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo, useRef } from 'react'
1+
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
22
import { observer } from 'mobx-react'
33
import { RunnablesStore } from '../runnables/runnables-store'
44
import { Duration } from '../duration/duration'
@@ -65,6 +65,8 @@ export const StudioTest = observer(({ appState, runnablesStore, statsStore }: St
6565
// Single we're in single test mode, the current test is the first test in the runnablesStore._tests
6666
const currentTest = Object.values(runnablesStore._tests)[0]
6767
const tooltipRef = useRef<HTMLUListElement>(null)
68+
const testSectionRef = useRef<HTMLDivElement>(null)
69+
const fixedElementRef = useRef<HTMLDivElement>(null)
6870

6971
const { containerRef, isMounted, scrollIntoView } = useScrollIntoView({
7072
appState,
@@ -89,10 +91,28 @@ export const StudioTest = observer(({ appState, runnablesStore, statsStore }: St
8991

9092
const testTitle = currentTest ? <span data-cy='studio-single-test-title' className='studio-header__test-title'>{currentTest.title}</span> : null
9193

94+
const toggleHeaderShadow = (entries) => {
95+
const [entry] = entries
96+
97+
testSectionRef.current?.classList.toggle('shadow-active', !entry.isIntersecting)
98+
}
99+
100+
useEffect(() => {
101+
if (!fixedElementRef.current) return
102+
103+
const observer = new IntersectionObserver(toggleHeaderShadow)
104+
105+
observer.observe(fixedElementRef.current)
106+
107+
return () => observer.disconnect()
108+
}, [])
109+
92110
return (
93-
currentTest && (
94-
<div className='studio-single-test-container' >
95-
<div className='studio-header__test-section'>
111+
currentTest && (<>
112+
{/* This empty div acts as an intersection observer target to toggle the header shadow based on scroll position */}
113+
<div ref={fixedElementRef} />
114+
<div className='studio-single-test-container'>
115+
<div className='studio-header__test-section' ref={testSectionRef}>
96116
<div className='studio-header__test-section-left'>
97117

98118
<Tooltip placement='bottom' title={<p>All tests</p>} className='cy-tooltip'>
@@ -126,6 +146,7 @@ export const StudioTest = observer(({ appState, runnablesStore, statsStore }: St
126146
<Attempts isSingleStudioTest test={currentTest} scrollIntoView={scrollIntoView} />
127147
</div>
128148
</div>
149+
</>
129150
)
130151
)
131152
})

0 commit comments

Comments
 (0)