diff --git a/packages/main/src/components/MessageItem/index.tsx b/packages/main/src/components/MessageItem/index.tsx index 5b2a0774d7b..8a96228e8c8 100644 --- a/packages/main/src/components/MessageItem/index.tsx +++ b/packages/main/src/components/MessageItem/index.tsx @@ -7,7 +7,7 @@ import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js'; import iconArrowRight from '@ui5/webcomponents-icons/dist/slim-arrow-right.js'; import { useI18nBundle, useStylesheet } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; -import { Children, isValidElement, forwardRef, useContext, useEffect, useRef, useState } from 'react'; +import { Children, forwardRef, useContext, useEffect, useRef, useState } from 'react'; import type { ReactNode } from 'react'; import { FlexBoxAlignItems } from '../../enums/FlexBoxAlignItems.js'; import { FlexBoxDirection } from '../../enums/FlexBoxDirection.js'; @@ -16,11 +16,10 @@ import { MessageViewContext } from '../../internal/MessageViewContext.js'; import type { CommonProps } from '../../types/index.js'; import { Icon } from '../../webComponents/Icon/index.js'; import { Label } from '../../webComponents/Label/index.js'; -import type { LinkPropTypes } from '../../webComponents/Link/index.js'; import type { ListItemCustomDomRef, ListItemCustomPropTypes } from '../../webComponents/ListItemCustom/index.js'; import { ListItemCustom } from '../../webComponents/ListItemCustom/index.js'; import { FlexBox } from '../FlexBox/index.js'; -import { getIconNameForType, getValueStateMap } from '../MessageView/utils.js'; +import { getIconNameForType, getValueStateMap, resolveTitleTextStr } from '../MessageView/utils.js'; import { classNames, styleData } from './MessageItem.module.css.js'; export interface MessageItemPropTypes @@ -69,14 +68,7 @@ const MessageItem = forwardRef((prop const titleTextRef = useRef(null); const hasDetails = !!(children || isTitleTextOverflowing); const i18nBundle = useI18nBundle('@ui5/webcomponents-react'); - const titleTextStr = (() => { - if (typeof titleText === 'string') { - return titleText; - } else if (isValidElement(titleText) && typeof titleText.props.children === 'string') { - return titleText.props.children; - } - return ''; - })(); + const titleTextStr = resolveTitleTextStr(titleText); useStylesheet(styleData, MessageItem.displayName); diff --git a/packages/main/src/components/MessageView/MessageView.cy.tsx b/packages/main/src/components/MessageView/MessageView.cy.tsx index 9060fedc806..2f837b90029 100644 --- a/packages/main/src/components/MessageView/MessageView.cy.tsx +++ b/packages/main/src/components/MessageView/MessageView.cy.tsx @@ -1,7 +1,7 @@ import WrappingType from '@ui5/webcomponents/dist/types/WrappingType.js'; import ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js'; import { Link } from '@ui5/webcomponents-react'; -import { useRef } from 'react'; +import { useRef, useState } from 'react'; import { Dialog } from '../../webComponents/Dialog/index.js'; import { MessageItem } from '../MessageItem/index.js'; import { MessageView } from './index.js'; @@ -184,6 +184,56 @@ describe('MessageView', () => { cy.findByText('1337').should('not.exist'); }); + it('remove MessageItems', () => { + const TestComp = () => { + const [showInfo, setShowInfo] = useState(true); + const [showNeg, setShowNeg] = useState(true); + return ( + <> + + + + {showInfo && ( + + Info Body + + )} + {showNeg && ( + + Neg Body + + )} + + Neg2 Body + + + + ); + }; + cy.mount(); + + cy.get('[icon="information"]').click(); + cy.findByText('InfoTitle').click(); + cy.findByText('Info Body').should('be.visible'); + cy.get('[data-component-name="MessageViewDetailsNavBackBtn"]').should('exist'); + + // removing an unrelated item leaves both filter + details intact + cy.findByTestId('remove-neg').click(); + cy.findByText('Info Body').should('be.visible'); + cy.get('[data-component-name="MessageViewDetailsNavBackBtn"]').should('exist'); + + // removing the open + filtered-type item: details collapse, filter falls back, Bar unmounts + cy.findByTestId('remove-info').click(); + cy.findByText('Info Body').should('not.exist'); + cy.get('[data-component-name="MessageViewDetailsNavBackBtn"]').should('not.exist'); + cy.findByText('Neg2Title').should('be.visible'); + cy.get('[ui5-bar]').should('not.exist'); + }); + it('MessageItem - titleText overflow', () => { const selectSpy = cy.spy().as('select'); cy.mount( diff --git a/packages/main/src/components/MessageView/index.tsx b/packages/main/src/components/MessageView/index.tsx index a7f5b1ea83a..cf530291a39 100644 --- a/packages/main/src/components/MessageView/index.tsx +++ b/packages/main/src/components/MessageView/index.tsx @@ -32,7 +32,7 @@ import { Title } from '../../webComponents/Title/index.js'; import { FlexBox } from '../FlexBox/index.js'; import type { MessageItemPropTypes } from '../MessageItem/index.js'; import { classNames, styleData } from './MessageView.module.css.js'; -import { getIconNameForType, getValueStateMap } from './utils.js'; +import { getIconNameForType, getValueStateMap, resolveTitleTextStr } from './utils.js'; export interface MessageViewDomRef extends HTMLDivElement { /** @@ -131,19 +131,37 @@ const MessageView = forwardRef((props, const childrenArray = Children.toArray(children); const messageTypes = resolveMessageTypes(childrenArray as ReactElement[]); const filledTypes = Object.values(messageTypes).filter((count) => count > 0).length; + // fallback to All if selected filter is removed + const effectiveListFilter: ValueState | 'All' = + listFilter !== 'All' && messageTypes[listFilter] === 0 ? 'All' : listFilter; + + // collapse details pane if the open MessageItem is no longer in children + const effectiveSelectedMessage: SelectedMessage | null = + selectedMessage !== null && + childrenArray.some((child) => { + if (!isValidElement(child)) { + return false; + } + return ( + resolveTitleTextStr(child.props.titleText) === selectedMessage.titleTextStr && + child.props.type === selectedMessage.type + ); + }) + ? selectedMessage + : null; const filteredChildren = - listFilter === 'All' + effectiveListFilter === 'All' ? childrenArray : childrenArray.filter((message) => { if (!isValidElement(message)) { return false; } const castMessage = message as ReactElement; - if (listFilter === ValueState.Information) { + if (effectiveListFilter === ValueState.Information) { return castMessage?.props?.type === ValueState.Information || castMessage?.props?.type === ValueState.None; } - return castMessage?.props?.type === listFilter; + return castMessage?.props?.type === effectiveListFilter; }); const groupedMessages = resolveMessageGroups(filteredChildren as ReactElement[]); @@ -194,7 +212,7 @@ const MessageView = forwardRef((props, } }; - const outerClasses = clsx(classNames.container, className, selectedMessage && classNames.showDetails); + const outerClasses = clsx(classNames.container, className, effectiveSelectedMessage && classNames.showDetails); return (
((props, }} >
- {!selectedMessage && ( + {!effectiveSelectedMessage && ( <> {filledTypes > 1 && ( ((props, onSelectionChange={handleListFilterChange} accessibleName={i18nBundle.getText(MESSAGE_TYPES)} > - + {i18nBundle.getText(ALL)} {/* @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((props, ((props,
{childrenArray.length > 0 ? ( <> - {showDetailsPageHeader && selectedMessage && ( + {showDetailsPageHeader && effectiveSelectedMessage && ( ((props, } /> )} - {selectedMessage && ( + {effectiveSelectedMessage && ( - {selectedMessage.titleText} + {effectiveSelectedMessage.titleText} -
{selectedMessage.children}
+
{effectiveSelectedMessage.children}
)} diff --git a/packages/main/src/components/MessageView/utils.ts b/packages/main/src/components/MessageView/utils.ts index 50727829d69..b7dc9227d33 100644 --- a/packages/main/src/components/MessageView/utils.ts +++ b/packages/main/src/components/MessageView/utils.ts @@ -4,7 +4,10 @@ import iconAlert from '@ui5/webcomponents-icons/dist/alert.js'; import iconError from '@ui5/webcomponents-icons/dist/error.js'; import iconInformation from '@ui5/webcomponents-icons/dist/information.js'; import iconSysEnter from '@ui5/webcomponents-icons/dist/sys-enter-2.js'; +import { isValidElement } from 'react'; +import type { ReactNode } from 'react'; import { ERROR, WARNING, SUCCESS, INFORMATION } from '../../i18n/i18n-defaults.js'; +import type { LinkPropTypes } from '../../webComponents/Link/index.js'; export const getIconNameForType = (type: ValueState | keyof typeof ValueState): string => { switch (type) { @@ -29,3 +32,16 @@ export const getValueStateMap = (i18nBundle: I18nBundle) => ({ [ValueState.Information]: i18nBundle.getText(INFORMATION), [ValueState.None]: i18nBundle.getText(INFORMATION), }); + +export const resolveTitleTextStr = (titleText: ReactNode): string => { + if (typeof titleText === 'string' || typeof titleText === 'number') { + return String(titleText); + } + if (isValidElement(titleText)) { + const linkChild = titleText.props.children; + if (typeof linkChild === 'string' || typeof linkChild === 'number') { + return String(linkChild); + } + } + return ''; +};