Skip to content

Commit 87412b5

Browse files
authored
fix(ui5-timeline): apply correct accessibility semantics (#11774)
We've now set correct tree/list roles on the Timeline based on conditions: If a ui5-timeline has ui5-timeline-group-item – the role is set to tree so we oblige with the latest accessibility standards. When we have a ui5-timeline with only ui5-timeline-items – the role is set to list. Depending on the role, the correct treeitem/listitem roles are set to the items within. Using a Screen Reader now announces the counting information natively due to correct semantics (e.g Timeline item 2 of 4). Added region="Timeline" to the root of the Component according to spec.
1 parent 234c178 commit 87412b5

File tree

7 files changed

+71
-10
lines changed

7 files changed

+71
-10
lines changed

packages/fiori/cypress/specs/Timeline.cy.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe("Timeline general interaction", () => {
8686
cy.mount(<Sample />);
8787
cy.get("[ui5-timeline]")
8888
.shadow()
89-
.find("ul")
89+
.find(".ui5-timeline-list")
9090
.should("have.attr", "aria-label", "Timeline vertical");
9191
});
9292

packages/fiori/src/Timeline.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ interface ITimelineItem extends UI5Element, ITabbable {
5151
lastItem: boolean;
5252
isNextItemGroup?: boolean;
5353
firstItemInTimeline?: boolean;
54+
effectiveRole?: string;
5455
}
5556

5657
const SHORT_LINE_WIDTH = "ShortLineWidth";
@@ -306,6 +307,12 @@ class Timeline extends UI5Element {
306307

307308
for (let i = 0; i < this.items.length; i++) {
308309
this.items[i].layout = this.layout;
310+
if (this.hasGroupItems) {
311+
this.items[i].effectiveRole = "treeitem";
312+
} else {
313+
this.items[i].effectiveRole = "listitem";
314+
}
315+
309316
if (this.items[i + 1] && !!this.items[i + 1].icon) {
310317
this.items[i].forcedLineWidth = SHORT_LINE_WIDTH;
311318
} else if (this.items[i].icon && this.items[i + 1] && !this.items[i + 1].icon) {
@@ -428,6 +435,10 @@ class Timeline extends UI5Element {
428435
item.focus();
429436
}
430437

438+
get hasGroupItems() {
439+
return this.items.some(item => item.isGroupItem);
440+
}
441+
431442
get _navigableItems() {
432443
const navigatableItems: Array<ITimelineItem | ToggleButton> = [];
433444

packages/fiori/src/TimelineGroupItem.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ class TimelineGroupItem extends UI5Element implements ITimelineItem {
110110
return;
111111
}
112112

113+
this.items.forEach(item => {
114+
item.effectiveRole = "treeitem";
115+
});
116+
113117
this._setGroupItemProps();
114118
}
115119

packages/fiori/src/TimelineGroupItemTemplate.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import slimArrowup from "@ui5/webcomponents-icons/dist/slim-arrow-up.js";
88

99
export default function TimelineGroupItemTemplate(this: TimelineGroupItem) {
1010
return (
11-
<div class="ui5-tlgi-root">
11+
<div class="ui5-tlgi-root" role="treeitem">
1212
<div class="ui5-tlgi-btn-root">
1313
<div class="ui5-tlgi-icon-placeholder">
1414
<div class="ui5-tlgi-icon-dot"></div>
@@ -23,17 +23,18 @@ export default function TimelineGroupItemTemplate(this: TimelineGroupItem) {
2323
icon={getEffectiveGroupIcon.call(this, this.layout, this.collapsed)}
2424
pressed={this.collapsed}
2525
onClick={this.onGroupItemClick}
26+
accessibleName={`${this.groupName || "Group"}, ${this.collapsed ? "collapsed" : "expanded"}`}
2627
>
2728
{this.groupName}
2829
</ToggleButton>
2930
</div>
30-
<ul class="ui5-tl-group-item">
31+
<div class="ui5-tl-group-item" role="group" aria-label={`${this.groupName || "Group"} items`}>
3132
{this.items.map(item =>
32-
<li class="ui5-timeline-group-list-item">
33+
<div class="ui5-timeline-group-list-item">
3334
<slot name={item._individualSlot}></slot>
34-
</li>
35+
</div>
3536
)}
36-
</ul>
37+
</div>
3738
</div>
3839
);
3940
}

packages/fiori/src/TimelineItem.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323

2424
import TimelineItemCss from "./generated/themes/TimelineItem.css.js";
2525

26+
type TimelineItemRole = "listitem" | "treeitem";
27+
2628
/**
2729
* @class
2830
*
@@ -163,6 +165,12 @@ class TimelineItem extends UI5Element implements ITimelineItem {
163165
@property({ type: Boolean })
164166
hidden = false;
165167

168+
/**
169+
* @private
170+
*/
171+
@property({ noAttribute: true })
172+
effectiveRole: `${TimelineItemRole}` = "listitem";
173+
166174
/**
167175
* Defines the position of the item in a group.
168176
* @private
@@ -204,6 +212,28 @@ class TimelineItem extends UI5Element implements ITimelineItem {
204212
get isGroupItem() {
205213
return false;
206214
}
215+
216+
get _getAccessibleLabel() {
217+
const parts = [];
218+
219+
if (this.name) {
220+
parts.push(this.name);
221+
}
222+
223+
if (this.titleText) {
224+
parts.push(this.titleText);
225+
}
226+
227+
if (this.subtitleText) {
228+
parts.push(this.subtitleText);
229+
}
230+
231+
if (this.timelineItemStateText) {
232+
parts.push(this.timelineItemStateText);
233+
}
234+
235+
return parts.join(", ");
236+
}
207237
}
208238

209239
TimelineItem.define();

packages/fiori/src/TimelineItemTemplate.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import Icon from "@ui5/webcomponents/dist/Icon.js";
44
import TimelineLayout from "./types/TimelineLayout.js";
55

66
export default function TimelineItemTemplate(this: TimelineItem) {
7+
// Create accessible label with status information
8+
const accessibleLabel = this._getAccessibleLabel;
9+
710
return (
811
<div class="ui5-tli-root">
912
<div
@@ -27,7 +30,9 @@ export default function TimelineItemTemplate(this: TimelineItem) {
2730
<div
2831
data-sap-focus-ref
2932
class="ui5-tli-bubble"
33+
role={this.effectiveRole}
3034
tabindex={parseInt(this.forcedTabIndex)}
35+
aria-label={accessibleLabel}
3136
aria-description={this.timelineItemStateText}
3237
>
3338
<div class="ui5-tli-title">

packages/fiori/src/TimelineTemplate.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@ import Button from "@ui5/webcomponents/dist/Button.js";
22
import type Timeline from "./Timeline.js";
33
import BusyIndicator from "@ui5/webcomponents/dist/BusyIndicator.js";
44

5+
type TimelineListRole = "list" | "tree";
6+
57
export default function TimelineTemplate(this: Timeline) {
8+
const listRole: `${TimelineListRole}` = this.hasGroupItems ? "tree" : "list";
9+
610
return (
711
<div class="ui5-timeline-root"
12+
role="region"
13+
aria-label="Timeline"
814
onFocusIn={this._onfocusin}
915
onKeyDown={this._onkeydown}
1016
>
@@ -16,15 +22,19 @@ export default function TimelineTemplate(this: Timeline) {
1622
>
1723
<div class="ui5-timeline-scroll-container">
1824

19-
<ul class="ui5-timeline-list" aria-live="polite" aria-label={this.ariaLabel}>
25+
<div class="ui5-timeline-list"
26+
role={listRole}
27+
aria-live="polite"
28+
aria-label={this.ariaLabel}
29+
>
2030
{this.items.map(item =>
21-
<li class="ui5-timeline-list-item">
31+
<div class="ui5-timeline-list-item">
2232
<slot name={item._individualSlot}></slot>
23-
</li>
33+
</div>
2434
)}
2535
{ this.growsWithButton && moreRow.call(this) }
2636
{ this.growsOnScroll && endRow.call(this) }
27-
</ul>
37+
</div>
2838
</div>
2939
</BusyIndicator>
3040
</div>

0 commit comments

Comments
 (0)