Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ exports[`Options Column.HeaderFilter dataSource: custom dataSource 1`] = `
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 2
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -635,6 +641,12 @@ exports[`Options Column.HeaderFilter dataSource: custom dataSource with exclude
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 2
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -1003,6 +1015,12 @@ exports[`Options Column.HeaderFilter dataSource: custom dataSource with exclude
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 2
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -1371,6 +1389,12 @@ exports[`Options Column.HeaderFilter dataSource: custom dataSource with filter v
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 2
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -1850,6 +1874,12 @@ exports[`Options Column.HeaderFilter filterType + values: exclude filter 1`] = `
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 5
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -2329,6 +2359,12 @@ exports[`Options Column.HeaderFilter filterType + values: exclude filter with va
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 5
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -2808,6 +2844,12 @@ exports[`Options Column.HeaderFilter filterType + values: filter values 1`] = `
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 5
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -3139,6 +3181,12 @@ exports[`Options HeaderFilter texts: custom translations 1`] = `
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 1
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -3470,6 +3518,12 @@ exports[`Options HeaderFilter texts: default translation 1`] = `
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 1
</div>
</div>
</div>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ exports[`HeaderFilter View integration should render popup with list by default
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 5
</div>
</div>
</div>
<div
Expand Down Expand Up @@ -857,6 +863,12 @@ exports[`HeaderFilter View integration should render popup with tree list if dat
style="width: 222px; height: 90px;"
/>
</div>
<div
class="dx-screen-reader-only"
role="status"
>
Items: 5
</div>
</div>
</div>
<div
Expand Down
29 changes: 29 additions & 0 deletions packages/devextreme/js/__internal/ui/list/list.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const LIST_SELECT_CHECKBOX = 'dx-list-select-checkbox';
const LIST_SELECT_RADIOBUTTON = 'dx-list-select-radiobutton';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check previous solution.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const WRAP_ITEM_TEXT_CLASS = 'dx-wrap-item-text';
const SELECT_ALL_ITEM_SELECTOR = '.dx-list-select-all';
export const SCREEN_READER_ONLY_CLASS = 'dx-screen-reader-only';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if it can be imported.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if we can reuse dx-gridbase-a11y-status-container

@dmlvr dmlvr Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


const LIST_ITEM_DATA_KEY = 'dxListItemData';
const LIST_FEEDBACK_SHOW_TIMEOUT = 70;
Expand Down Expand Up @@ -132,6 +133,8 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {

_$nextButton!: dxElementWrapper | null;

_$a11yStatusContainer?: dxElementWrapper;

_holdTimer?: ReturnType<typeof setTimeout>;

_loadNextPageTimer?: ReturnType<typeof setTimeout>;
Expand Down Expand Up @@ -975,6 +978,7 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
this._itemElementsCache = $();

this.$element().addClass(LIST_CLASS);
this._renderA11yStatusContainer();
super._initMarkup();

const { useInkRipple } = this.option();
Expand All @@ -997,10 +1001,35 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
const isEmpty = super._renderEmptyMessage(rootNodes);

this.setAria({ role: isEmpty ? undefined : 'application' }, this._focusTarget());
this._updateA11yStatusText();

return isEmpty;
}

_renderA11yStatusContainer(): void {
if (this._$a11yStatusContainer?.get(0)) {
return;
}

this._$a11yStatusContainer = $('<div>')
.addClass(SCREEN_READER_ONLY_CLASS)
.attr('role', 'status')
.appendTo(this.$element());
}
Comment on lines +1009 to +1018

_updateA11yStatusText(): void {
if (this._dataController.isLoading()) {
this._$a11yStatusContainer?.text('');
return;
}

const { noDataText } = this.option();
const itemsCount = this._editStrategy.itemsGetter().length;
const itemsLabel = messageLocalization.format('dxList-listAriaLabel');

this._$a11yStatusContainer?.text(itemsCount ? `${itemsLabel}: ${itemsCount}` : noDataText ?? '');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check what we need to announce.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check ?? ''

@dmlvr dmlvr Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noDataText from options

const { noDataText } = this.option() 

can be undefined. But dxElementWrapper.text do not support undefined

dxElementWrapper.text(text: string | number | boolean). 

So this is the best way to support typing

Image

}

_isMultiSelectMode(): boolean {
const { selectionMode } = this.option();
return selectionMode === 'multiple' || selectionMode === 'all';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3969,6 +3969,62 @@ if(devices.real().deviceType === 'desktop') {
});
}

QUnit.module('screen reader announcements (T1334729)', {
beforeEach: function() {
fx.off = true;
this.clock = sinon.useFakeTimers();

this.instance = $('#lookup').dxLookup({
dataSource: [1, 2, 3],
searchTimeout: 0,
'dropDownOptions.animation': null,
opened: true
}).dxLookup('instance');

this.search = (value) => {
this.instance._searchBox.option('value', value);
this.clock.tick(0);
};

this.getA11yStatus = () => $(`.${LIST_CLASS}`).children('[role="status"]');
this.getItemsAnnouncement = (itemsCount) => `${messageLocalization.format('dxList-listAriaLabel')}: ${itemsCount}`;
},
afterEach: function() {
this.clock.restore();
fx.off = false;
}
}, () => {
QUnit.test('list should have a single live region', function(assert) {
assert.strictEqual(this.getA11yStatus().length, 1);
});

QUnit.test('item count should be announced when the drop-down is opened', function(assert) {
assert.strictEqual(this.getA11yStatus().text(), this.getItemsAnnouncement(3));
});

QUnit.test('noDataText should be announced if search returns no items', function(assert) {
this.search('4');

assert.strictEqual(this.getA11yStatus().text(), messageLocalization.format('dxCollectionWidget-noDataText'));
});

QUnit.test('item count should be announced if search returns items', function(assert) {
this.search('1');

assert.strictEqual(this.getA11yStatus().text(), this.getItemsAnnouncement(1));
});

QUnit.test('live region should not be re-created on search', function(assert) {
const a11yStatusElement = this.getA11yStatus().get(0);

this.search('4');

assert.strictEqual(this.getA11yStatus().get(0), a11yStatusElement, 'the live region is the same element');
assert.strictEqual(a11yStatusElement.textContent, messageLocalization.format('dxCollectionWidget-noDataText'),
'the announcement is written into that element, otherwise screen readers do not announce it');
});
});

QUnit.module('default options', {
beforeEach: function() {
fx.off = true;
Expand Down
Loading
Loading