Skip to content

Commit baec15b

Browse files
authored
feat(ui5-avatar-group): Implement accessibility specification (#3154)
Implement accessibility specification. - Support for aria-haspopup has been added to ui5-avatar and ui5-avatar-group. - The interactive ui5-avatar now has role button. Fixes: #2745
1 parent eec4ba3 commit baec15b

File tree

9 files changed

+294
-11
lines changed

9 files changed

+294
-11
lines changed

packages/main/src/Avatar.hbs

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
@focusout={{_onfocusout}}
88
@focusin={{_onfocusin}}
99
@click={{_onclick}}
10+
role="{{_role}}"
11+
aria-haspopup="{{_ariaHasPopup}}"
1012
>
1113
{{#if image}}
1214
<span class="ui5-avatar-img" style="{{styles.img}}" role="img" aria-label="{{accessibleNameText}}"></span>

packages/main/src/Avatar.js

+27
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,17 @@ const metadata = {
196196
type: String,
197197
},
198198

199+
/**
200+
* Defines the aria-haspopup value of the <code>ui5-avatar</code> when <code>interactive</code> property is <code>true</code>.
201+
* <br><br>
202+
* @type String
203+
* @since 1.0.0-rc.15
204+
* @protected
205+
*/
206+
ariaHaspopup: {
207+
type: String,
208+
},
209+
199210
_tabIndex: {
200211
type: String,
201212
noAttribute: true,
@@ -306,6 +317,14 @@ class Avatar extends UI5Element {
306317
return this.getAttribute("background-color") || this._backgroundColor;
307318
}
308319

320+
get _role() {
321+
return this.interactive ? "button" : undefined;
322+
}
323+
324+
get _ariaHasPopup() {
325+
return this._getAriaHasPopup();
326+
}
327+
309328
get validInitials() {
310329
const validInitials = /^[a-zA-Z]{1,2}$/;
311330

@@ -369,6 +388,14 @@ class Avatar extends UI5Element {
369388
this.focused = true;
370389
}
371390
}
391+
392+
_getAriaHasPopup() {
393+
if (!this.interactive || this.ariaHaspopup === "") {
394+
return;
395+
}
396+
397+
return this.ariaHaspopup;
398+
}
372399
}
373400

374401
Avatar.define();

packages/main/src/AvatarGroup.hbs

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,22 @@
77
tabindex="{{_groupTabIndex}}"
88
@click="{{_onClick}}"
99
@ui5-click="{{_onUI5Click}}"
10+
aria-label="{{_ariaLabelText}}"
11+
role="{{_role}}"
12+
aria-haspopup="{{_containerAriaHasPopup}}"
1013
>
1114
<slot></slot>
1215

1316
{{#if _customOverflowButton}}
1417
<slot name="overflowButton"></slot>
1518
{{else}}
16-
<ui5-button ?hidden="{{_overflowBtnHidden}}" ?non-interactive={{_isGroup}} class="ui5-avatar-group-overflow-btn">
19+
<ui5-button
20+
._buttonAccInfo="{{_overflowButtonAccInfo}}"
21+
aria-label="{{_overflowButtonAriaLabelText}}"
22+
?hidden="{{_overflowBtnHidden}}"
23+
?non-interactive={{_isGroup}}
24+
class="ui5-avatar-group-overflow-btn"
25+
>
1726
{{_overflowButtonText}}
1827
</ui5-button>
1928
{{/if}}

packages/main/src/AvatarGroup.js

+103
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
22
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
33
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
44
import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
5+
import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
6+
57
import {
68
isEnter,
79
isSpace,
810
} from "@ui5/webcomponents-base/dist/Keys.js";
911

12+
import {
13+
AVATAR_GROUP_DISPLAYED_HIDDEN_LABEL,
14+
AVATAR_GROUP_SHOW_COMPLETE_LIST_LABEL,
15+
AVATAR_GROUP_ARIA_LABEL_INDIVIDUAL,
16+
AVATAR_GROUP_ARIA_LABEL_GROUP,
17+
AVATAR_GROUP_MOVE,
18+
} from "./generated/i18n/i18n-defaults.js";
19+
1020
// Template
1121
import AvatarGroupTemplate from "./generated/templates/AvatarGroupTemplate.lit.js";
1222
// Styles
@@ -89,6 +99,22 @@ const metadata = {
8999
defaultValue: AvatarSize.S,
90100
},
91101

102+
/**
103+
* Defines the aria-haspopup value of the <code>ui5-avatar-group</code> on:
104+
* <br><br>
105+
* <ul>
106+
* <li> the whole container when <code>type</code> property is <code>Group</code></li>
107+
* <li> the default "More" overflow button when <code>type</code> is <code>Individual</code></li>
108+
* </ul>
109+
* <br><br>
110+
* @type String
111+
* @since 1.0.0-rc.15
112+
* @protected
113+
*/
114+
ariaHaspopup: {
115+
type: String,
116+
},
117+
92118
/**
93119
* @private
94120
*/
@@ -196,6 +222,30 @@ const metadata = {
196222
* <li>You want to use it for other visual content than avatars.</li>
197223
* </ul>
198224
*
225+
* <h3>Keyboard Handling</h3>
226+
* The <code>ui5-avatar-group</code> provides advanced keyboard handling.
227+
* When focused, the user can use the following keyboard
228+
* shortcuts in order to perform a navigation:
229+
* <br>
230+
* - <code>type</code> Individual:
231+
* <br>
232+
* <ul>
233+
* <li>[TAB] - Move focus to the overflow button</li>
234+
* <li>[LEFT] - Navigate one avatar to the left</li>
235+
* <li>[RIGHT] - Navigate one avatar to the right</li>
236+
* <li>[HOME] - Navigate to the first avatar</li>
237+
* <li>[END] - Navigate to the last avatar</li>
238+
* <li>[SPACE],[ENTER],[RETURN] - Trigger <code>ui5-click</code> event</li>
239+
* </ul>
240+
* <br>
241+
* - <code>type</code> Group:
242+
* <br>
243+
* <ul>
244+
* <li>[TAB] - Move focus to the next interactive element after the <code>ui5-avatar-group</code></li>
245+
* <li>[SPACE],[ENTER],[RETURN] - Trigger <code>ui5-click</code> event</li>
246+
* </ul>
247+
* <br>
248+
*
199249
* @constructor
200250
* @author SAP SE
201251
* @alias sap.ui.webcomponents.main.AvatarGroup
@@ -217,6 +267,8 @@ class AvatarGroup extends UI5Element {
217267
this._colorIndex = 0;
218268
this._hiddenItems = 0;
219269
this._onResizeHandler = this._onResize.bind(this);
270+
271+
this.i18nBundle = getI18nBundle("@ui5/webcomponents");
220272
}
221273

222274
static get metadata() {
@@ -241,6 +293,10 @@ class AvatarGroup extends UI5Element {
241293
];
242294
}
243295

296+
static async onDefine() {
297+
await fetchI18nBundle("@ui5/webcomponents");
298+
}
299+
244300
/**
245301
* Returns an array containing the <code>ui5-avatar</code> instances that are currently not displayed due to lack of space.
246302
* @readonly
@@ -267,6 +323,45 @@ class AvatarGroup extends UI5Element {
267323
return this.overflowButton.length ? this.overflowButton[0] : undefined;
268324
}
269325

326+
get _ariaLabelText() {
327+
const hiddenItemsCount = this.hiddenItems.length;
328+
const typeLabelKey = this._isGroup ? AVATAR_GROUP_ARIA_LABEL_GROUP : AVATAR_GROUP_ARIA_LABEL_INDIVIDUAL;
329+
330+
// avatar type label
331+
let text = this.i18nBundle.getText(typeLabelKey);
332+
333+
// add displayed-hidden avatars label
334+
text += ` ${this.i18nBundle.getText(AVATAR_GROUP_DISPLAYED_HIDDEN_LABEL, [this._itemsCount - hiddenItemsCount], [hiddenItemsCount])}`;
335+
336+
if (this._isGroup) {
337+
// the container role is "button", add the message for complete list activation
338+
text += ` ${this.i18nBundle.getText(AVATAR_GROUP_SHOW_COMPLETE_LIST_LABEL)}`;
339+
} else {
340+
// the container role is "group", add the "how to navigate" message
341+
text += ` ${this.i18nBundle.getText(AVATAR_GROUP_MOVE)}`;
342+
}
343+
344+
return text;
345+
}
346+
347+
get _overflowButtonAriaLabelText() {
348+
return this._isGroup ? undefined : this.i18nBundle.getText(AVATAR_GROUP_SHOW_COMPLETE_LIST_LABEL);
349+
}
350+
351+
get _containerAriaHasPopup() {
352+
return this._isGroup ? this._getAriaHasPopup() : undefined;
353+
}
354+
355+
get _overflowButtonAccInfo() {
356+
return {
357+
ariaHaspopup: this._isGroup ? undefined : this._getAriaHasPopup(),
358+
};
359+
}
360+
361+
get _role() {
362+
return this._isGroup ? "button" : "group";
363+
}
364+
270365
get _hiddenStartIndex() {
271366
return this._itemsCount - this._hiddenItems;
272367
}
@@ -504,6 +599,14 @@ class AvatarGroup extends UI5Element {
504599
this.fireEvent("overflow");
505600
}
506601
}
602+
603+
_getAriaHasPopup() {
604+
if (this.ariaHaspopup === "") {
605+
return;
606+
}
607+
608+
return this.ariaHaspopup;
609+
}
507610
}
508611

509612
AvatarGroup.define();

packages/main/src/i18n/messagebundle.properties

+15
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ ARIA_ROLEDESCRIPTION_INTERACTIVE_CARD_HEADER=Interactive Card Header
1616
#XACT: ARIA announcement for the Avatar default tooltip
1717
AVATAR_TOOLTIP=Avatar
1818

19+
#XACT: ARIA announcement for the Avatar default tooltip
20+
AVATAR_GROUP_DISPLAYED_HIDDEN_LABEL={0} displayed, {1} hidden.
21+
22+
#XACT: ARIA announcement for the Avatar default tooltip
23+
AVATAR_GROUP_SHOW_COMPLETE_LIST_LABEL=Activate for complete list.
24+
25+
#XACT: ARIA announcement for the AvatarGroup type Individual aria-label attribute
26+
AVATAR_GROUP_ARIA_LABEL_INDIVIDUAL=Individual avatars.
27+
28+
#XACT: ARIA announcement for the AvatarGroup type Group aria-label attribute
29+
AVATAR_GROUP_ARIA_LABEL_GROUP=Conjoined avatars.
30+
31+
#XACT: ARIA announcement for the navigation in AvatarGroup
32+
AVATAR_GROUP_MOVE=Press ARROW keys to move.
33+
1934
#XACT: ARIA announcement for the badge
2035
BADGE_DESCRIPTION=Badge
2136

packages/main/test/pages/Avatar.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,12 @@ <h3>Avatar - test</h3>
114114

115115
<section>
116116
<h3>Avatar - interactive</h3>
117-
<ui5-avatar id="interactive-avatar" interactive initials="XS" size="XS"></ui5-avatar>
117+
<ui5-avatar id="interactive-avatar" aria-haspopup="menu" interactive initials="XS" size="XS"></ui5-avatar>
118118
<ui5-avatar id="non-interactive-avatar" initials="S" size="S"></ui5-avatar>
119119
<ui5-input id="click-event" value="0"></ui5-input>
120120

121121
<br>
122-
<ui5-avatar id="myInteractiveAvatar" interactive initials="L" size="L"></ui5-avatar>
122+
<ui5-avatar id="myInteractiveAvatar" aria-haspopup="menu" interactive initials="L" size="L"></ui5-avatar>
123123
<ui5-input id="click-event-2"></ui5-input>
124124
</section>
125125

packages/main/test/pages/AvatarGroup.html

+8-8
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,13 @@ <h5>Business card</h5>
107107
<p>this is footer</p>
108108
</div>
109109
</ui5-popover>
110-
<ui5-avatar-group type="Individual" avatar-size="XL" id="avatar-group-individual">
111-
<ui5-avatar interactive icon="home" initials="XL" id="avatar-1"></ui5-avatar>
112-
<ui5-avatar interactive initials="XL"></ui5-avatar>
113-
<ui5-avatar interactive initials="XL"></ui5-avatar>
114-
<ui5-avatar interactive initials="XL"></ui5-avatar>
115-
<ui5-avatar interactive initials="XL"></ui5-avatar>
116-
<ui5-avatar interactive initials="XL"></ui5-avatar>
110+
<ui5-avatar-group aria-haspopup="menu" type="Individual" avatar-size="XL" id="avatar-group-individual">
111+
<ui5-avatar interactive aria-haspopup="menu" icon="home" initials="XL" id="avatar-1"></ui5-avatar>
112+
<ui5-avatar interactive aria-haspopup="dialog" initials="XL"></ui5-avatar>
113+
<ui5-avatar interactive aria-haspopup="listbox" initials="XL"></ui5-avatar>
114+
<ui5-avatar interactive aria-haspopup="tree" initials="XL"></ui5-avatar>
115+
<ui5-avatar interactive aria-haspopup="grid" initials="XL"></ui5-avatar>
116+
<ui5-avatar interactive aria-haspopup="none" initials="XL"></ui5-avatar>
117117
<ui5-avatar interactive icon="home"></ui5-avatar>
118118
<ui5-avatar interactive initials="XL"></ui5-avatar>
119119
<ui5-avatar interactive initials="XL"></ui5-avatar>
@@ -122,7 +122,7 @@ <h5>Business card</h5>
122122
<ui5-avatar interactive initials="XL"></ui5-avatar>
123123
</ui5-avatar-group>
124124
<br>
125-
<ui5-avatar-group type="Group" avatar-size="M" id="avatar-group-group">
125+
<ui5-avatar-group aria-haspopup="menu" type="Group" avatar-size="M" id="avatar-group-group">
126126
<ui5-avatar initials="M" image="./img/woman_avatar_5.png"></ui5-avatar>
127127
<ui5-avatar initials="M"></ui5-avatar>
128128
<ui5-avatar icon="home"></ui5-avatar>

packages/main/test/specs/Avatar.spec.js

+27
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,30 @@ describe("Avatar", () => {
8787
assert.strictEqual(input.getAttribute("value"), "1", "Mouse click throws event");
8888
});
8989
});
90+
91+
describe("ARIA attributes", () => {
92+
before(() => {
93+
browser.url(`http://localhost:${PORT}/test-resources/pages/Avatar.html`);
94+
});
95+
96+
it ("role set correctly", () => {
97+
const avatar = $("#myInteractiveAvatar");;
98+
const avatarRoot = avatar.shadow$(".ui5-avatar-root");
99+
100+
assert.strictEqual(avatarRoot.getAttribute("role"), "button", "should have role button for interactive avatar");
101+
});
102+
103+
it ("aria-haspopup is correct for interactive avatar", () => {
104+
const avatar = $("#myInteractiveAvatar");;
105+
const ariaHasPopup = avatar.getProperty("_ariaHasPopup");
106+
107+
assert.strictEqual(ariaHasPopup, "menu", "should have aria-haspopup set");
108+
});
109+
110+
it ("aria-haspopup is correct for non-interactive avatar", () => {
111+
const avatar = $("#non-interactive-avatar");;
112+
const ariaHasPopup = avatar.getProperty("_ariaHasPopup");
113+
114+
assert.notExists(ariaHasPopup, "should not have aria-haspopup set");
115+
});
116+
});

0 commit comments

Comments
 (0)