MessageView + MessageItem: missing accessible names, no aria-expanded, and broken focus/announcement on detail view

オープン
#9,112 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@Lukas742 がすでに取り組んでいます。

2026年10月29日 から。

  • #9115 @Lukas742 による — オープン

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
52/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
react, typescript

調査の方向性

Start with MessageItem/index.js and MessageView/index.js, where the issue identifies the accessible name, expansion state, focus, and announcement behavior. Check the ListItemAccessibilityAttributes type in the matching @ui5/webcomponents package and the existing component tests. Reproduce with the stated NVDA and Chrome setup, then verify both list item names and aria-expanded, plus focus and announcements when details open with animations enabled and disabled.

索引モデルが issue の本文から書いたものです。

説明

Summary

MessageView and its sub-component MessageItem have multiple WCAG A violations found in a production audit with NVDA + Chrome on @ui5/webcomponents-react ~2.27.2:

MessageItem (list view):

  1. Wrong or missing accessible name on all items — active items announce "Is Active"; inactive items announce nothing
  2. aria-expanded not set on items that have a detail panel

MessageView (detail view transition):
3. Focus goes to Navigate Back button, not the message content when the detail panel opens
4. Generic announcement — announces "Message Details" not the actual message text; if animations are disabled, nothing is announced at all


Environment

  • @ui5/webcomponents-react: ~2.27.2
  • @ui5/webcomponents: matching peer
  • Browser: Chrome + NVDA screen reader

Bug 1 (MessageItem): Wrong or missing accessible name on all list items

Expected (per docs)

The MessageItem Storybook docs state:

accessibleName — Defines the text alternative of the component.
Note: If not provided a default text alternative will be set, if present.

The expectation is that when accessibleName is not passed, the titleText content becomes the accessible name.

Actual (confirmed via live DOM inspection)

Active items (hasDetails === true, type={ListItemType.Active}):

When hasDetails === true, MessageItem sets type={ListItemType.Active} on ListItemCustom. This causes ui5-li-custom to inject an internal hidden span and wire it into aria-labelledby, which overrides all other name sources:

shadowAriaLabelledby: "ui5wc_99-invisibleText"
resolvedText: "Is Active"  ← what NVDA announces

NVDA announces "Is Active" for every message with a detail panel. The message text is visible but AT-invisible.

Inactive items (hasDetails === false, type={ListItemType.Inactive}):

shadowAriaLabelledby: "ui5wc_100-invisibleText"
resolvedText: ""  ← empty string, no name announced

The aria-labelledby target span exists in the shadow DOM but its text is empty. Per the ARIA name computation spec, when aria-labelledby references an element with empty text, the accessible name is "". NVDA announces nothing.

Root cause in MessageItem/index.js:

return _jsx(ListItemCustom, {
  type: hasDetails ? ListItemType.Active : ListItemType.Inactive,
  ...rest,
  // BUG: no accessibleName prop derived from titleText
  // consumer must pass it manually, but docs say a default is auto-set
  children: ...
});
Suggested fix

In MessageItem, explicitly derive accessibleName from titleText when the consumer has not provided one:

<ListItemCustom
  accessibleName={rest.accessibleName ?? titleTextStr}
  type={hasDetails ? ListItemType.Active : ListItemType.Inactive}
  {...rest}
>

Bug 2 (MessageItem): aria-expanded not exposed for items with a detail panel

Expected

When a MessageItem has children (or isTitleTextOverflowing), clicking it opens a detail panel within the same MessageView container. This is an expand/collapse interaction — aria-expanded should be present and toggle.

Actual
  • No aria-expanded on any item in any state (confirmed via live DOM)
  • The MessageItem component has no accessibilityAttributes prop in its public API (Storybook only lists: titleText, subtitleText, counter, type, groupName, children, accessibleName, onClick, tooltip, className, style)
  • ListItemAccessibilityAttributes is typed as Pick<AccessibilityAttributes, "hasPopup" | "ariaSetsize" | "ariaPosinset"> — expanded is excluded, so even passing accessibilityAttributes via ...rest cannot set aria-expanded

Note: aria-haspopup is not the right fix here — the detail panel opens within the same dialog container as an in-place expansion, not as a separate popup. aria-expanded is the correct attribute.

Suggested fix

Two changes needed:

1. MessageItem should set aria-expanded internally:

// MessageItem/index.js — add to the ListItemCustom props:
accessibilityAttributes={{
  ...rest.accessibilityAttributes,
  expanded: hasDetails ? 'true' : 'false'
}}

2. ListItemAccessibilityAttributes type needs expanded (in @ui5/webcomponents):

// Currently:
type ListItemAccessibilityAttributes = Pick<AccessibilityAttributes, "hasPopup" | "ariaSetsize" | "ariaPosinset">;
// Needed:
type ListItemAccessibilityAttributes = Pick<AccessibilityAttributes, "hasPopup" | "ariaSetsize" | "ariaPosinset" | "expanded">;

