From aa09b596deadc452b2e9754375236da90f68e0ee Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Thu, 29 Oct 2026 08:50:49 +0100 Subject: [PATCH] fix(MessageView): improve details-page accessibility and navigation --- .../MessageItem/MessageItem.module.css | 7 --- .../main/src/components/MessageItem/index.tsx | 15 ++++-- .../components/MessageView/MessageView.mdx | 10 ++++ .../MessageView/MessageView.stories.tsx | 13 +++++ .../main/src/components/MessageView/index.tsx | 43 +++++++++++++---- .../MessageView/test/MessageView.gallery.tsx | 48 +++++++++++++++++++ .../MessageView/test/MessageView.spec.tsx | 35 ++++++++++++++ 7 files changed, 153 insertions(+), 18 deletions(-) create mode 100644 packages/main/src/components/MessageView/test/MessageView.gallery.tsx create mode 100644 packages/main/src/components/MessageView/test/MessageView.spec.tsx diff --git a/packages/main/src/components/MessageItem/MessageItem.module.css b/packages/main/src/components/MessageItem/MessageItem.module.css index f2db199a590..7b30343264a 100644 --- a/packages/main/src/components/MessageItem/MessageItem.module.css +++ b/packages/main/src/components/MessageItem/MessageItem.module.css @@ -65,13 +65,6 @@ flex: none; } -.navigation { - height: 0.875rem; - width: 0.875rem; - padding: 0 0.9375rem; - flex-shrink: 0; -} - .typeNegative { .icon { color: var(--sapNegativeElementColor); diff --git a/packages/main/src/components/MessageItem/index.tsx b/packages/main/src/components/MessageItem/index.tsx index 8a96228e8c8..fcebe2d400c 100644 --- a/packages/main/src/components/MessageItem/index.tsx +++ b/packages/main/src/components/MessageItem/index.tsx @@ -4,8 +4,8 @@ import IconMode from '@ui5/webcomponents/dist/types/IconMode.js'; import ListItemType from '@ui5/webcomponents/dist/types/ListItemType.js'; import WrappingType from '@ui5/webcomponents/dist/types/WrappingType.js'; 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 { addCustomCSSWithScoping } from '@ui5/webcomponents-react-base/internal/utils'; import { clsx } from 'clsx'; import { Children, forwardRef, useContext, useEffect, useRef, useState } from 'react'; import type { ReactNode } from 'react'; @@ -22,6 +22,15 @@ import { FlexBox } from '../FlexBox/index.js'; import { getIconNameForType, getValueStateMap, resolveTitleTextStr } from '../MessageView/utils.js'; import { classNames, styleData } from './MessageItem.module.css.js'; +// TODO: remove once https://github.com/UI5/webcomponents/issues/14221 is fixed (navigation arrow shrinks on long titles) +addCustomCSSWithScoping( + 'ui5-li-custom', + ` +:host([data-message-item]) [ui5-icon][name="slim-arrow-right"] { + flex-shrink: 0; +}`, +); + export interface MessageItemPropTypes extends Pick, Omit { /** @@ -131,9 +140,10 @@ const MessageItem = forwardRef((prop onKeyDown={handleKeyDown} data-title={titleTextStr} data-type={type} - type={hasDetails ? ListItemType.Active : ListItemType.Inactive} + type={hasDetails ? ListItemType.Navigation : ListItemType.Inactive} {...rest} className={listItemClasses} + data-message-item ref={ref} > @@ -157,7 +167,6 @@ const MessageItem = forwardRef((prop {counter} )} - {hasDetails && } {hasDetails && . {i18nBundle.getText(HAS_DETAILS)}} {type !== ValueState.None && type !== ValueState.Information && ( . {getValueStateMap(i18nBundle)[type]} diff --git a/packages/main/src/components/MessageView/MessageView.mdx b/packages/main/src/components/MessageView/MessageView.mdx index 4d18db23517..fc991356e46 100644 --- a/packages/main/src/components/MessageView/MessageView.mdx +++ b/packages/main/src/components/MessageView/MessageView.mdx @@ -39,6 +39,12 @@ When used in a `Dialog` or a `Popover`, we recommend not setting the `showDetail to the `onItemSelect` event and add a back button to your Dialog or Popover header and use the `navigateBack()` method to get back to the list view. +## Accessibility + +When the details page is opened, `MessageView` announces the opened message (its value state and title) to screen readers and, with `showDetailsPageHeader={true}`, also handles focus for the details page. + +With `showDetailsPageHeader={false}`, focus is not managed, so the details page needs to be made accessible manually — e.g. move focus to an interactive element (such as a back button) and provide any additional information for assistive technologies in the `onItemSelect` handler (see the example below). + ## More Examples ### MessageView in a Dialog @@ -50,6 +56,7 @@ Sample Code: _(Button and Dialog opening are omitted here)_ ```tsx function MyComponent() { const messageViewRef = useRef(null); + const backBtnRef = useRef(null); const [isOnDetailsPage, setIsOnDetailsPage] = useState(false); return ( {isOnDetailsPage && ( @@ -186,6 +193,7 @@ export const WithMessageViewButton: Story = { render() { const ref = useRef(null); const messageViewRef = useRef(null); + const backBtnRef = useRef(null); const [isOnDetailsPage, setIsOnDetailsPage] = useState(false); const [open, setOpen] = useState(false); const numberOfItems = { @@ -219,6 +227,7 @@ export const WithMessageViewButton: Story = { {isOnDetailsPage && ( + { + // no header → app ensures a11y by moving focus to its own (interactive) back button + requestAnimationFrame(() => { + backBtnRef.current?.focus(); + }); + }} + > + + The password must contain at least two character types. + + + Please review the highlighted fields. + + + + ); +}; diff --git a/packages/main/src/components/MessageView/test/MessageView.spec.tsx b/packages/main/src/components/MessageView/test/MessageView.spec.tsx new file mode 100644 index 00000000000..5b66e030727 --- /dev/null +++ b/packages/main/src/components/MessageView/test/MessageView.spec.tsx @@ -0,0 +1,35 @@ +import { expect, test } from '../../../../../../playwright/fixtures/gallery-fixtures.js'; + +test.describe('MessageView - accessibility', () => { + test('showDetailsPageHeader: focuses the Navigate Back button and announces the message', async ({ mount, page }) => { + await mount('MessageView/WithDetailsPageHeader'); + + await page.locator('[data-title="Password too short"]').click(); + + // focus moves to the Navigate Back button + const navBack = page.locator('[data-component-name="MessageViewDetailsNavBackBtn"]'); + await expect(navBack).toBeFocused(); + + // the message (value state + title) is announced, not a generic string + await expect(page.locator('.ui5-invisiblemessage-assertive')).toHaveText('Error Password too short'); + }); + + test('showDetailsPageHeader={false}: app moves focus to its own back button via onItemSelect', async ({ + mount, + page, + }) => { + await mount('MessageView/InDialogWithoutHeader'); + const dialog = page.locator('[ui5-dialog]'); + await expect(dialog).toBeVisible(); + + await page.locator('[data-title="Password too short"]').click(); + + // MessageView renders no details-page header and does not manage focus here + await expect(page.locator('[data-component-name="MessageViewDetailsNavBackBtn"]')).toHaveCount(0); + + // the app's onItemSelect handler moves focus to its own (interactive) back button, keeping focus in the dialog + const backButton = page.getByTestId('dialog-back'); + await expect(backButton).toBeFocused(); + await expect(dialog.getByTestId('dialog-back')).toHaveCount(1); + }); +});