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,89 @@ 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 ;
93+ rightContent ?: React . ReactNode ;
94+ caretPosition ?: "left" | "right" | "end" ;
10595 open : boolean ;
10696 onToggle : ( ) => void ;
107- containerStatus ?: string | null ;
108- isInsideTable ?: boolean ;
109- type ?: CollapseType ;
110- }
97+ showToggleTooltip ?: boolean ;
98+ style ?: React . CSSProperties ;
99+ className ?: string ;
100+ headerClassName ?: string ;
101+ contentClassName ?: string ;
102+ } ;
111103
112104const CollapseItem = ( {
113105 title,
114106 children,
107+ rightContent,
108+ caretPosition,
115109 open,
116110 onToggle,
117- containerStatus,
118- isInsideTable = false ,
119- type = "card-child" ,
111+ showToggleTooltip = false ,
112+ style,
113+ className,
114+ headerClassName,
115+ contentClassName,
120116} : CollapseItemProps ) => {
121117 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- }
118+ const containerRef = useRef < HTMLDivElement | null > ( null ) ;
119+
120+ const handleScrollIntoView = useCallback ( ( ) => {
121+ if ( ! containerRef . current ) return ;
122+
123+ const rect = containerRef . current . getBoundingClientRect ( ) ;
124+ if ( rect . bottom > window . innerHeight ) {
125+ containerRef . current . scrollIntoView ( {
126+ behavior : "smooth" ,
127+ block : "nearest" ,
128+ } ) ;
129+ }
130+ } , [ ] ) ;
131+
132+ const collapseTitle = showToggleTooltip
133+ ? open
134+ ? intl . formatMessage ( {
135+ id : "components.CollapseItem.collapseList" ,
136+ defaultMessage : "Collapse list" ,
137+ } )
138+ : intl . formatMessage ( {
139+ id : "components.CollapseItem.expandList" ,
140+ defaultMessage : "Expand list" ,
141+ } )
142+ : undefined ;
143+
144+ const renderCaret = ( ) => < CollapseCaret open = { open } /> ;
164145
165146 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- >
147+ < div ref = { containerRef } className = { className } >
148+ < CollapseHeaderButton
149+ open = { open }
150+ onToggle = { onToggle }
151+ title = { collapseTitle }
152+ className = { `d-flex align-items-center w-100 ${ headerClassName ?? "" } ` }
153+ style = { style }
154+ >
155+ < div className = "d-flex align-items-center gap-2" >
156+ { caretPosition === "left" && renderCaret ( ) }
174157 < span > { title } </ span >
175-
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" } ` } >
187- { children }
158+ { caretPosition === "right" && renderCaret ( ) }
188159 </ div >
160+
161+ { ( rightContent || caretPosition === "end" ) && (
162+ < div className = "ms-auto d-flex align-items-center gap-2" >
163+ { caretPosition === "end" && renderCaret ( ) }
164+ { rightContent }
165+ </ div >
166+ ) }
167+ </ CollapseHeaderButton >
168+
169+ < Collapse in = { open } onEntered = { handleScrollIntoView } >
170+ < div className = { contentClassName } > { children } </ div >
189171 </ Collapse >
190- </ Card >
172+ </ div >
191173 ) ;
192174} ;
193175
0 commit comments