diff --git a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js index 3560f7c2f7b4..befe1a36a0d7 100644 --- a/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js +++ b/packages/devextreme/js/ui/drop_down_editor/ui.drop_down_editor.js @@ -404,12 +404,15 @@ const DropDownEditor = TextBox.inherit({ }, _integrateInput: function() { + const { isValid } = this.option(); + this._renderFocusState(); this._refreshValueChangeEvent(); this._refreshEvents(); this._refreshEmptinessEvent(); this._setDefaultAria(); this._setFieldAria(); + this._toggleValidationClasses(!isValid); this.option('_onMarkupRendered')?.(); }, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js index 10519dc5b54a..efe6e08d19c1 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js @@ -2248,46 +2248,153 @@ QUnit.module('aria accessibility', () => { assert.equal($input.attr('aria-expanded'), 'false', 'aria-expanded property on closed'); }); - QUnit.test('component with fieldTemplate should retain aria attributes after interaction (T1230696, T1230971)', function(assert) { - const $dropDownEditor = $('#dropDownEditorSecond').dxDropDownEditor({ - dataSource: ['one', 'two', 'three'], - fieldTemplate: (data) => { - return $('
').dxTextBox({ value: data }); - }, - valueChangeEvent: 'keyup', - }).dxValidator({ - validationRules: [ { type: 'required' } ] - }); - let $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + [ + { attribute: 'aria-required', value: 'true' }, + { attribute: 'aria-haspopup', value: 'true' }, + { attribute: 'aria-autocomplete', value: 'none' }, + ].forEach(({ attribute, value }) => { + QUnit.test(`component with fieldTemplate should have proper ${attribute} attribute after interaction (T1230696, T1230971)`, function(assert) { + const $dropDownEditor = $('#dropDownEditorSecond').dxDropDownEditor({ + dataSource: ['one', 'two', 'three'], + fieldTemplate: (data) => { + return $('
').dxTextBox({ value: data }); + }, + valueChangeEvent: 'keyup', + }).dxValidator({ + validationRules: [ { type: 'required' } ] + }); + let $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); - assert.strictEqual($input.attr('aria-required'), 'true', 'initial render should have aria-required attribute set to true'); + assert.strictEqual($input.attr(attribute), value, `initial render should have ${attribute} attribute set to ${value}`); - assert.strictEqual($input.attr('aria-haspopup'), 'true', 'initial render should have aria-haspopup attribute set to true'); + keyboardMock($input) + .type('a'); - assert.strictEqual($input.attr('aria-autocomplete'), 'none', 'initial render should have aria-autocomplete attribute set to none'); + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.strictEqual($input.attr(attribute), value, `${attribute} attribute should remain ${value} after typing`); - keyboardMock($input) - .type('a'); + keyboardMock($input) + .caret(1) + .press('backspace'); - $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.strictEqual($input.attr(attribute), value, `${attribute} attribute should remain ${value} after deleting`); + }); + }); + + QUnit.module('aria-invalid', {}, () => { + [ + { valueRequired: true, emptyValue: 'true', nonEmptyValue: undefined }, + { valueRequired: false, emptyValue: undefined, nonEmptyValue: undefined } + ].forEach(({ valueRequired, emptyValue, nonEmptyValue }) => { + QUnit.test(`component with fieldTemplate should have proper aria-invalid attribute when validator is used and value is ${!valueRequired ? 'not' : ''} required (T1230706)`, function(assert) { + const clock = sinon.useFakeTimers(); + + const $dropDownEditor = $('#dropDownEditorSecond').dxDropDownEditor({ + dataSource: ['one', 'two', 'three'], + searchEnabled: true, + fieldTemplate: 'field', + templatesRenderAsynchronously: true, + integrationOptions: { + templates: { + field: { + render: function({ model, container, onRendered }) { + const $input = $('
').appendTo(container); + + setTimeout(() => { + $input.dxTextBox({ value: model }); + onRendered(); + }, 0); + } + } + } + }, + valueChangeEvent: 'keyup', + }).dxValidator({ + validationRules: valueRequired ? [{ type: 'required', message: 'required' }] : [], + }); - assert.strictEqual($input.attr('aria-required'), 'true', 'aria-required attribute should remain true after typing'); + clock.tick(500); - assert.strictEqual($input.attr('aria-haspopup'), 'true', 'aria-haspopup attribute should retain to true after typing'); + let $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); - assert.strictEqual($input.attr('aria-autocomplete'), 'none', 'aria-autocomplete attribute should retain to none after typing'); + assert.strictEqual($input.attr('aria-invalid'), nonEmptyValue, `initial render should set aria-invalid to ${nonEmptyValue}`); - keyboardMock($input) - .caret(1) - .press('backspace'); + keyboardMock($input) + .type('a'); - $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + clock.tick(500); + + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.equal($input.val(), 'a', 'input value is not empty'); + assert.strictEqual($input.attr('aria-invalid'), nonEmptyValue, `input should set 'aria-invalid' to ${nonEmptyValue} after typing`); + + keyboardMock($input) + .caret(1) + .press('backspace'); + + clock.tick(500); + + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.equal($input.val(), '', 'input value is empty'); + assert.strictEqual($input.attr('aria-invalid'), emptyValue, `input should set 'aria-invalid' to ${emptyValue} after deleting`); + + clock.restore(); + }); + }); + + QUnit.test('component with fieldTemplate should not have aria-invalid attribute when validator is not used (T1230706)', function(assert) { + const clock = sinon.useFakeTimers(); + + const $dropDownEditor = $('#dropDownEditorSecond').dxDropDownEditor({ + dataSource: ['one', 'two', 'three'], + searchEnabled: true, + fieldTemplate: 'field', + templatesRenderAsynchronously: true, + integrationOptions: { + templates: { + field: { + render: function({ model, container, onRendered }) { + const $input = $('
').appendTo(container); + + setTimeout(() => { + $input.dxTextBox({ value: model }); + onRendered(); + }, 0); + } + } + } + }, + valueChangeEvent: 'keyup', + }); - assert.strictEqual($input.attr('aria-required'), 'true', 'aria-required attribute should remain true after deleting'); + clock.tick(500); - assert.strictEqual($input.attr('aria-haspopup'), 'true', 'aria-haspopup attribute should retain to true after deleting'); + let $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + + assert.strictEqual($input.attr('aria-invalid'), undefined, 'initial render should set aria-invalid to undefined'); + + keyboardMock($input) + .type('a'); - assert.strictEqual($input.attr('aria-autocomplete'), 'none', 'aria-autocomplete attribute should retain to none after deleting'); + clock.tick(500); + + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.equal($input.val(), 'a', 'input value is not empty'); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'input should set \'aria-invalid\' to undefined after typing'); + + keyboardMock($input) + .caret(1) + .press('backspace'); + + clock.tick(500); + + $input = $dropDownEditor.find(`.${TEXT_EDITOR_INPUT_CLASS}`); + assert.equal($input.val(), '', 'input value is empty'); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'input should set \'aria-invalid\' to undefined after deleting'); + + clock.restore(); + }); }); QUnit.test('component with fieldTemplate should have proper role attribute after interaction (T1230635)', function(assert) { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/selectBox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/selectBox.tests.js index 8ea1639ef950..301160d70bcc 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/selectBox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/selectBox.tests.js @@ -3294,43 +3294,146 @@ QUnit.module('search', moduleSetup, () => { assert.equal($selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)).val(), 'Name 2', 'selectBox displays right value'); }); - QUnit.test('component with fieldTemplate should retain aria attributes after search and selection (T1230696, T1230971)', function(assert) { - const $selectBox = $('#selectBox').dxSelectBox({ - dataSource: ['a', 'ab', 'abc'], - fieldTemplate: () => { - return $('
').dxTextBox({}); - }, - searchEnabled: true, - searchTimeout: 0, - itemTemplate: () => { - return '
'; - } - }).dxValidator({ - validationRules: [ { type: 'required' } ] + [ + { attribute: 'aria-required', value: 'true' }, + { attribute: 'aria-haspopup', value: 'listbox' }, + { attribute: 'aria-autocomplete', value: 'list' }, + ].forEach(({ attribute, value }) => { + QUnit.test(`component with fieldTemplate should have correct ${attribute} attribute after search and selection (T1230696, T1230971)`, function(assert) { + const $selectBox = $('#selectBox').dxSelectBox({ + dataSource: ['a', 'ab', 'abc'], + fieldTemplate: () => { + return $('
').dxTextBox({}); + }, + searchEnabled: true, + searchTimeout: 0, + itemTemplate: () => { + return '
'; + } + }).dxValidator({ + validationRules: [ { type: 'required' } ] + }); + const selectBox = $selectBox.dxSelectBox('instance'); + let $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + + assert.strictEqual($input.attr(attribute), value, `initial render should have ${attribute} attribute set to ${value}`); + + keyboardMock($input) + .type('a'); + + const listItem = $(selectBox.content()).find(toSelector(LIST_ITEM_CLASS)).eq(1); + listItem.trigger('dxclick'); + + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr(attribute), value, `${attribute} should stay ${value} after search and selection`); + }); + }); + + QUnit.module('aria-invalid', {}, () => { + [ + { valueRequired: true, emptyValue: 'true', nonEmptyValue: undefined }, + { valueRequired: false, emptyValue: undefined, nonEmptyValue: undefined } + ].forEach(({ valueRequired, emptyValue, nonEmptyValue }) => { + QUnit.test(`component with fieldTemplate should have proper aria-invalid attribute when validator is used and value is ${!valueRequired ? 'not' : ''} required (T1230706)`, function(assert) { + const $selectBox = $('#selectBox').dxSelectBox({ + items: [1, 2, 3], + searchEnabled: true, + fieldTemplate: 'field', + showClearButton: true, + templatesRenderAsynchronously: true, + integrationOptions: { + templates: { + field: { + render: function({ model, container, onRendered }) { + const $input = $('
').appendTo(container); + + setTimeout(() => { + $input.dxTextBox({ value: model }); + onRendered(); + }, 0); + } + } + } + }, + }).dxValidator({ + validationRules: valueRequired ? [{ type: 'required', message: 'required' }] : [], + }); + + this.clock.tick(TIME_TO_WAIT); + + const selectBox = $selectBox.dxSelectBox('instance'); + let $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + + assert.strictEqual($input.attr('aria-invalid'), nonEmptyValue, `initial render should set aria-invalid to ${nonEmptyValue}`); + + const listItem = $(selectBox.content()).find(toSelector(LIST_ITEM_CLASS)).eq(0); + listItem.trigger('dxclick'); + + this.clock.tick(TIME_TO_WAIT); + + assert.equal($input.val(), '1', 'input value is not empty'); + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr('aria-invalid'), nonEmptyValue, `non empty input value set aria-invalid to ${nonEmptyValue}`); + + const $clearButton = $(toSelector(CLEAR_BUTTON_AREA)); + $($clearButton).trigger('dxclick'); + + this.clock.tick(TIME_TO_WAIT); + + assert.equal($input.val(), '', 'input value is empty'); + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr('aria-invalid'), emptyValue, `empty input value set aria-invalid to ${emptyValue}`); + }); }); - let $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); - assert.strictEqual($input.attr('aria-required'), 'true', 'initial render should have aria-required attribute set to true'); + QUnit.test('component with fieldTemplate should not have aria-invalid attribute when validator is not used (T1230706)', function(assert) { + const $selectBox = $('#selectBox').dxSelectBox({ + items: [1, 2, 3], + searchEnabled: true, + fieldTemplate: 'field', + showClearButton: true, + templatesRenderAsynchronously: true, + integrationOptions: { + templates: { + field: { + render: function({ model, container, onRendered }) { + const $input = $('
').appendTo(container); + + setTimeout(() => { + $input.dxTextBox({ value: model }); + onRendered(); + }, 0); + } + } + } + }, + }); - assert.strictEqual($input.attr('aria-haspopup'), 'listbox', 'initial render should have aria-haspopup attribute set to listbox'); + this.clock.tick(TIME_TO_WAIT); - assert.strictEqual($input.attr('aria-autocomplete'), 'list', 'initial render should have aria-autocomplete attribute set to list'); + const selectBox = $selectBox.dxSelectBox('instance'); + let $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); - const selectBox = $selectBox.dxSelectBox('instance'); - const keyboard = keyboardMock($input); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'initial render should set aria-invalid to undefined'); - keyboard.type('a'); + const listItem = $(selectBox.content()).find(toSelector(LIST_ITEM_CLASS)).eq(0); + listItem.trigger('dxclick'); - const listItem = $(selectBox.content()).find(toSelector(LIST_ITEM_CLASS)).eq(1); - listItem.trigger('dxclick'); + this.clock.tick(TIME_TO_WAIT); - $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.equal($input.val(), '1', 'input value is not empty'); + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'initial render should set aria-invalid to undefined'); - assert.strictEqual($input.attr('aria-required'), 'true', 'aria-required should stay true after search and selection'); + const $clearButton = $(toSelector(CLEAR_BUTTON_AREA)); + $($clearButton).trigger('dxclick'); - assert.strictEqual($input.attr('aria-haspopup'), 'listbox', 'aria-haspopup should stay to listbox after search and selection'); + this.clock.tick(TIME_TO_WAIT); - assert.strictEqual($input.attr('aria-autocomplete'), 'list', 'aria-autocomplete should stay to list after search and selection'); + assert.equal($input.val(), '', 'input value is empty'); + $input = $selectBox.find(toSelector(TEXTEDITOR_INPUT_CLASS)); + assert.strictEqual($input.attr('aria-invalid'), undefined, 'initial render should set aria-invalid to undefined'); + }); }); QUnit.test('component with fieldTemplate should have proper role attribute after search and selection (T1230635)', function(assert) {