1- " use client" ;
1+ ' use client' ;
22
3- import useResizeObserver from '@/common/hooks/useResizeObserver' ;
3+ import { useResizeObserver } from '@/common/hooks/useResizeObserver' ;
44import { Button } from '@/ui/Button' ;
55import { Text } from '@/ui/Text' ;
66import { Box , Flex , Icon , Stack } from '@chakra-ui/react' ;
77import { Plus , X } from '@phosphor-icons/react' ;
8- import { RefObject , createRef , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
8+ import { createRef , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
99
10- const cardPaddingY = 3 ;
11- const cardMarginBottom = 3 ;
12- const cardOverlap = 10 ; // Amount of overlap between cards
13- const onHoverRise = 40 ; // How much the card rises when hovered
14- const extraRiseToShowContent = cardOverlap - 5 ;
15-
16- const calculateCardHeight = ( cardTitleHeight : number ) => {
17- return cardTitleHeight + cardMarginBottom * 4 + cardPaddingY * 2 * 4 ;
18- } ;
10+ const DEFUALT_ITEM_HEIGHT_IN_PX = 40 ; // can make this custom
11+ const DEFAULT_ITEM_OVERLAP_IN_PX = 12 ;
12+ const DEFAULT_PX = 4 ;
13+ const DEFAULT_PY = 3 ;
14+ const DEFAULT_HOVER_RISE_IN_PX = 40 ; // How much the card rises when hovered
15+ const EXTRA_RISE_TO_SHOW_CONTENT_IN_PX = DEFAULT_ITEM_OVERLAP_IN_PX / 2 ;
16+ const MARGIN_FROM_TOP_IN_PX = 20 ;
1917
2018function ReverseAccordionItem ( {
2119 title,
@@ -25,9 +23,11 @@ function ReverseAccordionItem({
2523 index,
2624 setIsExpanded,
2725 isExpanded,
28- itemTitleRef,
29- topPosition,
3026 accordionWidth,
27+ cardHeightInPx,
28+ top,
29+ px,
30+ py,
3131} : {
3232 title : string ;
3333 text : string ;
@@ -36,103 +36,126 @@ function ReverseAccordionItem({
3636 index : number ;
3737 setIsExpanded : ( index : number , state : boolean ) => void ;
3838 isExpanded : boolean ;
39- itemTitleRef : RefObject < HTMLDivElement > ;
40- topPosition : number ;
4139 accordionWidth : number ;
40+ cardHeightInPx : number ;
41+ top : number ;
42+ px ?: number ;
43+ py ?: number ;
4244} ) {
4345 const [ isHovered , setIsHovered ] = useState ( false ) ;
4446 const [ contentHeight , setContentHeight ] = useState ( 0 ) ;
45- const [ titleHeight , setTitleHeight ] = useState ( 0 ) ;
4647 const contentRef = useRef < HTMLDivElement > ( null ) ;
4748 const containerRef = useRef < HTMLDivElement > ( null ) ;
48- const cardHeight = useMemo ( ( ) => calculateCardHeight ( titleHeight ) , [ titleHeight ] ) ;
49+ const [ rectTop , setRectTop ] = useState < number > ( 0 ) ;
50+ const maxRise = useMemo (
51+ ( ) => rectTop - cardHeightInPx - MARGIN_FROM_TOP_IN_PX ,
52+ [ rectTop , cardHeightInPx ]
53+ ) ;
4954
55+ // Sets the content height
5056 useEffect ( ( ) => {
5157 if ( contentRef . current ) {
5258 setContentHeight ( contentRef . current . scrollHeight ) ;
59+ if ( contentRef . current . getBoundingClientRect ( ) . top > rectTop ) {
60+ setRectTop ( contentRef . current . getBoundingClientRect ( ) . top ) ;
61+ }
5362 }
54- } , [ text , isExpanded , isHovered , accordionWidth ] ) ;
55-
56- useEffect ( ( ) => {
57- if ( itemTitleRef . current ) {
58- setTitleHeight ( itemTitleRef . current . scrollHeight ) ;
59- }
60- } , [ text , isExpanded , isHovered , accordionWidth ] ) ;
63+ } , [ text , isExpanded , isHovered , accordionWidth , rectTop ] ) ;
6164
6265 return (
6366 < Stack
6467 className = "CARD"
6568 ref = { containerRef }
66- gap = { 0 }
6769 position = "absolute"
68- top = { `${ topPosition } px` }
70+ top = { `${ top } px` }
6971 left = { 0 }
7072 right = { 0 }
71- bg = "white"
72- border = "1px solid var(--stacks-colors-sand-200)"
73+ gap = { 0 }
74+ bg = "surfaceFourth"
75+ border = "1px solid var(--stacks-colors-sand-50)"
7376 borderRadius = "xl"
7477 onMouseEnter = { ( ) => setIsHovered ( true ) }
7578 onMouseLeave = { ( ) => setIsHovered ( false ) }
7679 onClick = { ( ) => ( isExpanded ? null : setIsExpanded ( index , true ) ) }
77- style = { {
78- // the transform is there to balance out the height so that the card will look like it moves upward instead of downward. For example, if the height grows by 20px, we can move the card up 20px so that the height drops back down to the original bottom of the card
79- transition : 'all 0.3s ease-out' ,
80- transform : isExpanded
81- ? `translateY(-${ contentHeight + extraRiseToShowContent } px)`
80+ transition = "all 0.3s ease-out"
81+ transform = {
82+ isExpanded
83+ ? `translateY(-${ Math . min ( contentHeight , maxRise ) + EXTRA_RISE_TO_SHOW_CONTENT_IN_PX } px)`
8284 : isHovered
83- ? `translateY(-${ onHoverRise } px)`
84- : 'translateY(0)' ,
85- height : isExpanded
86- ? `${ cardHeight + contentHeight } px`
85+ ? `translateY(-${ DEFAULT_HOVER_RISE_IN_PX } px)`
86+ : 'translateY(0)'
87+ }
88+ height = {
89+ isExpanded
90+ ? `${ cardHeightInPx + Math . min ( contentHeight , maxRise ) } px`
8791 : isHovered
88- ? `${ cardHeight + onHoverRise } px`
89- : `${ cardHeight } px` ,
90- overflow : 'hidden' ,
91- zIndex : 10 + index ,
92- } }
92+ ? `${ cardHeightInPx + DEFAULT_HOVER_RISE_IN_PX } px`
93+ : `${ cardHeightInPx } px`
94+ }
95+ overflow = "hidden"
96+ zIndex = { 1000 + index }
9397 boxShadow = "0px -8px 10px -6px rgba(0, 0, 0, 0.1)"
9498 >
95- < Stack gap = { 0 } >
96- < Flex height = { cardHeight } className = "CARD-TITLE-CONTAINER" alignItems = "center" px = { 4 } >
97- < Flex
98- justifyContent = "space-between"
99- alignItems = "center"
100- className = "cardTitle"
101- ref = { itemTitleRef }
102- height = "fit-content"
103- w = "full"
99+ < >
100+ < Flex
101+ className = "CARD-TITLE"
102+ height = { `${ cardHeightInPx } px` }
103+ alignItems = "center"
104+ justifyContent = "space-between"
105+ w = "full"
106+ px = { px }
107+ py = { py }
108+ >
109+ < Text textStyle = "text-medium-xs" > { title } </ Text >
110+ < Icon
111+ onClick = { ( ) => ( isExpanded ? setIsExpanded ( index , false ) : null ) }
112+ h = { 4 }
113+ w = { 4 }
114+ color = "iconTertiary"
104115 >
105- < Text fontWeight = "medium" > { title } </ Text >
106- < Icon onClick = { ( ) => ( isExpanded ? setIsExpanded ( index , false ) : null ) } h = { 4 } w = { 4 } >
107- { isExpanded ? < X /> : < Plus /> }
108- </ Icon >
109- </ Flex >
116+ { isExpanded ? < X /> : < Plus /> }
117+ </ Icon >
110118 </ Flex >
111- < Flex ref = { contentRef } height = "fit-content" flexDirection = "column" gap = { 4 } px = { 3 } pb = { 3 } >
119+ < Stack className = "CARD-CONTENT" ref = { contentRef } gap = { 4 } px = { px } pb = { py } overflowY = "scroll" >
112120 < Text color = "textSubdued" > { text } </ Text >
113121 < Button > { linkLabel } </ Button >
114- </ Flex >
115- </ Stack >
122+ </ Stack >
123+ </ >
116124 </ Stack >
117125 ) ;
118126}
119127
120128export interface ReverseAccordionItem {
121- // This should be made more generic to accomodate other types of item content
122129 title : string ;
123130 text : string ;
124131 link : string ;
125132 linkLabel : string ;
126133}
127134
128- export function ReverseAccordion ( { items } : { items : ReverseAccordionItem [ ] } ) {
129- const [ totalHeight , setTotalHeight ] = useState ( 0 ) ;
135+ function calculateReverseAccordionHeight (
136+ numOfItems : number ,
137+ itemHeight : number ,
138+ itemOverlap : number
139+ ) {
140+ return `${ numOfItems * itemHeight - itemOverlap * ( numOfItems - 1 ) } px` ;
141+ }
142+
143+ export function ReverseAccordion ( {
144+ items,
145+ itemHeight = DEFUALT_ITEM_HEIGHT_IN_PX ,
146+ itemOverlap = DEFAULT_ITEM_OVERLAP_IN_PX ,
147+ } : {
148+ items : ReverseAccordionItem [ ] ;
149+ itemHeight ?: number ;
150+ itemOverlap ?: number ;
151+ } ) {
152+ const [ totalHeightInPx , _ ] = useState (
153+ calculateReverseAccordionHeight ( items . length , itemHeight , itemOverlap )
154+ ) ;
130155 const containerRef = useRef < HTMLDivElement > ( null ) ;
131156 const [ expandedIndex , setExpandedIndex ] = useState < number | null > ( null ) ;
132- const itemTitleRefs = useRef < Array < React . RefObject < HTMLDivElement > > > (
133- Array ( items . length )
134- . fill ( null )
135- . map ( ( ) => createRef < HTMLDivElement > ( ) )
157+ const itemTitleRefs = useRef < React . RefObject < HTMLDivElement | null > [ ] > (
158+ Array . from ( { length : items . length } , ( ) => createRef < HTMLDivElement > ( ) )
136159 ) ;
137160
138161 const setIsExpanded = useCallback ( ( index : number , state : boolean ) => {
@@ -145,21 +168,10 @@ export function ReverseAccordion({ items }: { items: ReverseAccordionItem[] }) {
145168
146169 const { width } = useResizeObserver ( containerRef ) ;
147170
148- useEffect ( ( ) => {
149- if ( containerRef . current ) {
150- const height =
151- itemTitleRefs . current . reduce (
152- ( sum , ref ) => sum + ( calculateCardHeight ( ref . current ?. scrollHeight ?? 0 ) - cardOverlap ) ,
153- 0
154- ) + cardOverlap ; // add the overlap back on to account for the last card
155- setTotalHeight ( height ) ;
156- }
157- } , [ items , width ] ) ;
158-
159171 return (
160172 < Box
161173 position = "relative"
162- height = { ` ${ totalHeight } px` }
174+ height = { totalHeightInPx }
163175 w = "full"
164176 ref = { containerRef }
165177 className = "REVERSE-ACCORDION"
@@ -174,15 +186,13 @@ export function ReverseAccordion({ items }: { items: ReverseAccordionItem[] }) {
174186 index = { index }
175187 setIsExpanded = { setIsExpanded }
176188 isExpanded = { expandedIndex === index }
177- itemTitleRef = { itemTitleRefs . current [ index ] }
178- topPosition = { itemTitleRefs . current
179- . slice ( 0 , index )
180- . reduce (
181- ( sum , ref ) =>
182- sum + ( calculateCardHeight ( ref . current ?. scrollHeight ?? 0 ) - cardOverlap ) ,
183- 0
184- ) }
185189 accordionWidth = { width }
190+ cardHeightInPx = { itemHeight }
191+ top = { itemTitleRefs . current // This controls the positioning of the cards, accounting for the overlap between cards
192+ . slice ( 0 , index )
193+ . reduce ( ( sum , ref ) => sum + ( itemHeight - itemOverlap ) , 0 ) }
194+ px = { DEFAULT_PX }
195+ py = { DEFAULT_PY }
186196 />
187197 ) ) }
188198 </ Box >
0 commit comments