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

未关闭
#9,112 1 条评论 0 个 reaction 已指派 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 分钟
30 天内合并 PR
76

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

UI5/webcomponents-react 的其他 Issue

查看 UI5/webcomponents-react 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。