Skip to content

Commit 2e7b968

Browse files
authored
feat(ui5-multi-input): Implement accessibility specifications (#2761)
1 parent e903164 commit 2e7b968

10 files changed

+205
-27
lines changed

packages/main/src/MultiComboBox.hbs

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="ui5-multi-combobox-root"
22
>
3-
<span id="{{_id}}-hiddenText-nMore" class="ui5-hidden-text">{{nMoreCountText}}</span>
3+
<span id="{{_id}}-hiddenText-nMore" class="ui5-hidden-text">{{_tokensCountText}}</span>
44

55
{{#if hasValueState}}
66
<span id="{{_id}}-valueStateDesc" class="ui5-hidden-text">{{valueStateText}}</span>
@@ -50,8 +50,7 @@
5050
aria-haspopup="listbox"
5151
aria-expanded="{{open}}"
5252
aria-autocomplete="both"
53-
aria-labelledby="{{_id}}-hiddenText-nMore"
54-
aria-describedby="{{valueStateTextId}}"
53+
aria-describedby="{{ariaDescribedByText}}"
5554
aria-required="{{required}}"
5655
/>
5756

packages/main/src/MultiComboBox.js

+15-17
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ import {
3131
VALUE_STATE_SUCCESS,
3232
VALUE_STATE_ERROR,
3333
VALUE_STATE_WARNING,
34-
TOKENIZER_ARIA_CONTAIN_TOKEN,
35-
TOKENIZER_ARIA_CONTAIN_ONE_TOKEN,
36-
TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS,
3734
INPUT_SUGGESTIONS_TITLE,
3835
SELECT_OPTIONS,
3936
MULTICOMBOBOX_DIALOG_OK_BUTTON,
@@ -752,20 +749,6 @@ class MultiComboBox extends UI5Element {
752749
return this.shadowRoot.querySelector("[ui5-tokenizer]");
753750
}
754751

755-
get nMoreCountText() {
756-
const iTokenCount = this._getSelectedItems().length;
757-
758-
if (iTokenCount === 0) {
759-
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_TOKEN);
760-
}
761-
762-
if (iTokenCount === 1) {
763-
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_ONE_TOKEN);
764-
}
765-
766-
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS, iTokenCount);
767-
}
768-
769752
inputFocusIn() {
770753
if (!isPhone()) {
771754
this.focused = true;
@@ -814,6 +797,21 @@ class MultiComboBox extends UI5Element {
814797
return this.getSlottedNodes("valueStateMessage").map(el => el.cloneNode(true));
815798
}
816799

800+
get _tokensCountText() {
801+
if (!this._tokenizer) {
802+
return;
803+
}
804+
return this._tokenizer._tokensCountText();
805+
}
806+
807+
get _tokensCountTextId() {
808+
return `${this._id}-hiddenText-nMore`;
809+
}
810+
811+
get ariaDescribedByText() {
812+
return this.valueStateTextId ? `${this._tokensCountTextId} ${this.valueStateTextId}` : `${this._tokensCountTextId}`;
813+
}
814+
817815
get shouldDisplayDefaultValueStateMessage() {
818816
return !this.valueStateMessage.length && this.hasValueStateMessage;
819817
}

packages/main/src/MultiInput.hbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{{>include "./Input.hbs"}}
2-
2+
<span id="{{_id}}-hiddenText-nMore" class="ui5-hidden-text">{{_tokensCountText}}</span>
33
{{#*inline "preContent"}}
44
<ui5-tokenizer
55
class="ui5-multi-input-tokenizer"

packages/main/src/MultiInput.js

+27
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isLeft,
66
isRight,
77
} from "@ui5/webcomponents-base/dist/Keys.js";
8+
import { MULTIINPUT_ROLEDESCRIPTION_TEXT } from "./generated/i18n/i18n-defaults.js";
89
import Input from "./Input.js";
910
import MultiInputTemplate from "./generated/templates/MultiInputTemplate.lit.js";
1011
import styles from "./generated/themes/MultiInput.css.js";
@@ -266,6 +267,32 @@ class MultiInput extends Input {
266267
return this.shadowRoot.querySelector("[ui5-tokenizer]");
267268
}
268269

270+
get _tokensCountText() {
271+
if (!this.tokenizer) {
272+
return;
273+
}
274+
return this.tokenizer._tokensCountText();
275+
}
276+
277+
get _tokensCountTextId() {
278+
return `${this._id}-hiddenText-nMore`;
279+
}
280+
281+
get accInfo() {
282+
const ariaDescribedBy = `${this._tokensCountTextId} ${this.suggestionsTextId} ${this.valueStateTextId} ${this.suggestionsCount}`.trim();
283+
return {
284+
"input": {
285+
...super.accInfo.input,
286+
"ariaRoledescription": this.ariaRoleDescription,
287+
"ariaDescribedBy": ariaDescribedBy,
288+
},
289+
};
290+
}
291+
292+
get ariaRoleDescription() {
293+
return this.i18nBundle.getText(MULTIINPUT_ROLEDESCRIPTION_TEXT);
294+
}
295+
269296
static get dependencies() {
270297
return [
271298
...Input.dependencies,

packages/main/src/Tokenizer.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ import List from "./List.js";
1313
import StandardListItem from "./StandardListItem.js";
1414
import TokenizerTemplate from "./generated/templates/TokenizerTemplate.lit.js";
1515
import TokenizerPopoverTemplate from "./generated/templates/TokenizerPopoverTemplate.lit.js";
16-
import { MULTIINPUT_SHOW_MORE_TOKENS, TOKENIZER_ARIA_LABEL, TOKENIZER_POPOVER_REMOVE } from "./generated/i18n/i18n-defaults.js";
16+
import {
17+
MULTIINPUT_SHOW_MORE_TOKENS,
18+
TOKENIZER_ARIA_LABEL,
19+
TOKENIZER_POPOVER_REMOVE,
20+
TOKENIZER_ARIA_CONTAIN_TOKEN,
21+
TOKENIZER_ARIA_CONTAIN_ONE_TOKEN,
22+
TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS,
23+
} from "./generated/i18n/i18n-defaults.js";
1724

1825
// Styles
1926
import styles from "./generated/themes/Tokenizer.css.js";
@@ -353,6 +360,20 @@ class Tokenizer extends UI5Element {
353360
};
354361
}
355362

363+
_tokensCountText() {
364+
const iTokenCount = this._getTokens().length;
365+
366+
if (iTokenCount === 0) {
367+
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_TOKEN);
368+
}
369+
370+
if (iTokenCount === 1) {
371+
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_ONE_TOKEN);
372+
}
373+
374+
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS, iTokenCount);
375+
}
376+
356377
static get dependencies() {
357378
return [
358379
ResponsivePopover,

packages/main/src/i18n/messagebundle.properties

+4-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ MESSAGE_STRIP_CLOSE_BUTTON=Message Strip Close
9797
#XFLD: MultiComboBox dialog button
9898
MULTICOMBOBOX_DIALOG_OK_BUTTON=OK
9999

100+
#XBUT: MultiInput aria-roledescription text
101+
MULTIINPUT_ROLEDESCRIPTION_TEXT=Multi Value Input
102+
100103
#XFLD: Token number indicator which is used to show more tokens in Tokenizers inside MultiInput and MultiComboBox
101104
MULTIINPUT_SHOW_MORE_TOKENS={0} More
102105

@@ -176,7 +179,7 @@ DATETIME_PICKER_TIME_BUTTON=Time
176179
TOKEN_ARIA_DELETABLE=Deletable
177180

178181
#XACT: ARIA announcement for tokens
179-
TOKENIZER_ARIA_CONTAIN_TOKEN=May contain tokens
182+
TOKENIZER_ARIA_CONTAIN_TOKEN=No Tokens
180183

181184
#XACT: ARIA announcement for tokenizer with 1 token
182185
TOKENIZER_ARIA_CONTAIN_ONE_TOKEN=Contains 1 token

packages/main/test/pages/MultiComboBox.html

+4-4
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@
171171
<span>value state error </span>
172172

173173
<br>
174-
<ui5-multi-combobox allow-custom-values placeholder="Some initial text"
174+
<ui5-multi-combobox id="mcb-error" allow-custom-values placeholder="Some initial text"
175175
value-state="Error">
176176
<ui5-mcb-item selected text="Cosy"></ui5-mcb-item>
177177
<ui5-mcb-item text="Compact"></ui5-mcb-item>
@@ -270,9 +270,9 @@
270270
<section class="ui5-content-density-compact">
271271
<h3>MultiComboBox in Compact</h3>
272272
<div>
273-
<ui5-multi-combobox placeholder="Some initial text">
274-
<ui5-mcb-item text="Cosy"></ui5-mcb-item>
275-
<ui5-mcb-item text="Compact"></ui5-mcb-item>
273+
<ui5-multi-combobox id="mcb-compact" placeholder="Some initial text">
274+
<ui5-mcb-item text="Cosy" selected></ui5-mcb-item>
275+
<ui5-mcb-item text="Compact" selected></ui5-mcb-item>
276276
<ui5-mcb-item text="Condensed"></ui5-mcb-item>
277277
<ui5-mcb-item text="Longest word in the world"></ui5-mcb-item>
278278
</ui5-multi-combobox>

packages/main/test/pages/MultiInput.html

+14
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,16 @@ <h1 class="sample-container-title">Multi Input - Error</h1>
204204
<div class="sample-container">
205205
<h1>Tokens</h1>
206206

207+
<h1 class="sample-container-title">Multi Input without tokens</h1>
208+
<ui5-multi-input id="no-tokens"></ui5-multi-input>
209+
<br>
210+
<br>
211+
212+
<ui5-button id="add-tokens">Add more tokens</ui5-button>
213+
214+
<br>
215+
<br>
216+
207217
<h1 class="sample-container-title">Multi Input with 1 token</h1>
208218
<ui5-multi-input id="single-token">
209219
<ui5-token slot="tokens" text="Amet"></ui5-token>
@@ -297,6 +307,10 @@ <h1>Test value-help-trigger with F4 and Alt + ArrowUp/Down</h1>
297307
document.getElementById(id).appendChild(token);
298308
}
299309

310+
document.getElementById("add-tokens").addEventListener("click", function(event) {
311+
addTokenToMI(createTokenFromText("test"), "no-tokens");
312+
});
313+
300314
document.getElementById("add-to-single").addEventListener("click", function(event) {
301315
addTokenToMI(createTokenFromText("test"), "single-token");
302316
});

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

+56
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,60 @@ describe("MultiComboBox general interaction", () => {
237237
icon.click();
238238
});
239239
});
240+
241+
describe("ARIA attributes", () => {
242+
browser.url("http://localhost:8080/test-resources/pages/MultiComboBox.html");
243+
244+
it ("aria-describedby value according to the tokens count and the value state", () => {
245+
const mcb = $("#mcb-error");
246+
const innerInput = mcb.shadow$("input");
247+
const invisibleText = mcb.shadow$(".ui5-hidden-text");
248+
let tokens = mcb.shadow$$(".ui5-multi-combobox-token");
249+
const tokensCountITextId = `${mcb.getProperty("_id")}-hiddenText-nMore`;
250+
const valuestateITextId = `${mcb.getProperty("_id")}-valueStateDesc`;
251+
const ariaDescribedBy = `${tokensCountITextId} ${valuestateITextId}`;
252+
253+
assert.strictEqual(tokens.length, 3, "should have three tokens");
254+
assert.strictEqual(innerInput.getAttribute("aria-describedby"), ariaDescribedBy, "aria-describedby has a reference for the value state and the tokens count");
255+
});
256+
257+
it ("aria-describedby value according to the tokens count", () => {
258+
const mcb = $("#mcb-compact");
259+
const innerInput = mcb.shadow$("input");
260+
const invisibleText = mcb.shadow$(".ui5-hidden-text");
261+
const inivisbleTextId = invisibleText.getProperty("id");
262+
let tokens = mcb.shadow$$(".ui5-multi-combobox-token");
263+
let resourceBundleText = null;
264+
265+
assert.strictEqual(tokens.length, 2, "should have two tokens");
266+
assert.strictEqual(innerInput.getAttribute("aria-describedby"), inivisbleTextId, "aria-describedby reference is correct");
267+
assert.strictEqual(invisibleText.getText(), "Contains 2 tokens", "aria-describedby text is correct");
268+
269+
mcb.scrollIntoView();
270+
innerInput.click();
271+
innerInput.keys("Backspace");
272+
innerInput.keys("Backspace");
273+
274+
tokens = mcb.shadow$$(".ui5-multi-combobox-token");
275+
276+
resourceBundleText = browser.execute(() => {
277+
const mcb = document.getElementById("mcb-compact");
278+
return mcb.i18nBundle.getText("TOKENIZER_ARIA_CONTAIN_ONE_TOKEN");
279+
});
280+
281+
assert.strictEqual(tokens.length, 1, "should have one token");
282+
assert.strictEqual(invisibleText.getText(), resourceBundleText, "aria-describedby text is correct");
283+
284+
innerInput.keys("Backspace");
285+
286+
tokens = mcb.shadow$$(".ui5-multi-combobox-token");
287+
resourceBundleText = browser.execute(() => {
288+
const mcb = document.getElementById("mcb-compact");
289+
return mcb.i18nBundle.getText("TOKENIZER_ARIA_CONTAIN_TOKEN");
290+
});
291+
292+
assert.strictEqual(tokens.length, 0, "should not have tokens");
293+
assert.strictEqual(invisibleText.getText(), resourceBundleText, "aria-describedby text is correct");
294+
});
295+
});
240296
});

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

