Skip to content

Commit fa4245e

Browse files
marker-daomarker dao ®
andauthored
RadioGroup: Move radio semantic from container to value container (#32926)
Co-authored-by: marker dao ® <youdontknow@marker-dao.eth>
1 parent 70844dd commit fa4245e

File tree

7 files changed

+282
-25
lines changed

7 files changed

+282
-25
lines changed

apps/demos/Demos/RadioGroup/Overview/jQuery/index.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ $(() => {
1919
$('#radio-group-with-template').dxRadioGroup({
2020
items: priorities,
2121
value: priorities[2],
22-
itemTemplate(itemData, _, itemElement) {
23-
itemElement
24-
.parent().addClass(itemData.toLowerCase())
25-
.text(itemData);
22+
itemTemplate: (itemData, _, itemElement) => {
23+
itemElement.text(itemData);
2624
},
27-
});
25+
onValueChanged: (e) => {
26+
const $element = $(e.element);
27+
const priorityClass = e.previousValue.toLowerCase();
28+
const newPriorityClass = e.value.toLowerCase();
29+
30+
$element.removeClass(priorityClass);
31+
$element.addClass(newPriorityClass);
32+
},
33+
}).addClass(priorities[2].toLowerCase());
2834

2935
const radioGroup = $('#radio-group-with-selection').dxRadioGroup({
3036
items: priorityEntities,

apps/demos/Demos/RadioGroup/Overview/jQuery/styles.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
.low.dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
1+
.low .dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
22
background: gray;
33
}
44

5-
.normal.dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
5+
.normal .dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
66
background: green;
77
}
88

9-
.urgent.dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
9+
.urgent .dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
1010
background: orange;
1111
}
1212

13-
.high.dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
13+
.high .dx-radiobutton-checked .dx-radiobutton-icon .dx-radiobutton-icon-dot {
1414
background: red;
1515
}
1616

e2e/testcafe-devextreme/tests/accessibility/radioGroup.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,29 @@ const configuration: Configuration = {
2424
};
2525

2626
testAccessibility(configuration);
27+
28+
const buttons = [
29+
{
30+
text: 'custom 1',
31+
},
32+
{
33+
text: 'custom 2',
34+
},
35+
];
36+
37+
const interactiveItemsConfiguration: Configuration = {
38+
component: 'dxRadioGroup',
39+
a11yCheckConfig,
40+
options: {
41+
items: [buttons],
42+
itemTemplate: [
43+
(itemData, _, itemElement) => {
44+
const $button = $('<button>').text(itemData.text);
45+
46+
itemElement.append($button);
47+
},
48+
],
49+
},
50+
};
51+
52+
testAccessibility(interactiveItemsConfiguration);

packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ class CollectionWidget<
156156
> extends Widget<TProperties> {
157157
private _focusedItemId?: string;
158158

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

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

607+
_getItemIdTarget($target: dxElementWrapper): dxElementWrapper {
608+
return $target;
609+
}
610+
608611
_refreshItemId(
609612
$target: dxElementWrapper,
610613
needCleanItemId: boolean | undefined,
@@ -616,10 +619,12 @@ class CollectionWidget<
616619
return;
617620
}
618621

622+
const $idTarget = this._getItemIdTarget($target);
623+
619624
if (!needCleanItemId && focusedElement) {
620-
this.setAria('id', this.getFocusedItemId(), $target);
625+
this.setAria('id', this.getFocusedItemId(), $idTarget);
621626
} else {
622-
this.setAria('id', null, $target);
627+
this.setAria('id', null, $idTarget);
623628
}
624629
}
625630

packages/devextreme/js/__internal/ui/radio_group/m_radio_collection.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Guid from '@js/core/guid';
12
import type { dxElementWrapper } from '@js/core/renderer';
23
import $ from '@js/core/renderer';
34
import { deferRender } from '@js/core/utils/common';
@@ -13,6 +14,8 @@ const RADIO_BUTTON_ICON_DOT_CLASS = 'dx-radiobutton-icon-dot';
1314
const RADIO_VALUE_CONTAINER_CLASS = 'dx-radio-value-container';
1415
const RADIO_BUTTON_CLASS = 'dx-radiobutton';
1516

17+
const ITEM_CONTENT_CLASS = 'dx-item-content';
18+
1619
export type Properties = CollectionWidgetBaseProperties<RadioCollection>;
1720

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

3134
// @ts-expect-error
32-
return extend(defaultOptions, DataExpressionMixin._dataExpressionDefaultOptions(), {
33-
_itemAttributes: { role: 'radio' },
34-
});
35+
return extend(defaultOptions, DataExpressionMixin._dataExpressionDefaultOptions());
3536
}
3637

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

