Skip to content

Commit

Permalink
TagBox: Add role, aria-roledescription, aria-labelledby on root and a…
Browse files Browse the repository at this point in the history
…dd aria-activedescendant on input
  • Loading branch information
marker-dao committed May 5, 2023
1 parent 815fc66 commit f7494bd
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 16 deletions.
71 changes: 65 additions & 6 deletions js/ui/tag_box.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { addNamespace, isCommandKeyPressed, normalizeKeyName } from '../events/u
import { name as clickEvent } from '../events/click';
import caret from './text_box/utils.caret';
import { normalizeLoadResult } from '../data/data_source/utils';
import Guid from '../core/guid';

import SelectBox from './select_box';
import { BindableTemplate } from '../core/templates/bindable_template';
Expand All @@ -42,7 +43,6 @@ const TAGBOX_TAG_CONTENT_CLASS = 'dx-tag-content';
const TAGBOX_DEFAULT_FIELD_TEMPLATE_CLASS = 'dx-tagbox-default-template';
const TAGBOX_CUSTOM_FIELD_TEMPLATE_CLASS = 'dx-tagbox-custom-template';
const TEXTEDITOR_INPUT_CONTAINER_CLASS = 'dx-texteditor-input-container';
const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input';

const TAGBOX_MOUSE_WHEEL_DELTA_MULTIPLIER = -0.3;

