66import { Config } from '../../helper/config' ;
77import { DomBuilder , ExtendedHTMLElement } from '../../helper/dom' ;
88import { cancelEvent } from '../../helper/events' ;
9+ import { StyleLoader } from '../../helper/style-loader' ;
910import testIds from '../../helper/test-ids' ;
1011import { isMandatoryItemValid , isTextualFormItemValid } from '../../helper/validator' ;
1112import { ChatItem , ChatItemFormItem , TextBasedFormItem } from '../../static' ;
13+ import { Button } from '../button' ;
1214import { Card } from '../card/card' ;
1315import { CardBody } from '../card/card-body' ;
1416import { Checkbox } from '../form-items/checkbox' ;
@@ -35,6 +37,7 @@ export class ChatItemFormItemsWrapper {
3537 private readonly props : ChatItemFormItemsWrapperProps ;
3638 private readonly options : Record < string , Select | TextArea | TextInput | RadioGroup | Stars | Checkbox | FormItemList | FormItemPillBox > = { } ;
3739 private readonly validationItems : Record < string , boolean > = { } ;
40+ private readonly descriptionStates : Record < string , boolean > = { } ;
3841 private isValid : boolean = false ;
3942 private tooltipOverlay : Overlay | null ;
4043 private tooltipTimeout : ReturnType < typeof setTimeout > ;
@@ -43,6 +46,7 @@ export class ChatItemFormItemsWrapper {
4346
4447 render : ExtendedHTMLElement ;
4548 constructor ( props : ChatItemFormItemsWrapperProps ) {
49+ StyleLoader . getInstance ( ) . load ( 'components/_form-item-description-collapsible.scss' ) ;
4650 this . props = props ;
4751 this . render = DomBuilder . getInstance ( ) . build ( {
4852 type : 'div' ,
@@ -75,16 +79,84 @@ export class ChatItemFormItemsWrapper {
7579 chatItemOption . value = chatItemOption . options ?. [ 0 ] ?. value ;
7680 }
7781 }
78- let description ;
82+ let description : ExtendedHTMLElement | undefined ;
83+
7984 if ( chatItemOption . description != null ) {
80- description = DomBuilder . getInstance ( ) . build ( {
81- type : 'span' ,
82- testId : testIds . chatItem . chatItemForm . description ,
85+ // Create a temporary element to check for overflow
86+ const tempDescriptionElement = DomBuilder . getInstance ( ) . build ( {
87+ type : 'div' ,
8388 classNames : [ 'mynah-ui-form-item-description' ] ,
84- children : [
85- chatItemOption . description ,
86- ]
89+ children : [ chatItemOption . description ] ,
90+ attributes : {
91+ style : 'position: absolute; visibility: hidden; pointer-events: none;'
92+ }
8793 } ) ;
94+
95+ // Add to DOM temporarily to measure overflow
96+ document . body . appendChild ( tempDescriptionElement ) ;
97+ const isLongDescription = this . hasDescriptionOverflow ( tempDescriptionElement ) ;
98+ document . body . removeChild ( tempDescriptionElement ) ;
99+
100+ const initialCollapsed = isLongDescription ; // Auto-collapse descriptions with overflow
101+
102+ if ( isLongDescription ) {
103+ // Initialize collapse state for long descriptions
104+ this . descriptionStates [ chatItemOption . id ] = initialCollapsed ;
105+
106+ const descriptionContent = DomBuilder . getInstance ( ) . build ( {
107+ type : 'div' ,
108+ classNames : [
109+ 'mynah-ui-form-item-description' ,
110+ 'mynah-ui-form-item-description-collapsible' ,
111+ ...( initialCollapsed ? [ 'collapsed' ] : [ ] )
112+ ] ,
113+ children : [ chatItemOption . description ]
114+ } ) ;
115+
116+ const toggleButton = new Button ( {
117+ label : initialCollapsed ? 'Show more' : 'Show less' ,
118+ primary : false ,
119+ status : 'clear' ,
120+ classNames : [ 'mynah-ui-form-item-description-toggle' ] ,
121+ onClick : ( e : Event ) => {
122+ cancelEvent ( e ) ;
123+ const isCurrentlyCollapsed = this . descriptionStates [ chatItemOption . id ] ;
124+ this . descriptionStates [ chatItemOption . id ] = ! isCurrentlyCollapsed ;
125+
126+ if ( isCurrentlyCollapsed ) {
127+ // Expand
128+ descriptionContent . removeClass ( 'collapsed' ) ;
129+ // Update button label by finding the label element
130+ const buttonLabel = toggleButton . render . querySelector ( '.mynah-button-label' ) ;
131+ if ( buttonLabel != null ) {
132+ buttonLabel . innerHTML = 'Show less' ;
133+ }
134+ } else {
135+ // Collapse
136+ descriptionContent . addClass ( 'collapsed' ) ;
137+ // Update button label by finding the label element
138+ const buttonLabel = toggleButton . render . querySelector ( '.mynah-button-label' ) ;
139+ if ( buttonLabel != null ) {
140+ buttonLabel . innerHTML = 'Show more' ;
141+ }
142+ }
143+ }
144+ } ) ;
145+
146+ description = DomBuilder . getInstance ( ) . build ( {
147+ type : 'div' ,
148+ testId : testIds . chatItem . chatItemForm . description ,
149+ children : [ descriptionContent , toggleButton . render ]
150+ } ) ;
151+ } else {
152+ // Regular non-collapsible description for short text
153+ description = DomBuilder . getInstance ( ) . build ( {
154+ type : 'span' ,
155+ testId : testIds . chatItem . chatItemForm . description ,
156+ classNames : [ 'mynah-ui-form-item-description' ] ,
157+ children : [ chatItemOption . description ]
158+ } ) ;
159+ }
88160 }
89161 const fireModifierAndEnterKeyPress = ( ) : void => {
90162 if ( ( chatItemOption as TextBasedFormItem ) . checkModifierEnterKeyPress === true && this . isFormValid ( ) ) {
@@ -323,6 +395,18 @@ export class ChatItemFormItemsWrapper {
323395 }
324396 } ;
325397
398+ private readonly hasDescriptionOverflow = ( element : HTMLElement ) : boolean => {
399+ // Get computed line height from CSS variables or fallback
400+ const computedStyle = window . getComputedStyle ( element ) ;
401+ const lineHeight = parseFloat ( computedStyle . lineHeight ) ;
402+ const fallbackLineHeight = isNaN ( lineHeight ) || lineHeight === 0 ? 20 : lineHeight ;
403+
404+ // Calculate if content would exceed 3 lines (the collapsed state shows 3 lines)
405+ const maxCollapsedHeight = fallbackLineHeight * 3 ;
406+
407+ return element . scrollHeight > maxCollapsedHeight ;
408+ } ;
409+
326410 private readonly getHandlers = ( chatItemOption : ChatItemFormItem ) : Object => {
327411 if ( chatItemOption . mandatory === true ||
328412 ( [ 'textarea' , 'textinput' , 'numericinput' , 'email' , 'pillbox' ] . includes ( chatItemOption . type ) && ( chatItemOption as TextBasedFormItem ) . validationPatterns != null ) ) {
@@ -387,4 +471,33 @@ export class ChatItemFormItemsWrapper {
387471 } ) ;
388472 return valueMap ;
389473 } ;
474+
475+ // Programmatic control methods for collapsible descriptions
476+ setDescriptionCollapsed = ( itemId : string , collapsed : boolean ) : void => {
477+ if ( this . descriptionStates [ itemId ] !== undefined ) {
478+ this . descriptionStates [ itemId ] = collapsed ;
479+ const descriptionElement = this . render . querySelector ( `[data-testid="${ testIds . chatItem . chatItemForm . description } "]` ) ;
480+ if ( descriptionElement !== null ) {
481+ if ( collapsed ) {
482+ descriptionElement . classList . add ( 'collapsed' ) ;
483+ } else {
484+ descriptionElement . classList . remove ( 'collapsed' ) ;
485+ }
486+ }
487+ }
488+ } ;
489+
490+ toggleDescriptionCollapsed = ( itemId : string ) : void => {
491+ if ( this . descriptionStates [ itemId ] !== undefined ) {
492+ this . setDescriptionCollapsed ( itemId , ! this . descriptionStates [ itemId ] ) ;
493+ }
494+ } ;
495+
496+ getDescriptionCollapsed = ( itemId : string ) : boolean | undefined => {
497+ return this . descriptionStates [ itemId ] ;
498+ } ;
499+
500+ getAllDescriptionStates = ( ) : Record < string , boolean > => {
501+ return { ...this . descriptionStates } ;
502+ } ;
390503}
0 commit comments