Bug 3 (MessageView): Wrong focus target when detail panel opens; focus may escape dialog when showDetailsPageHeader is false

Expected

When a user activates a MessageItem to open the detail panel, focus should move to the message content so screen reader users can immediately read it.

Actual

Case A — showDetailsPageHeader={true} (default): Focus moves to the Navigate Back button (<). Confirmed: the button receives a visible focus ring. A screen reader user hears "Navigate Back" — they must then navigate forward manually to reach the message text they just activated. The correct target is the message content.

Case B — showDetailsPageHeader={false}: No focus management runs at all. The browser moves focus to the nearest focusable ancestor of the disappearing list item, which in typical host layouts lands outside the dialog. Confirmed via Chrome DevTools Accessibility panel:

contentinfo ""
  toolbar "FloatingFooter"
  group "" focusable: true  focused: true   ← actual keyboard focus (outside dialog!)
dialog "Messages" modal: true
  ...
    heading "The password must contain at least two kinds..."

The focused group element has Name: "", Role: generic — a screen reader announces nothing. Focus is outside the modal: true dialog, so AT focus-trapping doesn't apply.

Root cause in MessageView/index.js:

const handleTransitionEnd = e => {
  if (showDetailsPageHeader && transitionTrigger.current === 'list') {
    requestAnimationFrame(() => {
      void navBtnRef.current?.focus(); // wrong target even when it runs
    });
    ...
  }
  // no focus management at all when showDetailsPageHeader is false
};

Additional edge case: withAnimation = getAnimationMode() !== 'none' — if animations are disabled (accessibility setting), transitionend never fires. No focus management runs regardless of showDetailsPageHeader.

Suggested fix

Focus the detail content container regardless of showDetailsPageHeader, and handle the no-animation case:

// 1. Add a ref to the details container
const detailsContainerRef = useRef(null);

// 2. Make it programmatically focusable:
// <FlexBox ref={detailsContainerRef} tabIndex={-1} className={classNames.details} ...>

// 3. In handleTransitionEnd, focus content instead of nav button:
if (transitionTrigger.current === 'list') {
  requestAnimationFrame(() => {
    void detailsContainerRef.current?.focus();
  });
}

// 4. Guard the no-animation case:
// In handleListItemClick, after setSelectedMessage, when !withAnimation:
if (!withAnimation) {
  requestAnimationFrame(() => { void detailsContainerRef.current?.focus(); });
}

Bug 4 (MessageView): Generic live region announcement on detail view open

Expected

When the detail panel opens, the screen reader should announce the message content so the user understands what they navigated to.

Actual

MessageView announces the generic string "Message Details" (an i18n key), not the actual message text:

setTimeout(() => {
  announce(i18nBundle.getText(MESSAGE_DETAILS), 'Polite'); // BUG: "Message Details", not the message text
}, 300);

Additional edge case: Same as Bug 3 — if animations are disabled, transitionend never fires and this setTimeout is never reached, so nothing is announced.

Suggested fix

Announce the actual message title:

setTimeout(() => {
  const title = prevSelectedMessage.current?.titleTextStr ?? i18nBundle.getText(MESSAGE_DETAILS);
  announce(title, 'Polite');
}, 300);

And guard against the animation-disabled case:

if (!withAnimation) {
  // Fire focus + announcement immediately when there is no transition
  requestAnimationFrame(() => { void detailsContainerRef.current?.focus(); });
  announce(titleStr, 'Polite');
}

Reproduction

import { MessageView, MessageItem, ResponsivePopover } from '@ui5/webcomponents-react';

<ResponsivePopover>
  <MessageView>
    {/* Active item (hasDetails === true because children are passed) */}
    <MessageItem titleText="The password must contain at least two character types" type="Negative">
      Full detail text here
    </MessageItem>
    {/* Inactive item (no children, no detail) */}
    <MessageItem titleText="Enter at least 6 in the Minimum Length field" type="Warning" />
  </MessageView>
</ResponsivePopover>

What happens:

  • Active item: NVDA announces "Is Active" (not the title text); no aria-expanded
  • Inactive item: NVDA announces nothing; no aria-expanded
  • Clicking an active item: focus lands on Navigate Back button; "Message Details" announced (not the message text); if animations off, nothing announced

WCAG impact

Issue Component WCAG SC Level
Wrong accessible name ("Is Active") MessageItem 4.1.2 Name, Role, Value A
No accessible name on inactive items MessageItem 4.1.2 Name, Role, Value A
Missing aria-expanded MessageItem 1.3.1 Info and Relationships A
Wrong focus target on detail open (nav button or outside dialog) MessageView 2.4.3 Focus Order A
Generic live region announcement MessageView 4.1.3 Status Messages AA
No announcement when animations disabled MessageView 4.1.3 Status Messages AA
主要言語
TypeScript
スター
543
フォーク
114
平均マージ
18時間 30分
マージ済み PR(30日)
76

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

UI5/webcomponents-react のほかの issue

UI5/webcomponents-react の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。