Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions apps/demos/Demos/RadioGroup/Overview/jQuery/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ $(() => {
$('#radio-group-with-template').dxRadioGroup({
items: priorities,
value: priorities[2],
itemTemplate(itemData, _, itemElement) {
itemElement
.parent().addClass(itemData.toLowerCase())
.text(itemData);
itemTemplate: (itemData, _, itemElement) => {
itemElement.text(itemData);
},
});
onValueChanged: (e) => {
const $element = $(e.element);
const priorityClass = e.previousValue.toLowerCase();
const newPriorityClass = e.value.toLowerCase();

$element.removeClass(priorityClass);
$element.addClass(newPriorityClass);
},
}).addClass(priorities[2].toLowerCase());

const radioGroup = $('#radio-group-with-selection').dxRadioGroup({
items: priorityEntities,
Expand Down
8 changes: 4 additions & 4 deletions apps/demos/Demos/RadioGroup/Overview/jQuery/styles.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
.low.dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
.low .dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
background: gray;
}

.normal.dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
.normal .dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
background: green;
}

.urgent.dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
.urgent .dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
background: orange;
}

.high.dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
.high .dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
background: red;
}

Expand Down
26 changes: 26 additions & 0 deletions e2e/testcafe-devextreme/tests/accessibility/radioGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,29 @@ const configuration: Configuration = {
};

testAccessibility(configuration);

const buttons = [
{
text: 'custom 1',
},
{
text: 'custom 2',
},
];

const interactiveItemsConfiguration: Configuration = {
component: 'dxRadioGroup',
a11yCheckConfig,
options: {
items: [buttons],
itemTemplate: [
(itemData, _, itemElement) => {
const $button = $('<button>').text(itemData.text);

itemElement.append($button);
},
],
},
};

testAccessibility(interactiveItemsConfiguration);
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ class CollectionWidget<
> extends Widget<TProperties> {
private _focusedItemId?: string;

// eslint-disable-next-line no-restricted-globals
private _itemFocusTimeout?: ReturnType<typeof setTimeout>;

private _itemRenderAction?: (event?: ActionArgs<TItem>) => void;
Expand Down Expand Up @@ -605,6 +604,10 @@ class CollectionWidget<
this.setAria('activedescendant', null, $target);
}

_getItemIdTarget($target: dxElementWrapper): dxElementWrapper {
return $target;
}

_refreshItemId(
$target: dxElementWrapper,
needCleanItemId: boolean | undefined,
Expand All @@ -616,10 +619,12 @@ class CollectionWidget<
return;
}

const $idTarget = this._getItemIdTarget($target);