51+
// eslint-disable-next-line class-methods-use-this
52+
_getItemIdTarget($target: dxElementWrapper): dxElementWrapper {
53+
const $radioContainer = $target.find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
54+
55+
if ($radioContainer.length) {
56+
return $radioContainer;
57+
}
58+
59+
return $target;
60+
}
61+
5062
_postprocessRenderItem(args): void {
51-
const { itemData: { html }, itemElement } = args;
63+
const { itemData, itemElement } = args;
64+
const { html } = itemData;
65+
66+
const $itemElement = $(itemElement);
5267

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

56-
$('<div>').addClass(RADIO_BUTTON_ICON_DOT_CLASS).appendTo($radio);
71+
$('<div>')
72+
.addClass(RADIO_BUTTON_ICON_DOT_CLASS)
73+
.appendTo($radio);
5774

58-
const $radioContainer = $('<div>').append($radio).addClass(RADIO_VALUE_CONTAINER_CLASS);
75+
const $radioContainer = $('<div>')
76+
.append($radio)
77+
.addClass(RADIO_VALUE_CONTAINER_CLASS);
5978

60-
$(itemElement).prepend($radioContainer);
79+
$itemElement.prepend($radioContainer);
6180
}
6281

6382
super._postprocessRenderItem(args);
83+
84+
// eslint-disable-next-line spellcheck/spell-checker
85+
const aria: { role: string; labelledby?: string } = {
86+
role: 'radio',
87+
};
88+
89+
if (!html) {
90+
const $itemContent = $itemElement.find(`.${ITEM_CONTENT_CLASS}`);
91+
92+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
93+
const contentId = $itemContent.attr('id') || `dx-${new Guid()}`;
94+
95+
$itemContent.attr('id', contentId);
96+
97+
// eslint-disable-next-line spellcheck/spell-checker
98+
aria.labelledby = contentId;
99+
}
100+
101+
const $ariaTarget = this._getItemIdTarget($itemElement);
102+
103+
this.setAria(aria, $ariaTarget);
64104
}
65105

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

78-
this.setAria('checked', isSelected, $itemElement);
118+
const $radioContainer = $itemElement.find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
119+
const $ariaCheckedTarget = $radioContainer.length ? $radioContainer : $itemElement;
120+
121+
this.setAria('checked', isSelected, $ariaCheckedTarget);
79122
}
80123

81124
_refreshContent(): void {

packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/radioGroup.markup.tests.js

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const RADIO_BUTTON_CLASS = 'dx-radiobutton';
2121
const RADIO_BUTTON_CHECKED_CLASS = 'dx-radiobutton-checked';
2222
const RADIO_GROUP_VERTICAL_CLASS = 'dx-radiogroup-vertical';
2323
const RADIO_GROUP_HORIZONTAL_CLASS = 'dx-radiogroup-horizontal';
24+
const RADIO_VALUE_CONTAINER_CLASS = 'dx-radio-value-container';
25+
const ITEM_CONTENT_CLASS = 'dx-item-content';
2426

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

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

325327
helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
326-
helper.checkItemsAttributes([0], { attributes: ['aria-checked'], role: 'radio' });
328+
helper.checkItemsAttributes([], {});
329+
330+
helper.getItems().each((index, item) => {
331+
const $radioContainer = $(item).find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
332+
const $itemContent = $(item).find(`.${ITEM_CONTENT_CLASS}`);
333+
const contentId = $itemContent.attr('id');
334+
335+
assert.ok(contentId, `item[${index}] content element has an id`);
336+
assert.strictEqual($radioContainer.attr('role'), 'radio', `item[${index}] radio container has role="radio"`);
337+
assert.strictEqual($radioContainer.attr('aria-checked'), index === 0 ? 'true' : 'false', `item[${index}] radio container has correct aria-checked`);
338+
assert.strictEqual($radioContainer.attr('aria-labelledby'), contentId, `item[${index}] radio container aria-labelledby references content id`);
339+
});
327340
});
328341

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

