MessageView + MessageItem: missing accessible names, no aria-expanded, and broken focus/announcement on detail view
メンテナーはふだん 1 日以内に返信
評価
- 難易度
- 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):
- Wrong or missing accessible name on all items — active items announce "Is Active"; inactive items announce nothing
aria-expandednot 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-expandedon any item in any state (confirmed via live DOM) - The
MessageItemcomponent has noaccessibilityAttributesprop in its public API (Storybook only lists:titleText,subtitleText,counter,type,groupName,children,accessibleName,onClick,tooltip,className,style) ListItemAccessibilityAttributesis typed asPick<AccessibilityAttributes, "hasPopup" | "ariaSetsize" | "ariaPosinset">—expandedis excluded, so even passingaccessibilityAttributesvia...restcannot setaria-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
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
UI5/webcomponents-react のほかの issue
-
author action
難易度 3/5 半日 初心者へのやさしさ 55/100
UI5/webcomponents-react#9128 · コメント 3 件 ·
メンテナーはふだん 1 日以内に返信
-
Modals: preventInitialFocus is set too late when using Modals.showDialog対応中かも @Lukas742 が 5 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 25/100
UI5/webcomponents-react#9116 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
ObjectPage: tabIndex=0 set on first ObjectPageSection even for single-section layout causing WCAG 2.1.1 keyboard accessibility violation対応中かも @Lukas742 が 11 日前に担当しました。 オープン
UI5/webcomponents-react#9080 · コメント 5 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 5/5 1週間以上 初心者へのやさしさ 20/100
UI5/webcomponents-react#8529 ·
メンテナーはふだん 1 日以内に返信
-
Design Pending SAP SF
難易度 3/5 1〜2日 初心者へのやさしさ 45/100
UI5/webcomponents-react#8158 · コメント 1 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
UI5/webcomponents-react の issue をすべて見る
似ている issue
-
needs investigation
難易度 2/5 1〜3時間 初心者へのやさしさ 60/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
area:visualizer tech-debt
難易度 2/5 1時間未満 初心者へのやさしさ 66/100
jmbarzee/temporal-architect#292 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
riverqueue/river#1576 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
colbymchenry/codegraph#3228 ·
メンテナーはふだん 1 日以内に返信