+60
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,63 @@ describe("MultiInput general interaction", () => {
124124
assert.strictEqual(mi.$$("ui5-token").length, 1, "a token is added after selection");
125125
});
126126
});
127+
128+
describe("ARIA attributes", () => {
129+
it ("aria-describedby value according to the tokens count", () => {
130+
const mi = $("#no-tokens");
131+
const innerInput = mi.shadow$("input");
132+
const btn = $("#add-tokens");
133+
const invisibleText = mi.shadow$(".ui5-hidden-text");
134+
const inivisbleTextId = invisibleText.getProperty("id");
135+
let resourceBundleText = null;
136+
137+
resourceBundleText = browser.execute(() => {
138+
const mi = document.getElementById("no-tokens");
139+
return mi.i18nBundle.getText("TOKENIZER_ARIA_CONTAIN_TOKEN");
140+
});
141+
142+
assert.strictEqual(mi.$$("ui5-token").length, 0, "should not have tokens");
143+
assert.strictEqual(innerInput.getAttribute("aria-describedby"), inivisbleTextId, "aria-describedby reference is correct");
144+
assert.strictEqual(invisibleText.getText(), resourceBundleText, "aria-describedby text is correct");
145+
146+
$("#add-tokens").scrollIntoView();
147+
btn.click();
148+
149+
resourceBundleText = browser.execute(() => {
150+
const mi = document.getElementById("no-tokens");
151+
return mi.i18nBundle.getText("TOKENIZER_ARIA_CONTAIN_ONE_TOKEN");
152+
});
153+
154+
assert.strictEqual(mi.$$("ui5-token").length, 1, "should have one token");
155+
assert.strictEqual(invisibleText.getText(), resourceBundleText, "aria-describedby text is correct");
156+
157+
btn.click();
158+
assert.strictEqual(mi.$$("ui5-token").length, 2, "should have two tokens");
159+
assert.strictEqual(invisibleText.getText(), "Contains 2 tokens", "aria-describedby text is correct");
160+
});
161+
162+
it ("aria-describedby value according to the tokens and suggestions count", () => {
163+
const mi = $("#suggestion-token");
164+
const innerInput = mi.shadow$("input");
165+
const tokensCountITextId = `${mi.getProperty("_id")}-hiddenText-nMore`;
166+
const suggestionsITextId = `${mi.getProperty("_id")}-suggestionsText`;
167+
const suggestionsCountITextId = `${mi.getProperty("_id")}-suggestionsCount`;
168+
const ariaDescribedBy = `${tokensCountITextId} ${suggestionsITextId} ${suggestionsCountITextId}`;
169+
170+
$("#suggestion-token").scrollIntoView();
171+
innerInput.click();
172+
innerInput.keys("a");
173+
innerInput.keys("ArrowDown");
174+
innerInput.keys("Enter");
175+
176+
assert.strictEqual(innerInput.getAttribute("aria-describedby"), ariaDescribedBy, "aria-describedby attribute contains multiple references");
177+
});
178+
179+
it ("aria-roledescription is set properly", () => {
180+
const mi = $("#no-tokens");
181+
const innerInput = mi.shadow$("input");
182+
183+
assert.strictEqual(innerInput.getAttribute("aria-roledescription"), "Multi Value Input", "aria-roledescription value is correct");
184+
});
185+
});
186+

0 commit comments

Comments
 (0)