From 942ae9182fdf0e740e49ffa0b105c6d2474c49ac Mon Sep 17 00:00:00 2001 From: pharret31 Date: Wed, 21 Jan 2026 10:25:11 +0100 Subject: [PATCH 1/7] validation summary --- .../scss/widgets/base/_gridBase.scss | 9 ---- .../scss/widgets/base/_ui.scss | 10 ++++ .../views/a11y_status_container_component.ts | 2 +- .../__snapshots__/widget.test.ts.snap | 14 ++--- .../new/grid_core/accessibility/status.tsx | 2 +- .../a11y_status/a11y_status_render.ts | 2 +- .../js/__internal/ui/m_validation_summary.ts | 38 ++++++++++++++ .../validationSummary.markup.tests.js | 51 +++++++++++++++++-- 8 files changed, 104 insertions(+), 24 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/_gridBase.scss b/packages/devextreme-scss/scss/widgets/base/_gridBase.scss index f7611e450b53..77fee48bcfc7 100644 --- a/packages/devextreme-scss/scss/widgets/base/_gridBase.scss +++ b/packages/devextreme-scss/scss/widgets/base/_gridBase.scss @@ -17,15 +17,6 @@ flex: 0 0 auto; } -// NOTE: a11y aria-live container must be visible to allow screen readers read it -.dx-gridbase-a11y-status-container { - position: fixed; - left: 0; - top: 0; - clip: rect(1px, 1px, 1px, 1px); - clip-path: polygon(0 0); -} - @mixin grid-base($widget-name) { $grid-cell-padding: 7px; $grid-texteditor-input-padding: 32px; diff --git a/packages/devextreme-scss/scss/widgets/base/_ui.scss b/packages/devextreme-scss/scss/widgets/base/_ui.scss index 48a4d628c3cd..98de253df885 100644 --- a/packages/devextreme-scss/scss/widgets/base/_ui.scss +++ b/packages/devextreme-scss/scss/widgets/base/_ui.scss @@ -38,6 +38,16 @@ z-index: $max-integer; } +.dx-screen-reader-only { + position: absolute; + height: 1px; + width: 1px; + overflow: hidden; + white-space: nowrap; + clip: rect(0 0 0 0); + clip-path: inset(50%); +} + /* animations */ .dx-animating { diff --git a/packages/devextreme/js/__internal/grids/grid_core/views/a11y_status_container_component.ts b/packages/devextreme/js/__internal/grids/grid_core/views/a11y_status_container_component.ts index 875af6837f7d..3a75fac9c3e6 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/views/a11y_status_container_component.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/views/a11y_status_container_component.ts @@ -2,7 +2,7 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; const CLASSES = { - container: 'dx-gridbase-a11y-status-container', + container: 'dx-screen-reader-only', }; export interface A11yStatusContainerComponentProps { diff --git a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap index d3e76a106f46..2e93597f2ee6 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap @@ -10,13 +10,13 @@ exports[`common initial render should be successfull 1`] = ` role="group" >
- +
@@ -51,7 +51,7 @@ exports[`common initial render should be successfull 1`] = ` class="dx-cardview-headerpanel-text-empty" role="menuitem" > - Use + Use @@ -81,7 +81,7 @@ exports[`common initial render should be successfull 1`] = `
- +
@@ -141,7 +141,7 @@ exports[`common initial render should be successfull 1`] = `
- +
- - + +
diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/status.tsx b/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/status.tsx index 819e89cdf83a..a2c7e78bd5e6 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/status.tsx +++ b/packages/devextreme/js/__internal/grids/new/grid_core/accessibility/status.tsx @@ -4,7 +4,7 @@ import { CLASSES as BASE_CLASSES } from '../const'; const CLASSES = { ...BASE_CLASSES, - container: 'dx-gridbase-a11y-status-container', + container: 'dx-screen-reader-only', }; export interface A11yStatusContainerComponentProps { diff --git a/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_render.ts b/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_render.ts index a07a6fbd817d..558f93c69c36 100644 --- a/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_render.ts +++ b/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_render.ts @@ -2,7 +2,7 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; const CLASSES = { - container: 'dx-scheduler-a11y-status-container', + container: 'dx-screen-reader-only', }; export const createA11yStatusContainer = (statusText = ''): dxElementWrapper => $('
') diff --git a/packages/devextreme/js/__internal/ui/m_validation_summary.ts b/packages/devextreme/js/__internal/ui/m_validation_summary.ts index 8b8ea4d2ecd6..f69b6f030da6 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_summary.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_summary.ts @@ -1,5 +1,7 @@ import eventsEngine from '@js/common/core/events/core/events_engine'; import registerComponent from '@js/core/component_registrator'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; // @ts-expect-error ts-error import { grep } from '@js/core/utils/common'; import { extend } from '@js/core/utils/extend'; @@ -12,6 +14,7 @@ import ValidationEngine from './m_validation_engine'; import type ValidationGroup from './m_validation_group'; const VALIDATION_SUMMARY_CLASS = 'dx-validationsummary'; +const VALIDATION_SUMMARY_SCREEN_READER_ONLY = 'dx-screen-reader-only'; const ITEM_CLASS = `${VALIDATION_SUMMARY_CLASS}-item`; const ITEM_DATA_KEY = `${VALIDATION_SUMMARY_CLASS}-item-data`; @@ -26,6 +29,10 @@ class ValidationSummary extends CollectionWidget { validators?: any[]; + _$announceContainer?: dxElementWrapper; + + _lastAnnouncedText?: string; + groupSubscription?: (params) => void; _getDefaultOptions(): ValidationSummaryProperties { @@ -117,6 +124,31 @@ class ValidationSummary extends CollectionWidget { }); this.option('items', items); + + this._announceOnGroupValidation(items); + } + + _announceOnGroupValidation(items): void { + if (!items?.length) { + this._lastAnnouncedText = ''; + this._$announceContainer?.text(''); + return; + } + + const text = items.map((item) => item.text).join('. '); + + if (text !== this._lastAnnouncedText) { + this._lastAnnouncedText = text; + this._announceText(text); + } + } + + _announceText(text: string): void { + if (!this._$announceContainer) { + return; + } + + this._$announceContainer.text(text); } _itemValidationHandler({ isValid, validator, brokenRules }): void { @@ -164,6 +196,12 @@ class ValidationSummary extends CollectionWidget { _initMarkup(): void { this.$element().addClass(VALIDATION_SUMMARY_CLASS); + + this._$announceContainer = $('
') + .addClass(VALIDATION_SUMMARY_SCREEN_READER_ONLY) + .attr('role', 'alert') + .appendTo(this.element()); + super._initMarkup(); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js index c2fc8bb49abd..75bae7c88db0 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js @@ -51,7 +51,7 @@ QUnit.module('General', { }); - QUnit.test('Summary can subscribe on group\'s Validated event', function(assert) { + QUnit.test('Summary can subscribe on group Validated event', function(assert) { const group = 'group1'; const validator = sinon.createStubInstance(Validator); validator.validate.returns({ isValid: true, brokenRule: null }); @@ -173,7 +173,7 @@ QUnit.module('Regression', { assert.strictEqual(group, summary.option('validationGroup')); }); - QUnit.test('T212238: Summary can subscribe on group\'s Validated event when Summary is created before any validator in group', function(assert) { + QUnit.test('T212238: Summary can subscribe on group Validated event when Summary is created before any validator in group', function(assert) { const group = 'group1'; const validator = sinon.createStubInstance(Validator); validator.validate.returns({ isValid: true, brokenRule: null }); @@ -232,7 +232,7 @@ QUnit.module('Regression', { }); }); -QUnit.module('Update on validator\'s validation', { +QUnit.module('Update on validator validation', { beforeEach: function() { this.fixture = new Fixture(); } @@ -408,7 +408,7 @@ QUnit.module('Update on validator\'s validation', { assert.equal(items[1].text, message + ' 2', 'Message should be updated'); }); - QUnit.test('T270338: Summary should subscribe to validator\'s events only once', function(assert) { + QUnit.test('T270338: Summary should subscribe to validator events only once', function(assert) { const validator1 = this.fixture.createValidator({ validationGroup: 'group1', validationRules: [{ @@ -428,7 +428,7 @@ QUnit.module('Update on validator\'s validation', { assert.equal(spy.callCount, 1, 'Render of validation summary should be called only once'); }); - QUnit.test('T270338 - the \'items\' option changed should not be called if validator state is not changed', function(assert) { + QUnit.test('T270338 - the items option changed should not be called if validator state is not changed', function(assert) { let itemsChangedCallCount = 0; const validator = this.fixture.createValidator({ validationGroup: 'group', @@ -452,3 +452,44 @@ QUnit.module('Update on validator\'s validation', { assert.equal(itemsChangedCallCount, 1, 'items should not be changed if the validator state is not changed'); }); }); + +QUnit.module('Accessibility', { + beforeEach: function() { + this.fixture = new Fixture(); + } +}, () => { + QUnit.test('ValidationSummary should have announce container with validation summary for screen reader', function(assert) { + const summary = this.fixture.createSummary(); + const validator = sinon.createStubInstance(Validator); + const messages = ['test message 1', 'test message 2']; + + summary._groupValidationHandler({ + isValid: false, + brokenRules: [{ + type: 'async', + message: messages[0], + validator: validator, + }, + { + type: 'async', + message: messages[1], + validator: validator, + }], + validators: [validator] + }); + + const items = summary.option('items'); + + const $announceContainer = summary.$element().find('.dx-screen-reader-only'); + + assert.strictEqual($announceContainer.length, 1, 'announce container is present'); + assert.strictEqual($announceContainer.text(), messages.join('. '), 'announce container has correct text'); + }); + + QUnit.test('ValidationSummary announce container should have role=alert attribute', function(assert) { + const summary = this.fixture.createSummary(); + const $announceContainer = summary.$element().find('.dx-screen-reader-only'); + + assert.strictEqual($announceContainer.attr('role'), 'alert', 'role=alert is present'); + }); +}); From b6cf9a00dd8368b01110f60dcc18c0ae404f9b08 Mon Sep 17 00:00:00 2001 From: pharret31 Date: Wed, 21 Jan 2026 14:05:10 +0100 Subject: [PATCH 2/7] fixes --- .../scss/widgets/base/scheduler/_common.scss | 9 ------ .../__tests__/__mock__/model/scheduler.ts | 2 +- .../js/__internal/ui/m_validation_summary.ts | 28 ++++++++++++------- .../validationSummary.markup.tests.js | 14 +++++++++- .../DevExpress.ui.widgets.form/form.tests.js | 4 +-- packages/testcafe-models/scheduler/index.ts | 2 +- 6 files changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/_common.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/_common.scss index b34a4ed89777..0a0f2411c040 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/_common.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/_common.scss @@ -7,15 +7,6 @@ $scheduler-appointment-collector-margin: 3px; $scheduler-appointment-collector-height: 22px; $scheduler-popup-scrollable-content-padding: 20px; -// NOTE: a11y aria-live container must be visible to allow screen readers read it -.dx-scheduler-a11y-status-container { - position: fixed; - left: 0; - top: 0; - clip: rect(1px, 1px, 1px, 1px); - clip-path: polygon(0 0); -} - .dx-scheduler-legacy-appointment-popup { .dx-popup-content { padding-top: 0; diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts index 7b04f562f7d6..c65cee6d9b4c 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts @@ -26,7 +26,7 @@ export class SchedulerModel { } getStatusContent(): string { - return this.container.querySelector('.dx-scheduler-a11y-status-container')?.textContent ?? ''; + return this.container.querySelector('.dx-screen-reader-only')?.textContent ?? ''; } getAppointment(text?: string): AppointmentModel { diff --git a/packages/devextreme/js/__internal/ui/m_validation_summary.ts b/packages/devextreme/js/__internal/ui/m_validation_summary.ts index f69b6f030da6..282ca17962d7 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_summary.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_summary.ts @@ -14,7 +14,7 @@ import ValidationEngine from './m_validation_engine'; import type ValidationGroup from './m_validation_group'; const VALIDATION_SUMMARY_CLASS = 'dx-validationsummary'; -const VALIDATION_SUMMARY_SCREEN_READER_ONLY = 'dx-screen-reader-only'; +const SCREEN_READER_ONLY_CLASS = 'dx-screen-reader-only'; const ITEM_CLASS = `${VALIDATION_SUMMARY_CLASS}-item`; const ITEM_DATA_KEY = `${VALIDATION_SUMMARY_CLASS}-item-data`; @@ -143,12 +143,24 @@ class ValidationSummary extends CollectionWidget { } } + _removeAnnounceContainer(): void { + this._$announceContainer?.remove(); + this._$announceContainer = undefined; + } + + _renderAnnounceContainer(): void { + this._removeAnnounceContainer(); + + this._$announceContainer = $('
') + .addClass(SCREEN_READER_ONLY_CLASS) + .attr('role', 'alert') + .appendTo(this.element()); + } + _announceText(text: string): void { - if (!this._$announceContainer) { - return; - } + this._renderAnnounceContainer(); - this._$announceContainer.text(text); + this._$announceContainer?.text(text); } _itemValidationHandler({ isValid, validator, brokenRules }): void { @@ -197,11 +209,6 @@ class ValidationSummary extends CollectionWidget { _initMarkup(): void { this.$element().addClass(VALIDATION_SUMMARY_CLASS); - this._$announceContainer = $('
') - .addClass(VALIDATION_SUMMARY_SCREEN_READER_ONLY) - .attr('role', 'alert') - .appendTo(this.element()); - super._initMarkup(); } @@ -230,6 +237,7 @@ class ValidationSummary extends CollectionWidget { } _dispose(): void { + this._removeAnnounceContainer(); super._dispose(); this._unsubscribeGroup(); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js index 75bae7c88db0..346053d760e9 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js @@ -475,7 +475,7 @@ QUnit.module('Accessibility', { message: messages[1], validator: validator, }], - validators: [validator] + validators: [validator], }); const items = summary.option('items'); @@ -488,6 +488,18 @@ QUnit.module('Accessibility', { QUnit.test('ValidationSummary announce container should have role=alert attribute', function(assert) { const summary = this.fixture.createSummary(); + const validator = sinon.createStubInstance(Validator); + + summary._groupValidationHandler({ + isValid: false, + brokenRules: [{ + type: 'async', + message: 'test message', + validator: validator, + }], + validators: [validator], + }); + const $announceContainer = summary.$element().find('.dx-screen-reader-only'); assert.strictEqual($announceContainer.attr('role'), 'alert', 'role=alert is present'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js index 287061f56c2a..2a437b23dec1 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.form/form.tests.js @@ -4257,7 +4257,7 @@ QUnit.module('reset', () => { const validationItemsBeforeReset = $(`.${FORM_VALIDATION_SUMMARY}`).children(); - assert.strictEqual(validationItemsBeforeReset.length, 2, 'form has validation summary items before reset'); + assert.strictEqual(validationItemsBeforeReset.length, 3, 'form has validation summary items before reset'); form.reset(); @@ -4284,7 +4284,7 @@ QUnit.module('reset', () => { const summaryItemsAfterValidate = $(`.${FORM_VALIDATION_SUMMARY}`).children(); - assert.strictEqual(summaryItemsAfterValidate.length, 2, 'form has validation summary after validation'); + assert.strictEqual(summaryItemsAfterValidate.length, 3, 'form has validation summary after validation'); }); [ diff --git a/packages/testcafe-models/scheduler/index.ts b/packages/testcafe-models/scheduler/index.ts index 5c28a5bd04d0..d8caced8124a 100644 --- a/packages/testcafe-models/scheduler/index.ts +++ b/packages/testcafe-models/scheduler/index.ts @@ -34,7 +34,7 @@ export const CLASS = { workspaceBothScrollbar: 'dx-scheduler-work-space-both-scrollbar', workSpace: 'dx-scheduler-work-space', - statusContainer: 'dx-scheduler-a11y-status-container ', + statusContainer: 'dx-screen-reader-only', }; const ViewTypeClassesMap = { From f7efa0ee119dd1aacd9ce94ddaa830f1cb3abe7d Mon Sep 17 00:00:00 2001 From: pharret31 Date: Wed, 21 Jan 2026 15:41:48 +0100 Subject: [PATCH 3/7] update snapshot --- .../new/card_view/__snapshots__/widget.test.ts.snap | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap index 2e93597f2ee6..bca54befcf6d 100644 --- a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap +++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap @@ -16,7 +16,7 @@ exports[`common initial render should be successfull 1`] = `
- +
@@ -141,7 +141,7 @@ exports[`common initial render should be successfull 1`] = `
- +
- - + +
From 8bb6dfbd60358e89e9c1a485358e03b8f4c6282d Mon Sep 17 00:00:00 2001 From: pharret31 Date: Wed, 21 Jan 2026 16:21:15 +0100 Subject: [PATCH 4/7] fix --- packages/devextreme/js/__internal/ui/m_validation_summary.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/ui/m_validation_summary.ts b/packages/devextreme/js/__internal/ui/m_validation_summary.ts index 282ca17962d7..15f0cb49197f 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_summary.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_summary.ts @@ -131,7 +131,7 @@ class ValidationSummary extends CollectionWidget { _announceOnGroupValidation(items): void { if (!items?.length) { this._lastAnnouncedText = ''; - this._$announceContainer?.text(''); + this._removeAnnounceContainer(); return; } From 0b3284eda14b519f7f0be1ff6c2050e3c7438f75 Mon Sep 17 00:00:00 2001 From: pharret31 Date: Fri, 23 Jan 2026 13:38:20 +0100 Subject: [PATCH 5/7] fix --- .../js/__internal/ui/m_validation_summary.ts | 8 +- .../validationSummary.markup.tests.js | 103 ++++++++++++++---- 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/m_validation_summary.ts b/packages/devextreme/js/__internal/ui/m_validation_summary.ts index 15f0cb49197f..915271c11350 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_summary.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_summary.ts @@ -125,10 +125,12 @@ class ValidationSummary extends CollectionWidget { this.option('items', items); - this._announceOnGroupValidation(items); + this._announceOnGroupValidation(); } - _announceOnGroupValidation(items): void { + _announceOnGroupValidation(): void { + const { items } = this.option(); + if (!items?.length) { this._lastAnnouncedText = ''; this._removeAnnounceContainer(); @@ -153,7 +155,7 @@ class ValidationSummary extends CollectionWidget { this._$announceContainer = $('
') .addClass(SCREEN_READER_ONLY_CLASS) - .attr('role', 'alert') + .attr('role', 'status') .appendTo(this.element()); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js index 346053d760e9..fc263b76171a 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js @@ -456,52 +456,113 @@ QUnit.module('Update on validator validation', { QUnit.module('Accessibility', { beforeEach: function() { this.fixture = new Fixture(); + + this.summary = this.fixture.createSummary(); + this.validator = sinon.createStubInstance(Validator); + + this.formatErrors = (errors) => errors.join('. '); } }, () => { QUnit.test('ValidationSummary should have announce container with validation summary for screen reader', function(assert) { - const summary = this.fixture.createSummary(); - const validator = sinon.createStubInstance(Validator); const messages = ['test message 1', 'test message 2']; - summary._groupValidationHandler({ + this.summary._groupValidationHandler({ isValid: false, brokenRules: [{ - type: 'async', message: messages[0], - validator: validator, + validator: this.validator, }, { - type: 'async', message: messages[1], - validator: validator, + validator: this.validator, }], - validators: [validator], + validators: [this.validator], }); - const items = summary.option('items'); - - const $announceContainer = summary.$element().find('.dx-screen-reader-only'); + const $announceContainer = this.summary.$element().find('.dx-screen-reader-only'); assert.strictEqual($announceContainer.length, 1, 'announce container is present'); - assert.strictEqual($announceContainer.text(), messages.join('. '), 'announce container has correct text'); + assert.strictEqual($announceContainer.text(), this.formatErrors(messages), 'announce container has correct text'); }); QUnit.test('ValidationSummary announce container should have role=alert attribute', function(assert) { - const summary = this.fixture.createSummary(); - const validator = sinon.createStubInstance(Validator); - - summary._groupValidationHandler({ + this.summary._groupValidationHandler({ isValid: false, brokenRules: [{ - type: 'async', message: 'test message', - validator: validator, + validator: this.validator, }], - validators: [validator], + validators: [this.validator], + }); + + const $announceContainer = this.summary.$element().find('.dx-screen-reader-only'); + + assert.strictEqual($announceContainer.attr('role'), 'status', 'role=status is present'); + }); + + QUnit.test('ValidationSummary should remove announce container when there are no validation errors', function(assert) { + this.summary._groupValidationHandler({ + isValid: true, + validators: [this.validator], }); - const $announceContainer = summary.$element().find('.dx-screen-reader-only'); + let $announceContainer = this.summary.$element().find('.dx-screen-reader-only'); - assert.strictEqual($announceContainer.attr('role'), 'alert', 'role=alert is present'); + assert.strictEqual($announceContainer.length, 0, 'announce container is not present'); + }); + + QUnit.test('ValidationSummary should not create announce container if errors are the same', function(assert) { + const message = 'test message'; + + const groupValidationPayload = { + isValid: false, + brokenRules: [{ + message, + validator: this.validator, + }], + validators: [this.validator], + }; + + this.summary._groupValidationHandler(groupValidationPayload); + this.summary._groupValidationHandler(groupValidationPayload); + + const $announceContainer = this.summary.$element().find('.dx-screen-reader-only'); + + assert.strictEqual($announceContainer.get(0), undefined, 'announce container was not created'); + }); + + [ + ['second'], + ['first', 'second'], + ['second', 'first'], + ['first', 'second', 'third'], + [''], + ['', ''], + ].forEach((errors) => { + QUnit.test(`ValidationSummary should accordingly update announce container when errors change from ["first"] to ${JSON.stringify(errors)}`, function (assert) { + this.summary._groupValidationHandler({ + isValid: false, + brokenRules: [{ + message: 'first', + validator: this.validator, + }], + validators: [this.validator], + }); + + const brokenRules = errors.map((error) => ({ + message: error, + validator: this.validator, + })); + + this.summary._groupValidationHandler({ + isValid: false, + brokenRules, + validators: [this.validator], + }); + + const $announceContainer = this.summary.$element().find('.dx-screen-reader-only'); + + assert.strictEqual($announceContainer.text(), this.formatErrors(errors), 'announce container has updated text'); + }); }); }); From 96628c1c5b9835a71926cb1bc8ea8e8cf37b9234 Mon Sep 17 00:00:00 2001 From: pharret31 Date: Fri, 23 Jan 2026 13:51:22 +0100 Subject: [PATCH 6/7] fix --- .../validationSummary.markup.tests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js index fc263b76171a..7359582c185c 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js @@ -506,7 +506,7 @@ QUnit.module('Accessibility', { validators: [this.validator], }); - let $announceContainer = this.summary.$element().find('.dx-screen-reader-only'); + const $announceContainer = this.summary.$element().find('.dx-screen-reader-only'); assert.strictEqual($announceContainer.length, 0, 'announce container is not present'); }); @@ -539,7 +539,7 @@ QUnit.module('Accessibility', { [''], ['', ''], ].forEach((errors) => { - QUnit.test(`ValidationSummary should accordingly update announce container when errors change from ["first"] to ${JSON.stringify(errors)}`, function (assert) { + QUnit.test(`ValidationSummary should accordingly update announce container when errors change from ["first"] to ${JSON.stringify(errors)}`, function(assert) { this.summary._groupValidationHandler({ isValid: false, brokenRules: [{ From ff37041ca606a7842bb43887f1e29f0e5e84baea Mon Sep 17 00:00:00 2001 From: pharret31 Date: Fri, 23 Jan 2026 14:27:46 +0100 Subject: [PATCH 7/7] fix --- packages/devextreme/js/__internal/ui/m_validation_summary.ts | 2 +- .../validationSummary.markup.tests.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/m_validation_summary.ts b/packages/devextreme/js/__internal/ui/m_validation_summary.ts index 915271c11350..2750b640e444 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_summary.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_summary.ts @@ -155,7 +155,7 @@ class ValidationSummary extends CollectionWidget { this._$announceContainer = $('
') .addClass(SCREEN_READER_ONLY_CLASS) - .attr('role', 'status') + .attr('role', 'alert') .appendTo(this.element()); } diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js index 7359582c185c..945fe2b73cc7 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/validationSummary.markup.tests.js @@ -497,7 +497,7 @@ QUnit.module('Accessibility', { const $announceContainer = this.summary.$element().find('.dx-screen-reader-only'); - assert.strictEqual($announceContainer.attr('role'), 'status', 'role=status is present'); + assert.strictEqual($announceContainer.attr('role'), 'alert', 'role=alert is present'); }); QUnit.test('ValidationSummary should remove announce container when there are no validation errors', function(assert) {