Skip to content

Commit

Permalink
feat(ui5-multi-input): Implement accessibility specifications (#2761)
Browse files Browse the repository at this point in the history
  • Loading branch information
niyap authored Feb 1, 2021
1 parent e903164 commit 2e7b968
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 27 deletions.
5 changes: 2 additions & 3 deletions packages/main/src/MultiComboBox.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="ui5-multi-combobox-root"
>
<span id="{{_id}}-hiddenText-nMore" class="ui5-hidden-text">{{nMoreCountText}}</span>
<span id="{{_id}}-hiddenText-nMore" class="ui5-hidden-text">{{_tokensCountText}}</span>

{{#if hasValueState}}
<span id="{{_id}}-valueStateDesc" class="ui5-hidden-text">{{valueStateText}}</span>
Expand Down Expand Up @@ -50,8 +50,7 @@
aria-haspopup="listbox"
aria-expanded="{{open}}"
aria-autocomplete="both"
aria-labelledby="{{_id}}-hiddenText-nMore"
aria-describedby="{{valueStateTextId}}"
aria-describedby="{{ariaDescribedByText}}"
aria-required="{{required}}"
/>

Expand Down
32 changes: 15 additions & 17 deletions packages/main/src/MultiComboBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ import {
VALUE_STATE_SUCCESS,
VALUE_STATE_ERROR,
VALUE_STATE_WARNING,
TOKENIZER_ARIA_CONTAIN_TOKEN,
TOKENIZER_ARIA_CONTAIN_ONE_TOKEN,
TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS,
INPUT_SUGGESTIONS_TITLE,
SELECT_OPTIONS,
MULTICOMBOBOX_DIALOG_OK_BUTTON,
Expand Down Expand Up @@ -752,20 +749,6 @@ class MultiComboBox extends UI5Element {
return this.shadowRoot.querySelector("[ui5-tokenizer]");
}

get nMoreCountText() {
const iTokenCount = this._getSelectedItems().length;

if (iTokenCount === 0) {
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_TOKEN);
}

if (iTokenCount === 1) {
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_ONE_TOKEN);
}

return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS, iTokenCount);
}

inputFocusIn() {
if (!isPhone()) {
this.focused = true;
Expand Down Expand Up @@ -814,6 +797,21 @@ class MultiComboBox extends UI5Element {
return this.getSlottedNodes("valueStateMessage").map(el => el.cloneNode(true));
}

get _tokensCountText() {
if (!this._tokenizer) {
return;
}
return this._tokenizer._tokensCountText();
}

get _tokensCountTextId() {
return `${this._id}-hiddenText-nMore`;
}

get ariaDescribedByText() {
return this.valueStateTextId ? `${this._tokensCountTextId} ${this.valueStateTextId}` : `${this._tokensCountTextId}`;
}

get shouldDisplayDefaultValueStateMessage() {
return !this.valueStateMessage.length && this.hasValueStateMessage;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/MultiInput.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{{>include "./Input.hbs"}}

<span id="{{_id}}-hiddenText-nMore" class="ui5-hidden-text">{{_tokensCountText}}</span>
{{#*inline "preContent"}}
<ui5-tokenizer
class="ui5-multi-input-tokenizer"
Expand Down
27 changes: 27 additions & 0 deletions packages/main/src/MultiInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isLeft,
isRight,
} from "@ui5/webcomponents-base/dist/Keys.js";
import { MULTIINPUT_ROLEDESCRIPTION_TEXT } from "./generated/i18n/i18n-defaults.js";
import Input from "./Input.js";
import MultiInputTemplate from "./generated/templates/MultiInputTemplate.lit.js";
import styles from "./generated/themes/MultiInput.css.js";
Expand Down Expand Up @@ -266,6 +267,32 @@ class MultiInput extends Input {
return this.shadowRoot.querySelector("[ui5-tokenizer]");
}

get _tokensCountText() {
if (!this.tokenizer) {
return;
}
return this.tokenizer._tokensCountText();
}

get _tokensCountTextId() {
return `${this._id}-hiddenText-nMore`;
}

get accInfo() {
const ariaDescribedBy = `${this._tokensCountTextId} ${this.suggestionsTextId} ${this.valueStateTextId} ${this.suggestionsCount}`.trim();
return {
"input": {
...super.accInfo.input,
"ariaRoledescription": this.ariaRoleDescription,
"ariaDescribedBy": ariaDescribedBy,
},
};
}

get ariaRoleDescription() {
return this.i18nBundle.getText(MULTIINPUT_ROLEDESCRIPTION_TEXT);
}

static get dependencies() {
return [
...Input.dependencies,
Expand Down
23 changes: 22 additions & 1 deletion packages/main/src/Tokenizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ import List from "./List.js";
import StandardListItem from "./StandardListItem.js";
import TokenizerTemplate from "./generated/templates/TokenizerTemplate.lit.js";
import TokenizerPopoverTemplate from "./generated/templates/TokenizerPopoverTemplate.lit.js";
import { MULTIINPUT_SHOW_MORE_TOKENS, TOKENIZER_ARIA_LABEL, TOKENIZER_POPOVER_REMOVE } from "./generated/i18n/i18n-defaults.js";
import {
MULTIINPUT_SHOW_MORE_TOKENS,
TOKENIZER_ARIA_LABEL,
TOKENIZER_POPOVER_REMOVE,
TOKENIZER_ARIA_CONTAIN_TOKEN,
TOKENIZER_ARIA_CONTAIN_ONE_TOKEN,
TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS,
} from "./generated/i18n/i18n-defaults.js";

// Styles
import styles from "./generated/themes/Tokenizer.css.js";
Expand Down Expand Up @@ -353,6 +360,20 @@ class Tokenizer extends UI5Element {
};
}

_tokensCountText() {
const iTokenCount = this._getTokens().length;

if (iTokenCount === 0) {
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_TOKEN);
}

if (iTokenCount === 1) {
return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_ONE_TOKEN);
}

return this.i18nBundle.getText(TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS, iTokenCount);
}

static get dependencies() {
return [
ResponsivePopover,
Expand Down
5 changes: 4 additions & 1 deletion packages/main/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ MESSAGE_STRIP_CLOSE_BUTTON=Message Strip Close
#XFLD: MultiComboBox dialog button
MULTICOMBOBOX_DIALOG_OK_BUTTON=OK

#XBUT: MultiInput aria-roledescription text
MULTIINPUT_ROLEDESCRIPTION_TEXT=Multi Value Input

#XFLD: Token number indicator which is used to show more tokens in Tokenizers inside MultiInput and MultiComboBox
MULTIINPUT_SHOW_MORE_TOKENS={0} More

Expand Down Expand Up @@ -176,7 +179,7 @@ DATETIME_PICKER_TIME_BUTTON=Time
TOKEN_ARIA_DELETABLE=Deletable

#XACT: ARIA announcement for tokens
TOKENIZER_ARIA_CONTAIN_TOKEN=May contain tokens
TOKENIZER_ARIA_CONTAIN_TOKEN=No Tokens

#XACT: ARIA announcement for tokenizer with 1 token
TOKENIZER_ARIA_CONTAIN_ONE_TOKEN=Contains 1 token
Expand Down
8 changes: 4 additions & 4 deletions packages/main/test/pages/MultiComboBox.html
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@
<span>value state error </span>

<br>
<ui5-multi-combobox allow-custom-values placeholder="Some initial text"
<ui5-multi-combobox id="mcb-error" allow-custom-values placeholder="Some initial text"
value-state="Error">
<ui5-mcb-item selected text="Cosy"></ui5-mcb-item>
<ui5-mcb-item text="Compact"></ui5-mcb-item>
Expand Down Expand Up @@ -270,9 +270,9 @@
<section class="ui5-content-density-compact">
<h3>MultiComboBox in Compact</h3>
<div>
<ui5-multi-combobox placeholder="Some initial text">
<ui5-mcb-item text="Cosy"></ui5-mcb-item>
<ui5-mcb-item text="Compact"></ui5-mcb-item>
<ui5-multi-combobox id="mcb-compact" placeholder="Some initial text">
<ui5-mcb-item text="Cosy" selected></ui5-mcb-item>
<ui5-mcb-item text="Compact" selected></ui5-mcb-item>
<ui5-mcb-item text="Condensed"></ui5-mcb-item>
<ui5-mcb-item text="Longest word in the world"></ui5-mcb-item>
</ui5-multi-combobox>
Expand Down
14 changes: 14 additions & 0 deletions packages/main/test/pages/MultiInput.html
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ <h1 class="sample-container-title">Multi Input - Error</h1>
<div class="sample-container">
<h1>Tokens</h1>

<h1 class="sample-container-title">Multi Input without tokens</h1>
<ui5-multi-input id="no-tokens"></ui5-multi-input>
<br>
<br>

<ui5-button id="add-tokens">Add more tokens</ui5-button>

<br>
<br>

<h1 class="sample-container-title">Multi Input with 1 token</h1>
<ui5-multi-input id="single-token">
<ui5-token slot="tokens" text="Amet"></ui5-token>
Expand Down Expand Up @@ -297,6 +307,10 @@ <h1>Test value-help-trigger with F4 and Alt + ArrowUp/Down</h1>
document.getElementById(id).appendChild(token);
}

document.getElementById("add-tokens").addEventListener("click", function(event) {
addTokenToMI(createTokenFromText("test"), "no-tokens");
});

document.getElementById("add-to-single").addEventListener("click", function(event) {
addTokenToMI(createTokenFromText("test"), "single-token");
});
Expand Down
56 changes: 56 additions & 0 deletions packages/main/test/specs/MultiComboBox.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,60 @@ describe("MultiComboBox general interaction", () => {
icon.click();
});
});

describe("ARIA attributes", () => {
browser.url("http://localhost:8080/test-resources/pages/MultiComboBox.html");

it ("aria-describedby value according to the tokens count and the value state", () => {
const mcb = $("#mcb-error");
const innerInput = mcb.shadow$("input");
const invisibleText = mcb.shadow$(".ui5-hidden-text");
let tokens = mcb.shadow$$(".ui5-multi-combobox-token");
const tokensCountITextId = `${mcb.getProperty("_id")}-hiddenText-nMore`;
const valuestateITextId = `${mcb.getProperty("_id")}-valueStateDesc`;
const ariaDescribedBy = `${tokensCountITextId} ${valuestateITextId}`;

assert.strictEqual(tokens.length, 3, "should have three tokens");
assert.strictEqual(innerInput.getAttribute("aria-describedby"), ariaDescribedBy, "aria-describedby has a reference for the value state and the tokens count");
});

it ("aria-describedby value according to the tokens count", () => {
const mcb = $("#mcb-compact");
const innerInput = mcb.shadow$("input");
const invisibleText = mcb.shadow$(".ui5-hidden-text");
const inivisbleTextId = invisibleText.getProperty("id");
let tokens = mcb.shadow$$(".ui5-multi-combobox-token");
let resourceBundleText = null;

assert.strictEqual(tokens.length, 2, "should have two tokens");
assert.strictEqual(innerInput.getAttribute("aria-describedby"), inivisbleTextId, "aria-describedby reference is correct");
assert.strictEqual(invisibleText.getText(), "Contains 2 tokens", "aria-describedby text is correct");

mcb.scrollIntoView();
innerInput.click();
innerInput.keys("Backspace");
innerInput.keys("Backspace");

tokens = mcb.shadow$$(".ui5-multi-combobox-token");

resourceBundleText = browser.execute(() => {
const mcb = document.getElementById("mcb-compact");
return mcb.i18nBundle.getText("TOKENIZER_ARIA_CONTAIN_ONE_TOKEN");
});

assert.strictEqual(tokens.length, 1, "should have one token");
assert.strictEqual(invisibleText.getText(), resourceBundleText, "aria-describedby text is correct");

innerInput.keys("Backspace");

tokens = mcb.shadow$$(".ui5-multi-combobox-token");
resourceBundleText = browser.execute(() => {
const mcb = document.getElementById("mcb-compact");
return mcb.i18nBundle.getText("TOKENIZER_ARIA_CONTAIN_TOKEN");
});

assert.strictEqual(tokens.length, 0, "should not have tokens");
assert.strictEqual(invisibleText.getText(), resourceBundleText, "aria-describedby text is correct");
});
});
});
60 changes: 60 additions & 0 deletions packages/main/test/specs/MultiInput.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,63 @@ describe("MultiInput general interaction", () => {
assert.strictEqual(mi.$$("ui5-token").length, 1, "a token is added after selection");
});
});