Expand Down Expand Up @@ -181,20 +181,29 @@ const TagBox = SelectBox.inherit({
return position.start === 0 && position.end === 0;
},

_updateInputAriaActiveDescendant(id) {
this.setAria('activedescendant', id, this._input());
},

_moveTagFocus: function(direction, clearOnBoundary) {
if(!this._$focusedTag) {
const tagElements = this._tagElements();

this._$focusedTag = direction === 'next' ? tagElements.first() : tagElements.last();

this._toggleFocusClass(true, this._$focusedTag);
this._updateInputAriaActiveDescendant(this._$focusedTag.attr('id'));
return;
}

const $nextFocusedTag = this._$focusedTag[direction](`.${TAGBOX_TAG_CLASS}`);

if($nextFocusedTag.length > 0) {
this._replaceFocusedTag($nextFocusedTag);
this._updateInputAriaActiveDescendant($nextFocusedTag.attr('id'));
} else if(clearOnBoundary || (direction === 'next' && this._isEditable())) {
this._clearTagFocus();
this._updateInputAriaActiveDescendant();
}
},

Expand All @@ -210,6 +219,7 @@ const TagBox = SelectBox.inherit({
}

this._toggleFocusClass(false, this._$focusedTag);
this._updateInputAriaActiveDescendant();
delete this._$focusedTag;
},

Expand All @@ -226,9 +236,7 @@ const TagBox = SelectBox.inherit({
},

_setLabelContainerAria: function() {
const $input = this.$element().find(`.${TEXTEDITOR_INPUT_CLASS}`);

this.setAria('labelledby', this._label.getId(), $input);
this.setAria('labelledby', this._label.getId(), this._input());
},

_scrollContainer: function(direction) {
Expand Down Expand Up @@ -492,11 +500,51 @@ const TagBox = SelectBox.inherit({
.toggleClass(TAGBOX_ONLY_SELECT_CLASS, !(this.option('searchEnabled') || this.option('acceptCustomValue')))
.toggleClass(TAGBOX_SINGLE_LINE_CLASS, isSingleLineMode);

const elementAria = {
'role': 'group',
'roledescription': 'tagbox',
};

this.setAria(elementAria, this.$element());

this._initTagTemplate();

this.callBase();
},

_getNewLabelId(actualId, newId, shouldRemove) {
if(!actualId) {
return newId;
}

if(shouldRemove) {
if(actualId === newId) {
return undefined;
}

return actualId
.split(' ')
.filter(id => id !== newId)
.join(' ');
}

return `${actualId} ${newId}`;
},

_updateElementAria(id, shouldRemove) {
const shouldClearLabel = !id;

if(shouldClearLabel) {
this.setAria('labelledby', undefined, this.$element());
return;
}

const labelId = this.$element().attr('aria-labelledby');
const newLabelId = this._getNewLabelId(labelId, id, shouldRemove);

this.setAria('labelledby', newLabelId, this.$element());
},

_render: function() {
this.callBase();

Expand Down Expand Up @@ -1080,6 +1128,8 @@ const TagBox = SelectBox.inherit({
}
});
}

this._updateElementAria();
},

_renderEmptyState: function() {
Expand Down Expand Up @@ -1125,15 +1175,20 @@ const TagBox = SelectBox.inherit({
}

$tag.removeClass(TAGBOX_CUSTOM_TAG_CLASS);
this._updateElementAria($tag.attr('id'));
} else {
$tag = this._createTag(value, $input);
const tagId = `dx-${new Guid()}`;

$tag = this._createTag(value, $input, tagId);

if(isDefined(item)) {
this._applyTagTemplate(itemModel, $tag);
} else {
$tag.addClass(TAGBOX_CUSTOM_TAG_CLASS);
this._applyTagTemplate(value, $tag);
}

this._updateElementAria(tagId);
}
},

Expand Down Expand Up @@ -1162,8 +1217,9 @@ const TagBox = SelectBox.inherit({
return result;
},

_createTag: function(value, $input) {
_createTag: function(value, $input, tagId) {
return $('<div>')
.attr('id', tagId)
.addClass(TAGBOX_TAG_CLASS)
.data(TAGBOX_TAG_DATA_KEY, value)
.insertBefore($input);
Expand Down Expand Up @@ -1199,7 +1255,10 @@ const TagBox = SelectBox.inherit({
}

const itemValue = $tag.data(TAGBOX_TAG_DATA_KEY);
const itemId = $tag.attr('id');

this._removeTagWithUpdate(itemValue);
this._updateElementAria(itemId, true);
this._refreshTagElements();
},

Expand Down
7 changes: 0 additions & 7 deletions scss/widgets/base/_tagBox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,6 @@
min-width: 30px;
text-align: center;
cursor: pointer;

&::before {
content: ".";
color: transparent;
display: inline-block;
width: 0;
}
}

.dx-tag-remove-button {
Expand Down
98 changes: 95 additions & 3 deletions testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -7649,16 +7649,108 @@ QUnit.module('accessibility', () => {
const $input = $tagBox.find(`.${TEXTEDITOR_INPUT_CLASS}`);
const $label = $tagBox.find(`.${TEXTEDITOR_LABEL_CLASS}`);

assert.equal($input.attr('aria-labelledby'), $label.attr('id'), 'aria-labelledby was set correctly');
assert.strictEqual($input.attr('aria-labelledby'), $label.attr('id'), 'aria-labelledby was set correctly');

tagBox.option('label', null);
assert.equal($input.attr('aria-labelledby'), undefined, 'aria-labelledby was not set');
assert.strictEqual($input.attr('aria-labelledby'), undefined, 'aria-labelledby was not set');
});

QUnit.test('select should have a correct aria-label', function(assert) {
const $tagBox = $('#tagBox').dxTagBox();
const $select = $tagBox.find(TAGBOX_SELECT_SELECTOR);

assert.equal($select.attr('aria-label'), 'Selected items', 'aria-label is correct');
assert.strictEqual($select.attr('aria-label'), 'Selected items', 'aria-label is correct');
});

QUnit.test('TagBox element should have correct aria attributes', function(assert) {
const $tagBox = $('#tagBox').dxTagBox();

assert.strictEqual($tagBox.attr('role'), 'group', 'role is set correctly');
assert.strictEqual($tagBox.attr('aria-roledescription'), 'tagbox', 'aria-roledescription is set correctly');
});

QUnit.test('TagBox element should have an aria-labelledby attribute with correct ids', function(assert) {
const items = [1, 2, 3];
const $tagBox = $('#tagBox').dxTagBox({ items });
const tagBox = $tagBox.dxTagBox('instance');

assert.strictEqual($tagBox.attr('aria-labelledby'), undefined, 'aria-labelledby is undefined');

tagBox.option('value', [items[0]]);
let tagId = $tagBox.find(`.${TAGBOX_TAG_CLASS}`).attr('id');

assert.strictEqual($tagBox.attr('aria-labelledby'), tagId, 'aria-labelledby is set correctly');

tagBox.option('value', []);
assert.strictEqual($tagBox.attr('aria-labelledby'), undefined, 'aria-labelledby is set correctly');

tagBox.option('value', [items[0], items[1]]);

const tagIds = $tagBox.find(`.${TAGBOX_TAG_CLASS}`).toArray().map(($tag) => {
return $($tag).attr('id');
}).join(' ');

assert.strictEqual($tagBox.attr('aria-labelledby'), tagIds, 'aria-labelledby is set correctly');

tagBox.option('value', [items[1]]);
tagId = $tagBox.find(`.${TAGBOX_TAG_CLASS}`).attr('id');

assert.strictEqual($tagBox.attr('aria-labelledby'), tagId, 'aria-labelledby is set correctly');
});

QUnit.test('TagBox element should have aria-labelledby with correct ids if tag was deleted by keyboard', function(assert) {
const items = [1, 2];
const $tagBox = $('#tagBox').dxTagBox({
items,
value: [items[0], items[1]],
});

const $input = $tagBox.find(`.${TEXTEDITOR_INPUT_CLASS}`);
const keyboard = keyboardMock($input);

keyboard
.press('right')
.press('backspace');

const tagId = $tagBox.find(`.${TAGBOX_TAG_CLASS}`).attr('id');

assert.strictEqual($tagBox.attr('aria-labelledby'), tagId, 'aria-labelledby is set correctly');
});

QUnit.test('input should have aria-activedescendant with correct id if tag is focused', function(assert) {
const items = [1, 2];
const $tagBox = $('#tagBox').dxTagBox({
items,
value: items,
});
const tagBox = $tagBox.dxTagBox('instance');

const $input = $tagBox.find(`.${TEXTEDITOR_INPUT_CLASS}`);
const keyboard = keyboardMock($input);

const $tags = $tagBox.find(`.${TAGBOX_TAG_CLASS}`);

const firstTagId = $tags.eq(0).attr('id');
const secondTagId = $tags.eq(1).attr('id');

keyboard.press('right');
assert.strictEqual($input.attr('aria-activedescendant'), firstTagId, 'aria-activedescendant is set correctly');

keyboard.press('right');
assert.strictEqual($input.attr('aria-activedescendant'), secondTagId, 'aria-activedescendant is set correctly');

keyboard.press('backspace');
assert.strictEqual($input.attr('aria-activedescendant'), firstTagId, 'aria-activedescendant is set correctly');

keyboard.press('backspace');
assert.strictEqual($input.attr('aria-activedescendant'), undefined, 'aria-activedescendant is set correctly');

tagBox.option('value', [items[0], items[1]]);

keyboard
.press('right')
.press('backspace');

assert.strictEqual($input.attr('aria-activedescendant'), undefined, 'aria-activedescendant is set correctly');
});
});

0 comments on commit f7494bd

Please sign in to comment.