if (!needCleanItemId && focusedElement) {
this.setAria('id', this.getFocusedItemId(), $target);
this.setAria('id', this.getFocusedItemId(), $idTarget);
} else {
this.setAria('id', null, $target);
this.setAria('id', null, $idTarget);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Guid from '@js/core/guid';
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import { deferRender } from '@js/core/utils/common';
Expand All @@ -13,6 +14,8 @@ const RADIO_BUTTON_ICON_DOT_CLASS = 'dx-radiobutton-icon-dot';
const RADIO_VALUE_CONTAINER_CLASS = 'dx-radio-value-container';
const RADIO_BUTTON_CLASS = 'dx-radiobutton';

const ITEM_CONTENT_CLASS = 'dx-item-content';

export type Properties = CollectionWidgetBaseProperties<RadioCollection>;

class RadioCollection extends CollectionWidget<Properties> {
Expand All @@ -29,9 +32,7 @@ class RadioCollection extends CollectionWidget<Properties> {
const defaultOptions = super._getDefaultOptions();

// @ts-expect-error
return extend(defaultOptions, DataExpressionMixin._dataExpressionDefaultOptions(), {
_itemAttributes: { role: 'radio' },
});
return extend(defaultOptions, DataExpressionMixin._dataExpressionDefaultOptions());
}

_initMarkup(): void {
Expand All @@ -47,20 +48,59 @@ class RadioCollection extends CollectionWidget<Properties> {
return this._focusTarget();
}

// eslint-disable-next-line class-methods-use-this
_getItemIdTarget($target: dxElementWrapper): dxElementWrapper {
const $radioContainer = $target.find(`.${RADIO_VALUE_CONTAINER_CLASS}`);

if ($radioContainer.length) {
return $radioContainer;
}

return $target;
}

_postprocessRenderItem(args): void {
const { itemData: { html }, itemElement } = args;
const { itemData, itemElement } = args;
const { html } = itemData;

const $itemElement = $(itemElement);

if (!html) {
const $radio = $('<div>').addClass(RADIO_BUTTON_ICON_CLASS);

$('<div>').addClass(RADIO_BUTTON_ICON_DOT_CLASS).appendTo($radio);
$('<div>')
.addClass(RADIO_BUTTON_ICON_DOT_CLASS)
.appendTo($radio);

const $radioContainer = $('<div>').append($radio).addClass(RADIO_VALUE_CONTAINER_CLASS);
const $radioContainer = $('<div>')
.append($radio)
.addClass(RADIO_VALUE_CONTAINER_CLASS);

$(itemElement).prepend($radioContainer);
$itemElement.prepend($radioContainer);
}

super._postprocessRenderItem(args);

// eslint-disable-next-line spellcheck/spell-checker
const aria: { role: string; labelledby?: string } = {
role: 'radio',
};

if (!html) {
const $itemContent = $itemElement.find(`.${ITEM_CONTENT_CLASS}`);

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const contentId = $itemContent.attr('id') || `dx-${new Guid()}`;

$itemContent.attr('id', contentId);

// eslint-disable-next-line spellcheck/spell-checker
aria.labelledby = contentId;
}

const $ariaTarget = this._getItemIdTarget($itemElement);

this.setAria(aria, $ariaTarget);
}

_processSelectableItem(
Expand All @@ -75,7 +115,10 @@ class RadioCollection extends CollectionWidget<Properties> {
.first()
.toggleClass(RADIO_BUTTON_ICON_CHECKED_CLASS, isSelected);

this.setAria('checked', isSelected, $itemElement);
const $radioContainer = $itemElement.find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
const $ariaCheckedTarget = $radioContainer.length ? $radioContainer : $itemElement;

this.setAria('checked', isSelected, $ariaCheckedTarget);
}

_refreshContent(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const RADIO_BUTTON_CLASS = 'dx-radiobutton';
const RADIO_BUTTON_CHECKED_CLASS = 'dx-radiobutton-checked';
const RADIO_GROUP_VERTICAL_CLASS = 'dx-radiogroup-vertical';
const RADIO_GROUP_HORIZONTAL_CLASS = 'dx-radiogroup-horizontal';
const RADIO_VALUE_CONTAINER_CLASS = 'dx-radio-value-container';
const ITEM_CONTENT_CLASS = 'dx-item-content';

const moduleConfig = {
beforeEach: function() {
Expand Down Expand Up @@ -319,23 +321,101 @@ QUnit.module('Aria accessibility', {
helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
});

QUnit.test('Items: [1, 2, 3], Item.selected: true', function() {
QUnit.test('Items: [1, 2, 3], Item.selected: true', function(assert) {
helper.createWidget({ items: [1, 2, 3], value: 1 });

helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
helper.checkItemsAttributes([0], { attributes: ['aria-checked'], role: 'radio' });
helper.checkItemsAttributes([], {});

helper.getItems().each((index, item) => {
const $radioContainer = $(item).find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
const $itemContent = $(item).find(`.${ITEM_CONTENT_CLASS}`);
const contentId = $itemContent.attr('id');

assert.ok(contentId, `item[${index}] content element has an id`);
assert.strictEqual($radioContainer.attr('role'), 'radio', `item[${index}] radio container has role="radio"`);
assert.strictEqual($radioContainer.attr('aria-checked'), index === 0 ? 'true' : 'false', `item[${index}] radio container has correct aria-checked`);
assert.strictEqual($radioContainer.attr('aria-labelledby'), contentId, `item[${index}] radio container aria-labelledby references content id`);
});
});

QUnit.test('Items: [1, 2, 3], Item.selected: true, set focusedElement -> clean focusedElement', function() {
QUnit.test('Items: [1, 2, 3], Item.selected: true, set focusedElement -> clean focusedElement', function(assert) {
helper.createWidget({ items: [1, 2, 3], value: 1 });

helper.widget.option('focusedElement', helper.getItems().eq(0));
helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
helper.checkItemsAttributes([0], { attributes: ['aria-checked'], role: 'radio' });
helper.checkItemsAttributes([], {});

helper.getItems().each((index, item) => {
const $radioContainer = $(item).find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
const $itemContent = $(item).find(`.${ITEM_CONTENT_CLASS}`);
const contentId = $itemContent.attr('id');

assert.ok(contentId, `item[${index}] content element has an id`);
assert.strictEqual($radioContainer.attr('role'), 'radio', `item[${index}] radio container has role="radio"`);
assert.strictEqual($radioContainer.attr('aria-checked'), index === 0 ? 'true' : 'false', `item[${index}] radio container has correct aria-checked`);
assert.strictEqual($radioContainer.attr('aria-labelledby'), contentId, `item[${index}] radio container aria-labelledby references content id`);
});

helper.widget.option('focusedElement', null);
helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
helper.checkItemsAttributes([0], { attributes: ['aria-checked'], role: 'radio' });
helper.checkItemsAttributes([], {});

helper.getItems().each((index, item) => {
const $radioContainer = $(item).find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
const $itemContent = $(item).find(`.${ITEM_CONTENT_CLASS}`);
const contentId = $itemContent.attr('id');

assert.ok(contentId, `item[${index}] content element has an id after clearing focusedElement`);
assert.strictEqual($radioContainer.attr('role'), 'radio', `item[${index}] radio container has role="radio" after clearing focusedElement`);
assert.strictEqual($radioContainer.attr('aria-checked'), index === 0 ? 'true' : 'false', `item[${index}] radio container has correct aria-checked after clearing focusedElement`);
assert.strictEqual($radioContainer.attr('aria-labelledby'), contentId, `item[${index}] radio container aria-labelledby references content id after clearing focusedElement`);
});
});

QUnit.test('Items with itemTemplate: radio container has correct aria attributes', function(assert) {
helper.createWidget({
items: [{ text: 'custom 1' }, { text: 'custom 2' }],
itemTemplate(itemData, _, itemElement) {
const $button = $('<button>').text(itemData.text);
itemElement.append($button);
},
});

helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
helper.checkItemsAttributes([], {});

helper.getItems().each((index, item) => {
const $radioContainer = $(item).find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
const $itemContent = $(item).find(`.${ITEM_CONTENT_CLASS}`);
const contentId = $itemContent.attr('id');

assert.ok(contentId, `item[${index}] content element has an id`);
assert.strictEqual($radioContainer.attr('role'), 'radio', `item[${index}] radio container has role="radio"`);
assert.strictEqual($radioContainer.attr('aria-checked'), 'false', `item[${index}] radio container has aria-checked="false" initially`);
assert.strictEqual($radioContainer.attr('aria-labelledby'), contentId, `item[${index}] radio container aria-labelledby references content id`);
});
});

QUnit.test('Item with html: role="radio" is set on item element, no radio container created', function(assert) {
helper.createWidget({
items: [
{ html: '<span>Option A</span>' },
{ html: '<span>Option B</span>' },
],
});

helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');

helper.getItems().each((index, item) => {
const $item = $(item);
const $radioContainer = $item.find(`.${RADIO_VALUE_CONTAINER_CLASS}`);

assert.strictEqual($radioContainer.length, 0, `item[${index}] has no radio container when html is provided`);
assert.strictEqual($item.attr('role'), 'radio', `item[${index}] element itself has role="radio"`);
assert.strictEqual($item.attr('aria-checked'), 'false', `item[${index}] element has aria-checked="false" by default`);
assert.strictEqual($item.attr('aria-labelledby'), undefined, `item[${index}] element has no aria-labelledby when html is provided`);
});
});
});

Expand Down
Loading
Loading