describe("ARIA attributes", () => {
it ("aria-describedby value according to the tokens count", () => {
const mi = $("#no-tokens");
const innerInput = mi.shadow$("input");
const btn = $("#add-tokens");
const invisibleText = mi.shadow$(".ui5-hidden-text");
const inivisbleTextId = invisibleText.getProperty("id");
let resourceBundleText = null;

resourceBundleText = browser.execute(() => {
const mi = document.getElementById("no-tokens");
return mi.i18nBundle.getText("TOKENIZER_ARIA_CONTAIN_TOKEN");
});

assert.strictEqual(mi.$$("ui5-token").length, 0, "should not have tokens");
assert.strictEqual(innerInput.getAttribute("aria-describedby"), inivisbleTextId, "aria-describedby reference is correct");
assert.strictEqual(invisibleText.getText(), resourceBundleText, "aria-describedby text is correct");

$("#add-tokens").scrollIntoView();
btn.click();

resourceBundleText = browser.execute(() => {
const mi = document.getElementById("no-tokens");
return mi.i18nBundle.getText("TOKENIZER_ARIA_CONTAIN_ONE_TOKEN");
});

assert.strictEqual(mi.$$("ui5-token").length, 1, "should have one token");
assert.strictEqual(invisibleText.getText(), resourceBundleText, "aria-describedby text is correct");

btn.click();
assert.strictEqual(mi.$$("ui5-token").length, 2, "should have two tokens");
assert.strictEqual(invisibleText.getText(), "Contains 2 tokens", "aria-describedby text is correct");
});

it ("aria-describedby value according to the tokens and suggestions count", () => {
const mi = $("#suggestion-token");
const innerInput = mi.shadow$("input");
const tokensCountITextId = `${mi.getProperty("_id")}-hiddenText-nMore`;
const suggestionsITextId = `${mi.getProperty("_id")}-suggestionsText`;
const suggestionsCountITextId = `${mi.getProperty("_id")}-suggestionsCount`;
const ariaDescribedBy = `${tokensCountITextId} ${suggestionsITextId} ${suggestionsCountITextId}`;

$("#suggestion-token").scrollIntoView();
innerInput.click();
innerInput.keys("a");
innerInput.keys("ArrowDown");
innerInput.keys("Enter");

assert.strictEqual(innerInput.getAttribute("aria-describedby"), ariaDescribedBy, "aria-describedby attribute contains multiple references");
});

it ("aria-roledescription is set properly", () => {
const mi = $("#no-tokens");
const innerInput = mi.shadow$("input");

assert.strictEqual(innerInput.getAttribute("aria-roledescription"), "Multi Value Input", "aria-roledescription value is correct");
});
});

0 comments on commit 2e7b968

Please sign in to comment.