11/*
22 * This file is part of Edgehog.
33 *
4- * Copyright 2025 SECO Mind Srl
4+ * Copyright 2025 - 2026 SECO Mind Srl
55 *
66 * Licensed under the Apache License, Version 2.0 (the "License");
77 * you may not use this file except in compliance with the License.
1818 * SPDX-License-Identifier: Apache-2.0
1919 */
2020
21- import { useState } from "react" ;
21+ import { useCallback , useRef , useState } from "react" ;
2222import { useIntl } from "react-intl" ;
23- import { Card , Button , Collapse } from "react-bootstrap" ;
23+ import { Button , Collapse } from "react-bootstrap" ;
2424
2525import Icon from "@/components/Icon" ;
26- import ContainerStatus , {
27- parseContainerState ,
28- } from "@/components/ContainerStatus" ;
2926
3027export function useCollapseToggle ( defaultOpen = false ) {
3128 const [ open , setOpen ] = useState ( defaultOpen ) ;
@@ -50,32 +47,26 @@ export function useCollapsibleSections<T extends string | number>(
5047 return { openSections, toggleSection, isSectionOpen, setOpenSections } ;
5148}
5249
53- interface CollapseCaretProps {
54- open : boolean ;
55- }
56-
57- const CollapseCaret = ( { open } : CollapseCaretProps ) => {
58- return (
59- < span
60- style = { {
61- display : "inline-flex" ,
62- transition : "transform 0.2s ease-in-out" ,
63- transform : open ? "rotate(0deg)" : "rotate(-180deg)" ,
64- } }
65- >
66- < Icon icon = "caretDown" />
67- </ span >
68- ) ;
69- } ;
50+ const CollapseCaret = ( { open } : { open : boolean } ) => (
51+ < span
52+ style = { {
53+ display : "inline-flex" ,
54+ transition : "transform 0.2s ease-in-out" ,
55+ transform : open ? "rotate(0deg)" : "rotate(-180deg)" ,
56+ } }
57+ >
58+ < Icon icon = "caretDown" />
59+ </ span >
60+ ) ;
7061
71- interface CollapseHeaderButtonProps {
62+ type CollapseHeaderButtonProps = {
7263 open : boolean ;
7364 onToggle : ( ) => void ;
7465 children : React . ReactNode ;
7566 className ?: string ;
7667 style ?: React . CSSProperties ;
7768 title ?: string ;
78- }
69+ } ;
7970
8071const CollapseHeaderButton = ( {
8172 open,
@@ -96,98 +87,107 @@ const CollapseHeaderButton = ({
9687 { children }
9788 </ Button >
9889) ;
99-
100- type CollapseType = "flat" | "card-parent" | "card-child" ;
101-
102- interface CollapseItemProps {
90+ type CollapseItemProps = {
10391 title : React . ReactNode ;
10492 children : React . ReactNode ;
10593 open : boolean ;
10694 onToggle : ( ) => void ;
107- containerStatus ?: string | null ;
108- isInsideTable ?: boolean ;
109- type ?: CollapseType ;
110- }
95+ rightContent ?: React . ReactNode ;
96+
97+ // layout props
98+ bordered ?: boolean ;
99+ borderBottom ?: boolean ;
100+ transparent ?: boolean ;
101+ boldTitle ?: boolean ;
102+ childFontSize ?: boolean ;
103+ showToggleTooltip ?: boolean ;
104+ caretPosition ?: "left" | "right" | "end" ;
105+ } ;
111106
112107const CollapseItem = ( {
113108 title,
114109 children,
115110 open,
116111 onToggle,
117- containerStatus,
118- isInsideTable = false ,
119- type = "card-child" ,
112+ rightContent,
113+ bordered = true ,
114+ borderBottom,
115+ transparent = false ,
116+ boldTitle = false ,
117+ childFontSize = false ,
118+ showToggleTooltip = false ,
119+ caretPosition,
120120} : CollapseItemProps ) => {
121121 const intl = useIntl ( ) ;
122-
123- const isFlat = type === "flat" ;
124- const isParent = type === "card-parent" ;
125-
126- if ( isFlat ) {
127- const collapseTitle = isInsideTable
128- ? open
129- ? intl . formatMessage ( {
130- id : "components.CollapseItem.collapseList" ,
131- defaultMessage : "Collapse list" ,
132- } )
133- : intl . formatMessage ( {
134- id : "components.CollapseItem.expandList" ,
135- defaultMessage : "Expand list" ,
136- } )
137- : undefined ;
138-
139- return (
140- < div
141- className = {
142- ! isInsideTable ? `mb-2 border-bottom ${ open ? "pb-4" : "pb-1" } ` : ""
143- }
144- >
145- < CollapseHeaderButton
146- open = { open }
147- onToggle = { onToggle }
148- title = { collapseTitle }
149- className = { `d-flex align-items-center ps-0 pe-1 ${ ! isInsideTable ? "fw-bold" : "" } ` }
150- style = { { backgroundColor : "transparent" , border : "none" } }
151- >
152- < span className = "d-flex align-items-center gap-2" >
153- { title }
154- < CollapseCaret open = { open } />
155- </ span >
156- </ CollapseHeaderButton >
157-
158- < Collapse in = { open } >
159- < div className = { isInsideTable ? "" : "pt-3" } > { children } </ div >
160- </ Collapse >
161- </ div >
162- ) ;
163- }
122+ const containerRef = useRef < HTMLDivElement | null > ( null ) ;
123+
124+ const handleScrollIntoView = useCallback ( ( ) => {
125+ if ( ! containerRef . current ) return ;
126+
127+ const rect = containerRef . current . getBoundingClientRect ( ) ;
128+ if ( rect . bottom > window . innerHeight ) {
129+ containerRef . current . scrollIntoView ( {
130+ behavior : "smooth" ,
131+ block : "nearest" ,
132+ } ) ;
133+ }
134+ } , [ ] ) ;
135+
136+ const collapseTitle = showToggleTooltip
137+ ? open
138+ ? intl . formatMessage ( {
139+ id : "components.CollapseItem.collapseList" ,
140+ defaultMessage : "Collapse list" ,
141+ } )
142+ : intl . formatMessage ( {
143+ id : "components.CollapseItem.expandList" ,
144+ defaultMessage : "Expand list" ,
145+ } )
146+ : undefined ;
147+
148+ const renderCaret = ( ) => < CollapseCaret open = { open } /> ;
164149
165150 return (
166- < Card className = { `shadow-sm ${ isParent ? "mb-3" : "mb-2" } ` } >
167- < Card . Header className = "p-0" >
168- < CollapseHeaderButton
169- open = { open }
170- onToggle = { onToggle }
171- className = { `w-100 d-flex align-items-center ${ isParent ? "fw-bold p-2" : "fw-semibold p-1" } ` }
172- style = { { fontSize : isParent ? "1rem" : "0.9rem" } }
173- >
151+ < div
152+ ref = { containerRef }
153+ className = { `
154+ ${ bordered ? "border rounded" : "" }
155+ ${ borderBottom ? `mb-2 border-bottom ${ open ? "pb-4" : "pb-1" } ` : "" }
156+ ` }
157+ >
158+ < CollapseHeaderButton
159+ open = { open }
160+ onToggle = { onToggle }
161+ title = { collapseTitle }
162+ className = { `d-flex align-items-center w-100 ${
163+ bordered ? "border-0" : "ps-0 pe-1"
164+ } ${ boldTitle ? "fw-bold" : "" } `}
165+ style = { {
166+ backgroundColor : transparent ? "transparent" : undefined ,
167+ border : transparent ? "none" : undefined ,
168+ fontSize : childFontSize ? "0.9rem" : undefined ,
169+ } }
170+ >
171+ < div className = "d-flex align-items-center gap-2" >
172+ { caretPosition === "left" && renderCaret ( ) }
174173 < span > { title } </ span >
174+ { caretPosition === "right" && renderCaret ( ) }
175+ </ div >
176+
177+ { ( rightContent || caretPosition === "end" ) && (
178+ < div className = "ms-auto d-flex align-items-center gap-2" >
179+ { caretPosition === "end" && renderCaret ( ) }
180+ { rightContent }
181+ </ div >
182+ ) }
183+ </ CollapseHeaderButton >
175184
176- < span className = "ms-auto d-inline-flex gap-2 align-items-center" >
177- { containerStatus && (
178- < ContainerStatus state = { parseContainerState ( containerStatus ) } />
179- ) }
180- < CollapseCaret open = { open } />
181- </ span >
182- </ CollapseHeaderButton >
183- </ Card . Header >
184-
185- < Collapse in = { open } >
186- < div className = { `border-top ${ isParent ? "p-3" : "p-2" } ` } >
185+ < Collapse in = { open } onEntered = { handleScrollIntoView } >
186+ < div className = { `${ bordered ? "border-top px-3 py-2" : "" } ` } >
187187 { children }
188188 </ div >
189189 </ Collapse >
190- </ Card >
190+ </ div >
191191 ) ;
192192} ;
193193
0 commit comments