Skip to content

Commit dfa4e06

Browse files
authored
fix(MessageView): reset filter and details when children change (UI5#8717)
Fixes UI5#8707
1 parent 9dff410 commit dfa4e06

4 files changed

Lines changed: 107 additions & 28 deletions

File tree

packages/main/src/components/MessageItem/index.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js';
77
import iconArrowRight from '@ui5/webcomponents-icons/dist/slim-arrow-right.js';
88
import { useI18nBundle, useStylesheet } from '@ui5/webcomponents-react-base';
99
import { clsx } from 'clsx';
10-
import { Children, isValidElement, forwardRef, useContext, useEffect, useRef, useState } from 'react';
10+
import { Children, forwardRef, useContext, useEffect, useRef, useState } from 'react';
1111
import type { ReactNode } from 'react';
1212
import { FlexBoxAlignItems } from '../../enums/FlexBoxAlignItems.js';
1313
import { FlexBoxDirection } from '../../enums/FlexBoxDirection.js';
@@ -16,11 +16,10 @@ import { MessageViewContext } from '../../internal/MessageViewContext.js';
1616
import type { CommonProps } from '../../types/index.js';
1717
import { Icon } from '../../webComponents/Icon/index.js';
1818
import { Label } from '../../webComponents/Label/index.js';
19-
import type { LinkPropTypes } from '../../webComponents/Link/index.js';
2019
import type { ListItemCustomDomRef, ListItemCustomPropTypes } from '../../webComponents/ListItemCustom/index.js';
2120
import { ListItemCustom } from '../../webComponents/ListItemCustom/index.js';
2221
import { FlexBox } from '../FlexBox/index.js';
23-
import { getIconNameForType, getValueStateMap } from '../MessageView/utils.js';
22+
import { getIconNameForType, getValueStateMap, resolveTitleTextStr } from '../MessageView/utils.js';
2423
import { classNames, styleData } from './MessageItem.module.css.js';
2524

2625
export interface MessageItemPropTypes
@@ -69,14 +68,7 @@ const MessageItem = forwardRef<ListItemCustomDomRef, MessageItemPropTypes>((prop
6968
const titleTextRef = useRef<HTMLSpanElement>(null);
7069
const hasDetails = !!(children || isTitleTextOverflowing);
7170
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
72-
const titleTextStr = (() => {
73-
if (typeof titleText === 'string') {
74-
return titleText;
75-
} else if (isValidElement<LinkPropTypes>(titleText) && typeof titleText.props.children === 'string') {
76-
return titleText.props.children;
77-
}
78-
return '';
79-
})();
71+
const titleTextStr = resolveTitleTextStr(titleText);
8072

8173
useStylesheet(styleData, MessageItem.displayName);
8274

packages/main/src/components/MessageView/MessageView.cy.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import WrappingType from '@ui5/webcomponents/dist/types/WrappingType.js';
22
import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js';
33
import { Link } from '@ui5/webcomponents-react';
4-
import { useRef } from 'react';
4+
import { useRef, useState } from 'react';
55
import { Dialog } from '../../webComponents/Dialog/index.js';
66
import { MessageItem } from '../MessageItem/index.js';
77
import { MessageView } from './index.js';
@@ -184,6 +184,56 @@ describe('MessageView', () => {
184184
cy.findByText('1337').should('not.exist');
185185
});
186186

187+
it('remove MessageItems', () => {
188+
const TestComp = () => {
189+
const [showInfo, setShowInfo] = useState(true);
190+
const [showNeg, setShowNeg] = useState(true);
191+
return (
192+
<>
193+
<button data-testid="remove-info" onClick={() => setShowInfo(false)}>
194+
remove info
195+
</button>
196+
<button data-testid="remove-neg" onClick={() => setShowNeg(false)}>
197+
remove neg
198+
</button>
199+
<MessageView showDetailsPageHeader>
200+
{showInfo && (
201+
<MessageItem titleText="InfoTitle" type={ValueState.Information}>
202+
Info Body
203+
</MessageItem>
204+
)}
205+
{showNeg && (
206+
<MessageItem titleText="NegTitle" type={ValueState.Negative}>
207+
Neg Body
208+
</MessageItem>
209+
)}
210+
<MessageItem titleText="Neg2Title" type={ValueState.Negative}>
211+
Neg2 Body
212+
</MessageItem>
213+
</MessageView>
214+
</>
215+
);
216+
};
217+
cy.mount(<TestComp />);
218+
219+
cy.get('[icon="information"]').click();
220+
cy.findByText('InfoTitle').click();
221+
cy.findByText('Info Body').should('be.visible');
222+
cy.get('[data-component-name="MessageViewDetailsNavBackBtn"]').should('exist');
223+
224+
// removing an unrelated item leaves both filter + details intact
225+
cy.findByTestId('remove-neg').click();
226+
cy.findByText('Info Body').should('be.visible');
227+
cy.get('[data-component-name="MessageViewDetailsNavBackBtn"]').should('exist');
228+
229+
// removing the open + filtered-type item: details collapse, filter falls back, Bar unmounts
230+
cy.findByTestId('remove-info').click();
231+
cy.findByText('Info Body').should('not.exist');
232+
cy.get('[data-component-name="MessageViewDetailsNavBackBtn"]').should('not.exist');
233+
cy.findByText('Neg2Title').should('be.visible');
234+
cy.get('[ui5-bar]').should('not.exist');
235+
});
236+
187237
it('MessageItem - titleText overflow', () => {
188238
const selectSpy = cy.spy().as('select');
189239
cy.mount(

packages/main/src/components/MessageView/index.tsx

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { Title } from '../../webComponents/Title/index.js';
3232
import { FlexBox } from '../FlexBox/index.js';
3333
import type { MessageItemPropTypes } from '../MessageItem/index.js';
3434
import { classNames, styleData } from './MessageView.module.css.js';
35-
import { getIconNameForType, getValueStateMap } from './utils.js';
35+
import { getIconNameForType, getValueStateMap, resolveTitleTextStr } from './utils.js';
3636

3737
export interface MessageViewDomRef extends HTMLDivElement {
3838
/**
@@ -131,19 +131,37 @@ const MessageView = forwardRef<MessageViewDomRef, MessageViewPropTypes>((props,
131131
const childrenArray = Children.toArray(children);
132132
const messageTypes = resolveMessageTypes(childrenArray as ReactElement<MessageItemPropTypes>[]);
133133
const filledTypes = Object.values(messageTypes).filter((count) => count > 0).length;
134+
// fallback to All if selected filter is removed
135+
const effectiveListFilter: ValueState | 'All' =
136+
listFilter !== 'All' && messageTypes[listFilter] === 0 ? 'All' : listFilter;
137+
138+
// collapse details pane if the open MessageItem is no longer in children
139+
const effectiveSelectedMessage: SelectedMessage | null =
140+
selectedMessage !== null &&
141+
childrenArray.some((child) => {
142+
if (!isValidElement<MessageItemPropTypes>(child)) {
143+
return false;
144+
}
145+
return (
146+
resolveTitleTextStr(child.props.titleText) === selectedMessage.titleTextStr &&
147+
child.props.type === selectedMessage.type
148+
);
149+
})
150+
? selectedMessage
151+
: null;
134152

135153
const filteredChildren =
136-
listFilter === 'All'
154+
effectiveListFilter === 'All'
137155
? childrenArray
138156
: childrenArray.filter((message) => {
139157
if (!isValidElement(message)) {
140158
return false;
141159
}
142160
const castMessage = message as ReactElement<MessageItemPropTypes>;
143-
if (listFilter === ValueState.Information) {
161+
if (effectiveListFilter === ValueState.Information) {
144162
return castMessage?.props?.type === ValueState.Information || castMessage?.props?.type === ValueState.None;
145163
}
146-
return castMessage?.props?.type === listFilter;
164+
return castMessage?.props?.type === effectiveListFilter;
147165
});
148166

149167
const groupedMessages = resolveMessageGroups(filteredChildren as ReactElement<MessageItemPropTypes>[]);
@@ -194,7 +212,7 @@ const MessageView = forwardRef<MessageViewDomRef, MessageViewPropTypes>((props,
194212
}
195213
};
196214

197-
const outerClasses = clsx(classNames.container, className, selectedMessage && classNames.showDetails);
215+
const outerClasses = clsx(classNames.container, className, effectiveSelectedMessage && classNames.showDetails);
198216
return (
199217
<div
200218
ref={componentRef}
@@ -209,10 +227,13 @@ const MessageView = forwardRef<MessageViewDomRef, MessageViewPropTypes>((props,
209227
}}
210228
>
211229
<div
212-
style={{ visibility: selectedMessage ? 'hidden' : 'visible', opacity: selectedMessage ? 0.3 : 1 }}
230+
style={{
231+
visibility: effectiveSelectedMessage ? 'hidden' : 'visible',
232+
opacity: effectiveSelectedMessage ? 0.3 : 1,
233+
}}
213234
className={classNames.messagesContainer}
214235
>
215-
{!selectedMessage && (
236+
{!effectiveSelectedMessage && (
216237
<>
217238
{filledTypes > 1 && (
218239
<Bar
@@ -221,7 +242,7 @@ const MessageView = forwardRef<MessageViewDomRef, MessageViewPropTypes>((props,
221242
onSelectionChange={handleListFilterChange}
222243
accessibleName={i18nBundle.getText(MESSAGE_TYPES)}
223244
>
224-
<SegmentedButtonItem data-key="All" selected={listFilter === 'All'}>
245+
<SegmentedButtonItem data-key="All" selected={effectiveListFilter === 'All'}>
225246
{i18nBundle.getText(ALL)}
226247
</SegmentedButtonItem>
227248
{/* @ts-expect-error: The key can't be typed, it's always `string`, but since the `ValueState` enum only contains strings it's fine to use here*/}
@@ -233,7 +254,7 @@ const MessageView = forwardRef<MessageViewDomRef, MessageViewPropTypes>((props,
233254
<SegmentedButtonItem
234255
key={valueState}
235256
data-key={valueState}
236-
selected={listFilter === valueState}
257+
selected={effectiveListFilter === valueState}
237258
icon={getIconNameForType(valueState)}
238259
className={classNames.button}
239260
tooltip={getValueStateMap(i18nBundle)[valueState]}
@@ -271,12 +292,12 @@ const MessageView = forwardRef<MessageViewDomRef, MessageViewPropTypes>((props,
271292
</div>
272293
<div
273294
className={classNames.detailsContainer}
274-
style={{ opacity: selectedMessage ? 1 : 0.3 }}
295+
style={{ opacity: effectiveSelectedMessage ? 1 : 0.3 }}
275296
data-component-name="MessageViewDetailsContainer"
276297
>
277298
{childrenArray.length > 0 ? (
278299
<>
279-
{showDetailsPageHeader && selectedMessage && (
300+
{showDetailsPageHeader && effectiveSelectedMessage && (
280301
<Bar
281302
startContent={
282303
<Button
@@ -291,18 +312,18 @@ const MessageView = forwardRef<MessageViewDomRef, MessageViewPropTypes>((props,
291312
}
292313
/>
293314
)}
294-
{selectedMessage && (
315+
{effectiveSelectedMessage && (
295316
<FlexBox className={classNames.details}>
296317
<Icon
297-
data-type={selectedMessage.type ?? ValueState.Negative}
298-
name={getIconNameForType(selectedMessage.type)}
318+
data-type={effectiveSelectedMessage.type ?? ValueState.Negative}
319+
name={getIconNameForType(effectiveSelectedMessage.type)}
299320
className={classNames.detailsIcon}
300321
/>
301322
<FlexBox direction={FlexBoxDirection.Column} className={classNames.detailsTextContainer}>
302323
<Title level={TitleLevel.H5} className={classNames.detailsTitle} wrappingType={WrappingType.Normal}>
303-
{selectedMessage.titleText}
324+
{effectiveSelectedMessage.titleText}
304325
</Title>
305-
<div className={classNames.detailsText}>{selectedMessage.children}</div>
326+
<div className={classNames.detailsText}>{effectiveSelectedMessage.children}</div>
306327
</FlexBox>
307328
</FlexBox>
308329
)}

packages/main/src/components/MessageView/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import iconAlert from '@ui5/webcomponents-icons/dist/alert.js';
44
import iconError from '@ui5/webcomponents-icons/dist/error.js';
55
import iconInformation from '@ui5/webcomponents-icons/dist/information.js';
66
import iconSysEnter from '@ui5/webcomponents-icons/dist/sys-enter-2.js';
7+
import { isValidElement } from 'react';
8+
import type { ReactNode } from 'react';
79
import { ERROR, WARNING, SUCCESS, INFORMATION } from '../../i18n/i18n-defaults.js';
10+
import type { LinkPropTypes } from '../../webComponents/Link/index.js';
811

912
export const getIconNameForType = (type: ValueState | keyof typeof ValueState): string => {
1013
switch (type) {
@@ -29,3 +32,16 @@ export const getValueStateMap = (i18nBundle: I18nBundle) => ({
2932
[ValueState.Information]: i18nBundle.getText(INFORMATION),
3033
[ValueState.None]: i18nBundle.getText(INFORMATION),
3134
});
35+
36+
export const resolveTitleTextStr = (titleText: ReactNode): string => {
37+
if (typeof titleText === 'string' || typeof titleText === 'number') {
38+
return String(titleText);
39+
}
40+
if (isValidElement<LinkPropTypes>(titleText)) {
41+
const linkChild = titleText.props.children;
42+
if (typeof linkChild === 'string' || typeof linkChild === 'number') {
43+
return String(linkChild);
44+
}
45+
}
46+
return '';
47+
};

0 commit comments

Comments
 (0)