332345
helper.widget.option('focusedElement', helper.getItems().eq(0));
333346
helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
334-
helper.checkItemsAttributes([0], { attributes: ['aria-checked'], role: 'radio' });
347+
helper.checkItemsAttributes([], {});
348+
349+
helper.getItems().each((index, item) => {
350+
const $radioContainer = $(item).find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
351+
const $itemContent = $(item).find(`.${ITEM_CONTENT_CLASS}`);
352+
const contentId = $itemContent.attr('id');
353+
354+
assert.ok(contentId, `item[${index}] content element has an id`);
355+
assert.strictEqual($radioContainer.attr('role'), 'radio', `item[${index}] radio container has role="radio"`);
356+
assert.strictEqual($radioContainer.attr('aria-checked'), index === 0 ? 'true' : 'false', `item[${index}] radio container has correct aria-checked`);
357+
assert.strictEqual($radioContainer.attr('aria-labelledby'), contentId, `item[${index}] radio container aria-labelledby references content id`);
358+
});
335359

336360
helper.widget.option('focusedElement', null);
337361
helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
338-
helper.checkItemsAttributes([0], { attributes: ['aria-checked'], role: 'radio' });
362+
helper.checkItemsAttributes([], {});
363+
364+
helper.getItems().each((index, item) => {
365+
const $radioContainer = $(item).find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
366+
const $itemContent = $(item).find(`.${ITEM_CONTENT_CLASS}`);
367+
const contentId = $itemContent.attr('id');
368+
369+
assert.ok(contentId, `item[${index}] content element has an id after clearing focusedElement`);
370+
assert.strictEqual($radioContainer.attr('role'), 'radio', `item[${index}] radio container has role="radio" after clearing focusedElement`);
371+
assert.strictEqual($radioContainer.attr('aria-checked'), index === 0 ? 'true' : 'false', `item[${index}] radio container has correct aria-checked after clearing focusedElement`);
372+
assert.strictEqual($radioContainer.attr('aria-labelledby'), contentId, `item[${index}] radio container aria-labelledby references content id after clearing focusedElement`);
373+
});
374+
});
375+
376+
QUnit.test('Items with itemTemplate: radio container has correct aria attributes', function(assert) {
377+
helper.createWidget({
378+
items: [{ text: 'custom 1' }, { text: 'custom 2' }],
379+
itemTemplate(itemData, _, itemElement) {
380+
const $button = $('<button>').text(itemData.text);
381+
itemElement.append($button);
382+
},
383+
});
384+
385+
helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
386+
helper.checkItemsAttributes([], {});
387+
388+
helper.getItems().each((index, item) => {
389+
const $radioContainer = $(item).find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
390+
const $itemContent = $(item).find(`.${ITEM_CONTENT_CLASS}`);
391+
const contentId = $itemContent.attr('id');
392+
393+
assert.ok(contentId, `item[${index}] content element has an id`);
394+
assert.strictEqual($radioContainer.attr('role'), 'radio', `item[${index}] radio container has role="radio"`);
395+
assert.strictEqual($radioContainer.attr('aria-checked'), 'false', `item[${index}] radio container has aria-checked="false" initially`);
396+
assert.strictEqual($radioContainer.attr('aria-labelledby'), contentId, `item[${index}] radio container aria-labelledby references content id`);
397+
});
398+
});
399+
400+
QUnit.test('Item with html: role="radio" is set on item element, no radio container created', function(assert) {
401+
helper.createWidget({
402+
items: [
403+
{ html: '<span>Option A</span>' },
404+
{ html: '<span>Option B</span>' },
405+
],
406+
});
407+
408+
helper.checkAttributes(helper.$widget, { role: 'radiogroup', tabindex: '0' }, 'widget');
409+
410+
helper.getItems().each((index, item) => {
411+
const $item = $(item);
412+
const $radioContainer = $item.find(`.${RADIO_VALUE_CONTAINER_CLASS}`);
413+
414+
assert.strictEqual($radioContainer.length, 0, `item[${index}] has no radio container when html is provided`);
415+
assert.strictEqual($item.attr('role'), 'radio', `item[${index}] element itself has role="radio"`);
416+
assert.strictEqual($item.attr('aria-checked'), 'false', `item[${index}] element has aria-checked="false" by default`);
417+
assert.strictEqual($item.attr('aria-labelledby'), undefined, `item[${index}] element has no aria-labelledby when html is provided`);
418+
});
339419
});
340420
});
341421

0 commit comments

Comments
 (0)