Skip to content

Commit 43e0021

Browse files
committed
feat: handle very long description in detailedList filterOption description
1 parent a8f7415 commit 43e0021

File tree

3 files changed

+184
-8
lines changed

3 files changed

+184
-8
lines changed

src/components/chat-item/chat-item-form-items.ts

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import { Config } from '../../helper/config';
77
import { DomBuilder, ExtendedHTMLElement } from '../../helper/dom';
88
import { cancelEvent } from '../../helper/events';
9+
import { StyleLoader } from '../../helper/style-loader';
910
import testIds from '../../helper/test-ids';
1011
import { isMandatoryItemValid, isTextualFormItemValid } from '../../helper/validator';
1112
import { ChatItem, ChatItemFormItem, TextBasedFormItem } from '../../static';
13+
import { Button } from '../button';
1214
import { Card } from '../card/card';
1315
import { CardBody } from '../card/card-body';
1416
import { 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
}

src/styles/components/_detailed-list.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
display: none;
4040
}
4141
> .mynah-chat-item-form-items-container {
42-
gap: var(--mynah-sizing-4);
42+
gap: var(--mynah-sizing-5);
4343
}
4444
}
4545
> .mynah-detailed-list-filter-actions-wrapper {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*!
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
.mynah-ui-form-item-description-collapsible {
7+
display: block;
8+
box-sizing: border-box;
9+
width: 100%;
10+
position: relative;
11+
color: var(--mynah-color-text-default);
12+
font-size: var(--mynah-font-size-small);
13+
line-height: var(--mynah-line-height);
14+
transition: all 0.3s ease;
15+
16+
&.collapsed {
17+
display: -webkit-box;
18+
-webkit-line-clamp: 3;
19+
-webkit-box-orient: vertical;
20+
overflow: hidden;
21+
text-overflow: ellipsis;
22+
max-height: calc(var(--mynah-line-height) * 3);
23+
position: relative;
24+
25+
&::before {
26+
content: '';
27+
position: absolute;
28+
top: var(--mynah-line-height);
29+
left: 0;
30+
right: 0;
31+
bottom: 0;
32+
background: linear-gradient(to bottom, transparent 0%, var(--mynah-color-bg) 100%);
33+
pointer-events: none;
34+
}
35+
}
36+
37+
&:not(.collapsed) {
38+
overflow: visible;
39+
}
40+
}
41+
42+
.mynah-ui-form-item-description-toggle {
43+
display: flex;
44+
justify-content: flex-end;
45+
align-items: center;
46+
padding: var(--mynah-sizing-1) var(--mynah-sizing-1) var(--mynah-sizing-1) var(--mynah-sizing-2);
47+
margin-top: var(--mynah-sizing-1);
48+
border: none;
49+
background: transparent;
50+
cursor: pointer;
51+
transition: var(--mynah-very-short-transition);
52+
53+
.mynah-button-label {
54+
font-size: var(--mynah-font-size-xsmall);
55+
font-weight: 500;
56+
text-align: right;
57+
flex-shrink: 0;
58+
}
59+
60+
&:focus {
61+
background: transparent;
62+
}
63+
}

0 commit comments

Comments
 (0)