diff --git a/.github/workflows/testcafe_tests.yml b/.github/workflows/testcafe_tests.yml index 9990a5113e64..71ec7a6607a9 100644 --- a/.github/workflows/testcafe_tests.yml +++ b/.github/workflows/testcafe_tests.yml @@ -90,6 +90,8 @@ jobs: { componentFolder: "scheduler", name: "scheduler (3/5)", indices: "3/5" }, { componentFolder: "scheduler", name: "scheduler (4/5)", indices: "4/5" }, { componentFolder: "scheduler", name: "scheduler (5/5)", indices: "5/5" }, + { componentFolder: "scheduler/timezones", name: "scheduler (Europe/Berlin)", timezone: "Europe/Berlin" }, + { componentFolder: "scheduler/timezones", name: "scheduler (America/Los_Angeles)", timezone: "America/Los_Angeles" }, { componentFolder: "form", name: "form (1/2)", indices: "1/2" }, { componentFolder: "form", name: "form (2/2)", indices: "2/2" }, { componentFolder: "form", name: "form - material (1/2)", theme: 'material.blue.light', indices: "1/2" }, @@ -119,6 +121,11 @@ jobs: timeout-minutes: 90 steps: + - name: Set machine timezone + run: | + sudo ln -sf "/usr/share/zoneinfo/$([ "${{ matrix.ARGS.timezone }}" != "" ] && echo "${{ matrix.ARGS.timezone }}" || echo "GMT")" /etc/localtime + date + - name: Get sources uses: actions/checkout@v4 diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts index 83c870e33e30..359ba0d9c9da 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing.ts @@ -1965,7 +1965,7 @@ class EditingControllerImpl extends modules.ViewController { } } - _needUpdateRow(column) { + _needUpdateRow(column?) { const visibleColumns = this._columnsController.getVisibleColumns(); if (!column) { diff --git a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts index faeae2f5f79c..a7c78a5f3a31 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/editing/m_editing_form_based.ts @@ -181,7 +181,6 @@ const editingControllerExtender = (Base: ModuleType) => class .appendTo(this.component.$element()) .addClass(editPopupClass); - // @ts-expect-error this._editPopup = this._createComponent($popupContainer, Popup); this._editPopup.on('hiding', this._getEditPopupHiddenHandler()); this._editPopup.on('shown', (e) => { @@ -214,7 +213,6 @@ const editingControllerExtender = (Base: ModuleType) => class return (container) => { const formTemplate = this.getEditFormTemplate(); - // @ts-expect-error const scrollable = this._createComponent($('
').appendTo(container), Scrollable); this._$popupContent = $((scrollable as any).content()); diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts index 61d10c963c76..3fe46d7d4c0f 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts @@ -119,7 +119,7 @@ export class HeaderPanel extends ColumnsView { $headerPanel.addClass(this.addWidgetPrefix(HEADER_PANEL_CLASS)); const label = messageLocalization.format(this.component.NAME + TOOLBAR_ARIA_LABEL); const $toolbar = $('
').attr('aria-label', label).appendTo($headerPanel); - this._toolbar = this._createComponent($toolbar, Toolbar, this._toolbarOptions!); + this._toolbar = this._createComponent($toolbar, Toolbar, this._toolbarOptions); } else { this._toolbar.option(this._toolbarOptions!); } diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts index 4a381a6e630c..9b0a2b5ffe37 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_types.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_types.ts @@ -62,7 +62,7 @@ export interface InternalGrid extends GridBaseType { _createComponent: >( $container: dxElementWrapper, component: new (...args) => TComponent, - options: TComponent extends Component ? TOptions : never + options?: TComponent extends Component ? TOptions : never ) => TComponent; } diff --git a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts index b717ee67ca6d..18f520c7c0b7 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/validating/m_validating.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable max-classes-per-file */ import $ from '@js/core/renderer'; import browser from '@js/core/utils/browser'; // @ts-expect-error @@ -26,7 +28,9 @@ import { focused } from '@js/ui/widget/selectors'; import errors from '@js/ui/widget/ui.errors'; import { EDITORS_INPUT_SELECTOR } from '../editing/const'; +import type { EditingController } from '../editing/m_editing'; import modules from '../m_modules'; +import type { ModuleType } from '../m_types'; import gridCoreUtils from '../m_utils'; const INVALIDATE_CLASS = 'invalid'; @@ -73,546 +77,557 @@ const cellValueShouldBeValidated = function (value, rowOptions) { return value !== undefined || (value === undefined && rowOptions && !rowOptions.isNewRow); }; -const ValidatingController = modules.Controller.inherit((function () { - return { - init() { - this._editingController = this.getController('editing'); - this.createAction('onRowValidating'); +class ValidatingController extends modules.Controller { + _isValidationInProgress = false; - if (!this._validationState) { - this.initValidationState(); - } - }, - - initValidationState() { - this._validationState = []; - this._validationStateCache = {}; - }, - - _rowIsValidated(change) { - const validationData = this._getValidationData(change?.key); - - return !!validationData && !!validationData.validated; - }, + _disableApplyValidationResults = false; - _getValidationData(key, create) { - const keyHash = getKeyHash(key); - const isObjectKeyHash = isObject(keyHash); - let validationData; + _editingController: any; - if (isObjectKeyHash) { - // eslint-disable-next-line prefer-destructuring - validationData = this._validationState.filter((data) => equalByValue(data.key, key))[0]; - } else { - validationData = this._validationStateCache[keyHash]; - } + _validationState: any; - if (!validationData && create) { - validationData = { key, isValid: true }; - this._validationState.push(validationData); - if (!isObjectKeyHash) { - this._validationStateCache[keyHash] = validationData; - } - } + _validationStateCache: any; - return validationData; - }, + _currentCellValidator: any; - _getBrokenRules(validationData, validationResults) { - let brokenRules; + init() { + this._editingController = this.getController('editing'); + this.createAction('onRowValidating'); - if (validationResults) { - brokenRules = validationResults.brokenRules || validationResults.brokenRule && [validationResults.brokenRule]; - } else { - brokenRules = validationData.brokenRules || []; - } + if (!this._validationState) { + this.initValidationState(); + } + } - return brokenRules; - }, + initValidationState() { + this._validationState = []; + this._validationStateCache = {}; + } - _rowValidating(validationData, validationResults) { - // @ts-expect-error - const deferred = new Deferred(); - const change = this._editingController.getChangeByKey(validationData?.key); - const brokenRules = this._getBrokenRules(validationData, validationResults); - const isValid = validationResults ? validationResults.isValid : validationData.isValid; - const parameters = { - brokenRules, - isValid, - key: change.key, - newData: change.data, - oldData: this._editingController._getOldData(change.key), - promise: null, - errorText: this.getHiddenValidatorsErrorText(brokenRules), - }; + _rowIsValidated(change) { + const validationData = this._getValidationData(change?.key); - this.executeAction('onRowValidating', parameters); + return !!validationData && !!validationData.validated; + } - when(fromPromise(parameters.promise)).always(() => { - validationData.isValid = parameters.isValid; - validationData.errorText = parameters.errorText; - deferred.resolve(parameters); - }); + _getValidationData(key, create?) { + const keyHash = getKeyHash(key); + const isObjectKeyHash = isObject(keyHash); + let validationData; - return deferred.promise(); - }, + if (isObjectKeyHash) { + // eslint-disable-next-line prefer-destructuring + validationData = this._validationState.filter((data) => equalByValue(data.key, key))[0]; + } else { + validationData = this._validationStateCache[keyHash]; + } - getHiddenValidatorsErrorText(brokenRules) { - const brokenRulesMessages: any[] = []; + if (!validationData && create) { + validationData = { key, isValid: true }; + this._validationState.push(validationData); + if (!isObjectKeyHash) { + this._validationStateCache[keyHash] = validationData; + } + } + + return validationData; + } + + _getBrokenRules(validationData, validationResults) { + let brokenRules; + + if (validationResults) { + brokenRules = validationResults.brokenRules || validationResults.brokenRule && [validationResults.brokenRule]; + } else { + brokenRules = validationData.brokenRules || []; + } + + return brokenRules; + } + + _rowValidating(validationData, validationResults) { + // @ts-expect-error + const deferred = new Deferred(); + const change = this._editingController.getChangeByKey(validationData?.key); + const brokenRules = this._getBrokenRules(validationData, validationResults); + const isValid = validationResults ? validationResults.isValid : validationData.isValid; + const parameters = { + brokenRules, + isValid, + key: change.key, + newData: change.data, + oldData: this._editingController._getOldData(change.key), + promise: null, + errorText: this.getHiddenValidatorsErrorText(brokenRules), + }; - each(brokenRules, (_, brokenRule) => { - const { column } = brokenRule; - const isGroupExpandColumn = column && column.groupIndex !== undefined && !column.showWhenGrouped; - const isVisibleColumn = column && column.visible; + this.executeAction('onRowValidating', parameters as any); - if (!brokenRule.validator.$element().parent().length && (!isVisibleColumn || isGroupExpandColumn)) { - brokenRulesMessages.push(brokenRule.message); - } - }); - return brokenRulesMessages.join(', '); - }, + when(fromPromise(parameters.promise)).always(() => { + validationData.isValid = parameters.isValid; + validationData.errorText = parameters.errorText; + deferred.resolve(parameters); + }); - validate(isFull) { - let isValid = true; - const editingController = this._editingController; - // @ts-expect-error - const deferred = new Deferred(); - const completeList: any[] = []; + return deferred.promise(); + } - const editMode = editingController.getEditMode(); - isFull = isFull || editMode === EDIT_MODE_ROW; + getHiddenValidatorsErrorText(brokenRules) { + const brokenRulesMessages: any[] = []; - if (this._isValidationInProgress) { - return deferred.resolve(false).promise(); - } + each(brokenRules, (_, brokenRule) => { + const { column } = brokenRule; + const isGroupExpandColumn = column && column.groupIndex !== undefined && !column.showWhenGrouped; + const isVisibleColumn = column && column.visible; - this._isValidationInProgress = true; - if (isFull) { - editingController.addDeferred(deferred); - const changes = editingController.getChanges(); - each(changes, (index, { type, key }) => { - if (type !== 'remove') { - const validationData = this._getValidationData(key, true); - const validationResult = this.validateGroup(validationData); - completeList.push(validationResult); - validationResult.done((validationResult) => { - validationData.validated = true; - isValid = isValid && validationResult.isValid; - }); - } - }); - } else if (this._currentCellValidator) { - const validationResult = this.validateGroup(this._currentCellValidator._findGroup()); - completeList.push(validationResult); - validationResult.done((validationResult) => { - isValid = validationResult.isValid; - }); + if (!brokenRule.validator.$element().parent().length && (!isVisibleColumn || isGroupExpandColumn)) { + brokenRulesMessages.push(brokenRule.message); } - - when(...completeList).done(() => { - this._isValidationInProgress = false; - deferred.resolve(isValid); + }); + return brokenRulesMessages.join(', '); + } + + validate(isFull) { + let isValid = true; + const editingController = this._editingController; + // @ts-expect-error + const deferred = new Deferred(); + const completeList: any[] = []; + + const editMode = editingController.getEditMode(); + isFull = isFull || editMode === EDIT_MODE_ROW; + + if (this._isValidationInProgress) { + return deferred.resolve(false).promise(); + } + + this._isValidationInProgress = true; + if (isFull) { + editingController.addDeferred(deferred); + const changes = editingController.getChanges(); + each(changes, (index, { type, key }) => { + if (type !== 'remove') { + const validationData = this._getValidationData(key, true); + const validationResult = this.validateGroup(validationData); + completeList.push(validationResult); + validationResult.done((validationResult) => { + validationData.validated = true; + isValid = isValid && validationResult.isValid; + }); + } + }); + } else if (this._currentCellValidator) { + const validationResult = this.validateGroup(this._currentCellValidator._findGroup()); + completeList.push(validationResult); + validationResult.done((validationResult) => { + isValid = validationResult.isValid; }); + } - return deferred.promise(); - }, + when(...completeList).done(() => { + this._isValidationInProgress = false; + deferred.resolve(isValid); + }); - validateGroup(validationData) { - // @ts-expect-error - const result = new Deferred(); - const validateGroup = validationData && ValidationEngine.getGroupConfig(validationData); - let validationResult; + return deferred.promise(); + } - if (validateGroup?.validators.length) { - this.resetRowValidationResults(validationData); - validationResult = ValidationEngine.validateGroup(validationData); - } + validateGroup(validationData) { + // @ts-expect-error + const result = new Deferred(); + const validateGroup = validationData && ValidationEngine.getGroupConfig(validationData); + let validationResult; - when(validationResult?.complete || validationResult).done((validationResult) => { - when(this._rowValidating(validationData, validationResult)).done(result.resolve); - }); + if (validateGroup?.validators.length) { + this.resetRowValidationResults(validationData); + validationResult = ValidationEngine.validateGroup(validationData); + } - return result.promise(); - }, + when(validationResult?.complete || validationResult).done((validationResult) => { + when(this._rowValidating(validationData, validationResult)).done(result.resolve); + }); - isRowDataModified(change) { - return !isEmptyObject(change.data); - }, + return result.promise(); + } - updateValidationState(change) { - const editMode = this._editingController.getEditMode(); - const { key } = change; - const validationData = this._getValidationData(key, true); + isRowDataModified(change) { + return !isEmptyObject(change.data); + } - if (!FORM_BASED_MODES.includes(editMode)) { - if (change.type === EDIT_DATA_INSERT_TYPE && !this.isRowDataModified(change)) { - validationData.isValid = true; - return; - } + updateValidationState(change) { + const editMode = this._editingController.getEditMode(); + const { key } = change; + const validationData = this._getValidationData(key, true); - this.setDisableApplyValidationResults(true); - const groupConfig = ValidationEngine.getGroupConfig(validationData); - if (groupConfig) { - const validationResult = ValidationEngine.validateGroup(validationData); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - when(validationResult.complete || validationResult).done((validationResult) => { - // @ts-expect-error - validationData.isValid = validationResult.isValid; - // @ts-expect-error - validationData.brokenRules = validationResult.brokenRules; - }); - } else if (!validationData.brokenRules || !validationData.brokenRules.length) { - validationData.isValid = true; - } - this.setDisableApplyValidationResults(false); - } else { + if (!FORM_BASED_MODES.includes(editMode)) { + if (change.type === EDIT_DATA_INSERT_TYPE && !this.isRowDataModified(change)) { validationData.isValid = true; + return; } - }, - setValidator(validator) { - this._currentCellValidator = validator; - }, - - renderCellPendingIndicator($container) { - let $indicator = $container.find(`.${PENDING_INDICATOR_CLASS}`); - if (!$indicator.length) { - const $indicatorContainer = $container; - $indicator = $('
').appendTo($indicatorContainer) - .addClass(PENDING_INDICATOR_CLASS); - this._createComponent($indicator, LoadIndicator); - $container.addClass(VALIDATION_PENDING_CLASS); + this.setDisableApplyValidationResults(true); + const groupConfig = ValidationEngine.getGroupConfig(validationData); + if (groupConfig) { + const validationResult = ValidationEngine.validateGroup(validationData); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + when(validationResult.complete || validationResult).done((validationResult) => { + // @ts-expect-error + validationData.isValid = validationResult.isValid; + // @ts-expect-error + validationData.brokenRules = validationResult.brokenRules; + }); + } else if (!validationData.brokenRules || !validationData.brokenRules.length) { + validationData.isValid = true; } - }, - - disposeCellPendingIndicator($container) { - const $indicator = $container.find(`.${PENDING_INDICATOR_CLASS}`); - if ($indicator.length) { - const indicator = LoadIndicator.getInstance($indicator); - if (indicator) { - indicator.dispose(); - indicator.$element().remove(); - } - $container.removeClass(VALIDATION_PENDING_CLASS); + this.setDisableApplyValidationResults(false); + } else { + validationData.isValid = true; + } + } + + setValidator(validator) { + this._currentCellValidator = validator; + } + + renderCellPendingIndicator($container) { + let $indicator = $container.find(`.${PENDING_INDICATOR_CLASS}`); + if (!$indicator.length) { + const $indicatorContainer = $container; + $indicator = $('
').appendTo($indicatorContainer) + .addClass(PENDING_INDICATOR_CLASS); + this._createComponent($indicator, LoadIndicator); + $container.addClass(VALIDATION_PENDING_CLASS); + } + } + + disposeCellPendingIndicator($container) { + const $indicator = $container.find(`.${PENDING_INDICATOR_CLASS}`); + if ($indicator.length) { + const indicator = LoadIndicator.getInstance($indicator); + if (indicator) { + indicator.dispose(); + indicator.$element().remove(); } - }, - - validationStatusChanged(result) { - const { validator } = result; - const validationGroup = validator.option('validationGroup'); - const { column } = validator.option('dataGetter')(); - - this.updateCellValidationResult({ - rowKey: validationGroup.key, + $container.removeClass(VALIDATION_PENDING_CLASS); + } + } + + validationStatusChanged(result) { + const { validator } = result; + const validationGroup = validator.option('validationGroup'); + const { column } = validator.option('dataGetter')(); + + this.updateCellValidationResult({ + rowKey: validationGroup.key, + columnIndex: column.index, + validationResult: result, + }); + } + + validatorInitialized(arg) { + arg.component.on('validating', this.validationStatusChanged.bind(this)); + arg.component.on('validated', this.validationStatusChanged.bind(this)); + } + + validatorDisposing(arg) { + const validator = arg.component; + const validationGroup = validator.option('validationGroup'); + const { column } = validator.option('dataGetter')(); + + const result = this.getCellValidationResult({ + rowKey: validationGroup?.key, + columnIndex: column.index, + }); + if (validationResultIsValid(result) && result.status === VALIDATION_STATUS.pending) { + this.cancelCellValidationResult({ + change: validationGroup, columnIndex: column.index, - validationResult: result, }); - }, - - validatorInitialized(arg) { - arg.component.on('validating', this.validationStatusChanged.bind(this)); - arg.component.on('validated', this.validationStatusChanged.bind(this)); - }, - - validatorDisposing(arg) { - const validator = arg.component; - const validationGroup = validator.option('validationGroup'); - const { column } = validator.option('dataGetter')(); - - const result = this.getCellValidationResult({ - rowKey: validationGroup?.key, + } + } + + applyValidationResult($container, result) { + const { validator } = result; + const validationGroup = validator.option('validationGroup'); + const { column } = validator.option('dataGetter')(); + + result.brokenRules && result.brokenRules.forEach((rule) => { + rule.columnIndex = column.index; + rule.column = column; + }); + if ($container) { + const validationResult = this.getCellValidationResult({ + rowKey: validationGroup.key, columnIndex: column.index, }); - if (validationResultIsValid(result) && result.status === VALIDATION_STATUS.pending) { - this.cancelCellValidationResult({ - change: validationGroup, - columnIndex: column.index, - }); + const requestIsDisabled = validationResultIsValid(validationResult) && validationResult.disabledPendingId === result.id; + if (this._disableApplyValidationResults || requestIsDisabled) { + return; } - }, - - applyValidationResult($container, result) { - const { validator } = result; - const validationGroup = validator.option('validationGroup'); - const { column } = validator.option('dataGetter')(); - - result.brokenRules && result.brokenRules.forEach((rule) => { - rule.columnIndex = column.index; - rule.column = column; - }); - if ($container) { - const validationResult = this.getCellValidationResult({ - rowKey: validationGroup.key, - columnIndex: column.index, - }); - const requestIsDisabled = validationResultIsValid(validationResult) && validationResult.disabledPendingId === result.id; - if (this._disableApplyValidationResults || requestIsDisabled) { - return; - } - if (result.status === VALIDATION_STATUS.invalid) { - const $focus = $container.find(':focus'); - if (!focused($focus)) { - // @ts-expect-error - eventsEngine.trigger($focus, 'focus'); - // @ts-expect-error - eventsEngine.trigger($focus, pointerEvents.down); - } + if (result.status === VALIDATION_STATUS.invalid) { + const $focus = $container.find(':focus'); + if (!focused($focus)) { + // @ts-expect-error + eventsEngine.trigger($focus, 'focus'); + // @ts-expect-error + eventsEngine.trigger($focus, pointerEvents.down); } - const editor = !column.editCellTemplate && this.getController('editorFactory').getEditorInstance($container); - if (result.status === VALIDATION_STATUS.pending) { - if (editor) { - editor.option('validationStatus', VALIDATION_STATUS.pending); - } else { - this.renderCellPendingIndicator($container); - } - } else if (editor) { - editor.option('validationStatus', VALIDATION_STATUS.valid); + } + const editor = !column.editCellTemplate && this.getController('editorFactory').getEditorInstance($container); + if (result.status === VALIDATION_STATUS.pending) { + if (editor) { + editor.option('validationStatus', VALIDATION_STATUS.pending); } else { - this.disposeCellPendingIndicator($container); + this.renderCellPendingIndicator($container); } - $container.toggleClass(this.addWidgetPrefix(INVALIDATE_CLASS), result.status === VALIDATION_STATUS.invalid); + } else if (editor) { + editor.option('validationStatus', VALIDATION_STATUS.valid); + } else { + this.disposeCellPendingIndicator($container); } - }, - - _syncInternalEditingData(parameters) { - const editingController = this._editingController; - const change = editingController.getChangeByKey(parameters.key); - const oldDataFromState = editingController._getOldData(parameters.key); - const oldData = parameters.row?.oldData; - - if (change && oldData && !oldDataFromState) { - editingController._addInternalData({ key: parameters.key, oldData }); + $container.toggleClass(this.addWidgetPrefix(INVALIDATE_CLASS), result.status === VALIDATION_STATUS.invalid); + } + } + + _syncInternalEditingData(parameters) { + const editingController = this._editingController; + const change = editingController.getChangeByKey(parameters.key); + const oldDataFromState = editingController._getOldData(parameters.key); + const oldData = parameters.row?.oldData; + + if (change && oldData && !oldDataFromState) { + editingController._addInternalData({ key: parameters.key, oldData }); + } + } + + createValidator(parameters, $container) { + const editingController = this._editingController; + const { column } = parameters; + let { showEditorAlways } = column; + + if (isDefined(column.command) || !column.validationRules || !Array.isArray(column.validationRules) || !column.validationRules.length) return; + + const editIndex = editingController.getIndexByKey(parameters.key, editingController.getChanges()); + let needCreateValidator = editIndex > -1; + + if (!needCreateValidator) { + if (!showEditorAlways) { + const columnsController = this.getController('columns'); + const visibleColumns = columnsController?.getVisibleColumns() || []; + showEditorAlways = visibleColumns.some((column) => column.showEditorAlways); } - }, - - createValidator(parameters, $container) { - const editingController = this._editingController; - const { column } = parameters; - let { showEditorAlways } = column; - if (isDefined(column.command) || !column.validationRules || !Array.isArray(column.validationRules) || !column.validationRules.length) return; + const isEditRow = equalByValue(this.option('editing.editRowKey'), parameters.key); + const isCellOrBatchEditingAllowed = editingController.isCellOrBatchEditMode() && editingController.allowUpdating({ row: parameters.row }); - const editIndex = editingController.getIndexByKey(parameters.key, editingController.getChanges()); - let needCreateValidator = editIndex > -1; + needCreateValidator = isEditRow || isCellOrBatchEditingAllowed && showEditorAlways; - if (!needCreateValidator) { - if (!showEditorAlways) { - const columnsController = this.getController('columns'); - const visibleColumns = columnsController?.getVisibleColumns() || []; - showEditorAlways = visibleColumns.some((column) => column.showEditorAlways); - } - - const isEditRow = equalByValue(this.option('editing.editRowKey'), parameters.key); - const isCellOrBatchEditingAllowed = editingController.isCellOrBatchEditMode() && editingController.allowUpdating({ row: parameters.row }); - - needCreateValidator = isEditRow || isCellOrBatchEditingAllowed && showEditorAlways; - - if (isCellOrBatchEditingAllowed && showEditorAlways) { - editingController._addInternalData({ key: parameters.key, oldData: parameters.row?.oldData ?? parameters.data }); - } + if (isCellOrBatchEditingAllowed && showEditorAlways) { + editingController._addInternalData({ key: parameters.key, oldData: parameters.row?.oldData ?? parameters.data }); } + } - if (needCreateValidator) { - if ($container && !$container.length) { - errors.log('E1050'); - return; - } + if (needCreateValidator) { + if ($container && !$container.length) { + errors.log('E1050'); + return; + } - this._syncInternalEditingData(parameters); - const validationData = this._getValidationData(parameters.key, true); + this._syncInternalEditingData(parameters); + const validationData = this._getValidationData(parameters.key, true); - const getValue = () => { - const change = editingController.getChangeByKey(validationData?.key); - const value = column.calculateCellValue(change?.data || {}); - return value !== undefined ? value : parameters.value; - }; + const getValue = () => { + const change = editingController.getChangeByKey(validationData?.key); + const value = column.calculateCellValue(change?.data || {}); + return value !== undefined ? value : parameters.value; + }; - const useDefaultValidator = $container && $container.hasClass('dx-widget'); - $container && $container.addClass(this.addWidgetPrefix(VALIDATOR_CLASS)); - const validator = new Validator($container || $('
'), { - name: column.caption, - validationRules: extend(true, [], column.validationRules), - validationGroup: validationData, - // @ts-expect-error - adapter: useDefaultValidator ? null : { - getValue, - applyValidationResults: (result) => { - this.applyValidationResult($container, result); - }, + const useDefaultValidator = $container && $container.hasClass('dx-widget'); + $container && $container.addClass(this.addWidgetPrefix(VALIDATOR_CLASS)); + const validator = new Validator($container || $('
'), { + name: column.caption, + validationRules: extend(true, [], column.validationRules), + validationGroup: validationData, + // @ts-expect-error + adapter: useDefaultValidator ? null : { + getValue, + applyValidationResults: (result) => { + this.applyValidationResult($container, result); }, - dataGetter() { - const key = validationData?.key; - const change = editingController.getChangeByKey(key); - const oldData = editingController._getOldData(key); - return { - data: createObjectWithChanges(oldData, change?.data), - column, - }; - }, - onInitialized: this.validatorInitialized.bind(this), - onDisposing: this.validatorDisposing.bind(this), - }); - if (useDefaultValidator) { - const adapter = validator.option('adapter'); - if (adapter) { - const originBypass = adapter.bypass; - const defaultAdapterBypass = () => parameters.row.isNewRow && !this._isValidationInProgress && !editingController.isCellModified(parameters); - - adapter.getValue = getValue; - adapter.validationRequestsCallbacks = []; - // @ts-expect-error - adapter.bypass = () => originBypass.call(adapter) || defaultAdapterBypass(); - } + }, + dataGetter() { + const key = validationData?.key; + const change = editingController.getChangeByKey(key); + const oldData = editingController._getOldData(key); + return { + data: createObjectWithChanges(oldData, change?.data), + column, + }; + }, + onInitialized: this.validatorInitialized.bind(this), + onDisposing: this.validatorDisposing.bind(this), + }); + if (useDefaultValidator) { + const adapter = validator.option('adapter'); + if (adapter) { + const originBypass = adapter.bypass; + const defaultAdapterBypass = () => parameters.row.isNewRow && !this._isValidationInProgress && !editingController.isCellModified(parameters); + + adapter.getValue = getValue; + adapter.validationRequestsCallbacks = []; + // @ts-expect-error + adapter.bypass = () => originBypass.call(adapter) || defaultAdapterBypass(); } - - return validator; } - return undefined; - }, + return validator; + } - setDisableApplyValidationResults(flag) { - this._disableApplyValidationResults = flag; - }, + return undefined; + } - getDisableApplyValidationResults() { - return this._disableApplyValidationResults; - }, + setDisableApplyValidationResults(flag) { + this._disableApplyValidationResults = flag; + } + + getDisableApplyValidationResults() { + return this._disableApplyValidationResults; + } - isCurrentValidatorProcessing({ rowKey, columnIndex }) { - return this._currentCellValidator && equalByValue(this._currentCellValidator.option('validationGroup').key, rowKey) + isCurrentValidatorProcessing({ rowKey, columnIndex }) { + return this._currentCellValidator && equalByValue(this._currentCellValidator.option('validationGroup').key, rowKey) && this._currentCellValidator.option('dataGetter')().column.index === columnIndex; - }, + } - validateCell(validator) { - const cellParams = { - rowKey: validator.option('validationGroup').key, - columnIndex: validator.option('dataGetter')().column.index, - }; - let validationResult = this.getCellValidationResult(cellParams); - const stateRestored = validationResultIsValid(validationResult); - const adapter = validator.option('adapter'); - if (!stateRestored) { + validateCell(validator) { + const cellParams = { + rowKey: validator.option('validationGroup').key, + columnIndex: validator.option('dataGetter')().column.index, + validationResult: null, + }; + let validationResult = this.getCellValidationResult(cellParams); + const stateRestored = validationResultIsValid(validationResult); + const adapter = validator.option('adapter'); + if (!stateRestored) { + validationResult = validator.validate(); + } else { + const currentCellValue = adapter.getValue(); + if (!equalByValue(currentCellValue, validationResult.value)) { validationResult = validator.validate(); - } else { - const currentCellValue = adapter.getValue(); - if (!equalByValue(currentCellValue, validationResult.value)) { - validationResult = validator.validate(); - } } - // @ts-expect-error - const deferred = new Deferred(); - if (stateRestored && validationResult.status === VALIDATION_STATUS.pending) { - this.updateCellValidationResult(cellParams); - adapter.applyValidationResults(validationResult); - } - when(validationResult.complete || validationResult).done((validationResult) => { - stateRestored && adapter.applyValidationResults(validationResult); - deferred.resolve(validationResult); - }); - return deferred.promise(); - }, - - updateCellValidationResult({ rowKey, columnIndex, validationResult }) { - const validationData = this._getValidationData(rowKey); - if (!validationData) { - return; - } - if (!validationData.validationResults) { - validationData.validationResults = {}; - } - let result; - if (validationResult) { - result = extend({}, validationResult); - validationData.validationResults[columnIndex] = result; - if (validationResult.status === VALIDATION_STATUS.pending) { - if (this._editingController.getEditMode() === EDIT_MODE_CELL) { - // @ts-expect-error - result.deferred = new Deferred(); - result.complete.always(() => { - result.deferred.resolve(); - }); - this._editingController.addDeferred(result.deferred); - } - if (this._disableApplyValidationResults) { - result.disabledPendingId = validationResult.id; - return; - } + } + // @ts-expect-error + const deferred = new Deferred(); + if (stateRestored && validationResult.status === VALIDATION_STATUS.pending) { + this.updateCellValidationResult(cellParams); + adapter.applyValidationResults(validationResult); + } + when(validationResult.complete || validationResult).done((validationResult) => { + stateRestored && adapter.applyValidationResults(validationResult); + deferred.resolve(validationResult); + }); + return deferred.promise(); + } + + updateCellValidationResult({ rowKey, columnIndex, validationResult }) { + const validationData = this._getValidationData(rowKey); + if (!validationData) { + return; + } + if (!validationData.validationResults) { + validationData.validationResults = {}; + } + let result; + if (validationResult) { + result = extend({}, validationResult); + validationData.validationResults[columnIndex] = result; + if (validationResult.status === VALIDATION_STATUS.pending) { + if (this._editingController.getEditMode() === EDIT_MODE_CELL) { + // @ts-expect-error + result.deferred = new Deferred(); + result.complete.always(() => { + result.deferred.resolve(); + }); + this._editingController.addDeferred(result.deferred); } - } else { - result = validationData.validationResults[columnIndex]; - } - if (result && result.disabledPendingId) { - delete result.disabledPendingId; - } - }, - - getCellValidationResult({ rowKey, columnIndex }) { - const validationData = this._getValidationData(rowKey, true); - return validationData?.validationResults?.[columnIndex]; - }, - - removeCellValidationResult({ change, columnIndex }) { - const validationData = this._getValidationData(change?.key); - - if (validationData && validationData.validationResults) { - this.cancelCellValidationResult({ change, columnIndex }); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete validationData.validationResults[columnIndex]; - } - }, - - cancelCellValidationResult({ change, columnIndex }) { - const validationData = this._getValidationData(change.key); - - if (change && validationData.validationResults) { - const result = validationData.validationResults[columnIndex]; - if (result) { - result.deferred && result.deferred.reject(VALIDATION_CANCELLED); - validationData.validationResults[columnIndex] = VALIDATION_CANCELLED; + if (this._disableApplyValidationResults) { + result.disabledPendingId = validationResult.id; + return; } } - }, - - resetRowValidationResults(validationData) { - if (validationData) { - validationData.validationResults && delete validationData.validationResults; - delete validationData.validated; + } else { + result = validationData.validationResults[columnIndex]; + } + if (result && result.disabledPendingId) { + delete result.disabledPendingId; + } + } + + getCellValidationResult({ rowKey, columnIndex }) { + const validationData = this._getValidationData(rowKey, true); + return validationData?.validationResults?.[columnIndex]; + } + + removeCellValidationResult({ change, columnIndex }) { + const validationData = this._getValidationData(change?.key); + + if (validationData && validationData.validationResults) { + this.cancelCellValidationResult({ change, columnIndex }); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete validationData.validationResults[columnIndex]; + } + } + + cancelCellValidationResult({ change, columnIndex }) { + const validationData = this._getValidationData(change.key); + + if (change && validationData.validationResults) { + const result = validationData.validationResults[columnIndex]; + if (result) { + result.deferred && result.deferred.reject(VALIDATION_CANCELLED); + validationData.validationResults[columnIndex] = VALIDATION_CANCELLED; } - }, - - isInvalidCell({ rowKey, columnIndex }) { - const result = this.getCellValidationResult({ - rowKey, - columnIndex, - }); - return validationResultIsValid(result) && result.status === VALIDATION_STATUS.invalid; - }, - - getCellValidator({ rowKey, columnIndex }) { - const validationData = this._getValidationData(rowKey); - const groupConfig = validationData && ValidationEngine.getGroupConfig(validationData); - const validators = groupConfig && groupConfig.validators; - return validators && validators.filter((v) => { - const { column } = v.option('dataGetter')(); - return column ? column.index === columnIndex : false; - })[0]; - }, - - setCellValidationStatus(cellOptions) { - const validationResult = this.getCellValidationResult({ - rowKey: cellOptions.key, - columnIndex: cellOptions.column.index, - }); - - if (isDefined(validationResult)) { - cellOptions.validationStatus = validationResult !== VALIDATION_CANCELLED ? validationResult.status : VALIDATION_CANCELLED; - } else { - delete cellOptions.validationStatus; - } - }, - }; -})()); + } + } + + resetRowValidationResults(validationData) { + if (validationData) { + validationData.validationResults && delete validationData.validationResults; + delete validationData.validated; + } + } + + isInvalidCell({ rowKey, columnIndex }) { + const result = this.getCellValidationResult({ + rowKey, + columnIndex, + }); + return validationResultIsValid(result) && result.status === VALIDATION_STATUS.invalid; + } + + getCellValidator({ rowKey, columnIndex }) { + const validationData = this._getValidationData(rowKey); + const groupConfig = validationData && ValidationEngine.getGroupConfig(validationData); + const validators = groupConfig && groupConfig.validators; + return validators && validators.filter((v) => { + const { column } = v.option('dataGetter')(); + return column ? column.index === columnIndex : false; + })[0]; + } + + setCellValidationStatus(cellOptions) { + const validationResult = this.getCellValidationResult({ + rowKey: cellOptions.key, + columnIndex: cellOptions.column.index, + }); + + if (isDefined(validationResult)) { + cellOptions.validationStatus = validationResult !== VALIDATION_CANCELLED ? validationResult.status : VALIDATION_CANCELLED; + } else { + delete cellOptions.validationStatus; + } + } +} export const validatingModule = { defaultOptions() { @@ -629,9 +644,19 @@ export const validatingModule = { }, extenders: { controllers: { - editing: { + editing: (Base: ModuleType) => class ValidateEditingController extends Base { + processDataItemTreeListHack(item) { + // @ts-expect-error + super.processDataItem.apply(this, arguments); + } + + processItemsTreeListHack(items, e) { + // @ts-expect-error + return super.processItems.apply(this, arguments); + } + _addChange(changeParams) { - const change = this.callBase.apply(this, arguments); + const change = super._addChange.apply(this, arguments as any); const validatingController = this.getController('validating'); if (change && changeParams.type !== EDIT_DATA_REMOVE_TYPE) { @@ -639,10 +664,10 @@ export const validatingModule = { } return change; - }, + } _handleChangesChange(args) { - this.callBase.apply(this, arguments); + super._handleChangesChange.apply(this, arguments as any); const validatingController = this.getController('validating'); @@ -651,7 +676,7 @@ export const validatingModule = { validatingController.updateValidationState(change); } }); - }, + } _updateRowAndPageIndices() { const that = this; @@ -670,7 +695,7 @@ export const validatingModule = { rowIndex++; } }); - }, + } _getValidationGroupsInForm(detailOptions) { const validatingController = this.getController('validating'); @@ -679,19 +704,20 @@ export const validatingModule = { return { validationGroup: validationData, }; - }, + } - _validateEditFormAfterUpdate(row, isCustomSetCellValue) { + _validateEditFormAfterUpdate(row?, isCustomSetCellValue?) { // T816256, T844143 if (isCustomSetCellValue && this._editForm) { this._editForm.validate(); } - this.callBase.apply(this, arguments); - }, + super._validateEditFormAfterUpdate.apply(this, arguments as any); + } _prepareEditCell(params) { - const isNotCanceled = this.callBase.apply(this, arguments); + // @ts-expect-error + const isNotCanceled = super._prepareEditCell.apply(this, arguments as any); const validatingController = this.getController('validating'); if (isNotCanceled && params.column.showEditorAlways) { @@ -699,7 +725,7 @@ export const validatingModule = { } return isNotCanceled; - }, + } processItems(items, changeType) { const changes = this.getChanges(); @@ -722,7 +748,7 @@ export const validatingModule = { return index; }; - items = this.callBase(items, changeType); + items = super.processItems(items, changeType); const itemsCount = items.length; const addInValidItem = function (change, validationData) { @@ -751,7 +777,7 @@ export const validatingModule = { } return items; - }, + } processDataItem(item) { const isInserted = item.data[INSERT_INDEX]; @@ -773,8 +799,8 @@ export const validatingModule = { } } - this.callBase.apply(this, arguments); - }, + super.processDataItem.apply(this, arguments as any); + } _createInvisibleColumnValidators(changes) { const that = this; @@ -823,11 +849,11 @@ export const validatingModule = { return function () { invisibleColumnValidators.forEach((validator) => { validator.dispose(); }); }; - }, + } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _beforeSaveEditData(change, editIndex) { - let result = this.callBase.apply(this, arguments); + _beforeSaveEditData(change, editIndex?) { + let result: any = super._beforeSaveEditData.apply(this, arguments as any); const validatingController = this.getController('validating'); const validationData = validatingController._getValidationData(change?.key); @@ -843,7 +869,7 @@ export const validatingModule = { disposeValidators(); this._updateRowAndPageIndices(); - // eslint-disable-next-line default-case + // eslint-disable-next-line default-case, @typescript-eslint/switch-exhaustiveness-check switch (this.getEditMode()) { case EDIT_MODE_CELL: if (!isFullValid) { @@ -863,7 +889,7 @@ export const validatingModule = { }); } return result.promise ? result.promise() : result; - }, + } /** * @param rowIndex Row index @@ -872,7 +898,8 @@ export const validatingModule = { * @returns A deferred object that resolves to a boolean or just a boolean to determine whether to cancel cell editing */ _beforeEditCell(rowIndex: number, columnIndex: number, item: any): DeferredObj | boolean { - const result = this.callBase(rowIndex, columnIndex, item); + // @ts-expect-error + const result = super._beforeEditCell(rowIndex, columnIndex, item); if (this.getEditMode() === EDIT_MODE_CELL) { const $cell = this._rowsView._getCellElement(rowIndex, columnIndex); @@ -892,9 +919,9 @@ export const validatingModule = { } } return false; - }, + } - _afterSaveEditData(cancel) { + _afterSaveEditData(cancel?) { let $firstErrorRow; const isCellEditMode = this.getEditMode() === EDIT_MODE_CELL; @@ -932,7 +959,7 @@ export const validatingModule = { this.getController('validating').initValidationState(); } } - }, + } _handleDataChanged(args) { const validationState = this.getController('validating')._validationState; @@ -947,8 +974,8 @@ export const validatingModule = { }); } - this.callBase(args); - }, + super._handleDataChanged(args); + } resetRowAndPageIndices() { const validationState = this.getController('validating')._validationState; @@ -959,16 +986,17 @@ export const validatingModule = { delete validationData.rowIndex; } }); - }, + } _beforeCancelEditData() { this.getController('validating').initValidationState(); - this.callBase(); - }, + super._beforeCancelEditData(); + } _showErrorRow(change) { let $popupContent; + // @ts-expect-error const errorHandling = this.getController('errorHandling'); const items = this.getController('data').items(); const rowIndex = this.getIndexByKey(change.key, items); @@ -978,7 +1006,7 @@ export const validatingModule = { $popupContent = this.getPopupContent(); return errorHandling && errorHandling.renderErrorRow(validationData?.errorText, rowIndex, $popupContent); } - }, + } updateFieldValue(e) { const validatingController = this.getController('validating'); @@ -989,7 +1017,7 @@ export const validatingModule = { columnIndex: e.column.index, }); - this.callBase.apply(this, arguments).done(() => { + super.updateFieldValue.apply(this, arguments as any).done(() => { const currentValidator = validatingController.getCellValidator({ rowKey: e.key, columnIndex: e.column.index, @@ -1001,10 +1029,10 @@ export const validatingModule = { }); }); return deferred.promise(); - }, + } highlightDataCell($cell, parameters) { - this.callBase.apply(this, arguments); + super.highlightDataCell.apply(this, arguments as any); const validatingController = this.getController('validating'); validatingController.setCellValidationStatus(parameters); @@ -1021,22 +1049,22 @@ export const validatingModule = { }); } } - }, + } getChangeByKey(key) { const changes = this.getChanges(); return changes[gridCoreUtils.getIndexByKey(key, changes)]; - }, + } isCellModified(parameters) { - const cellModified = this.callBase(parameters); + const cellModified = super.isCellModified(parameters); const change = this.getChangeByKey(parameters.key); const isCellInvalid = !!parameters.row && this.getController('validating').isInvalidCell({ rowKey: parameters.key, columnIndex: parameters.column.index, }); return cellModified || (this.getController('validating')._rowIsValidated(change) && isCellInvalid); - }, + } }, editorFactory: (function () { const getWidthOfVisibleCells = function (that, element) { diff --git a/packages/devextreme/js/__internal/grids/tree_list/m_validating.ts b/packages/devextreme/js/__internal/grids/tree_list/m_validating.ts index 29660970f8db..84a7db49d1ec 100644 --- a/packages/devextreme/js/__internal/grids/tree_list/m_validating.ts +++ b/packages/devextreme/js/__internal/grids/tree_list/m_validating.ts @@ -1,18 +1,26 @@ -import { extend } from '@js/core/utils/extend'; +/* eslint-disable @typescript-eslint/no-unused-vars */ import { validatingModule } from '@ts/grids/grid_core/validating/m_validating'; +import type { EditingController } from '../grid_core/editing/m_editing'; +import type { ModuleType } from '../grid_core/m_types'; import treeListCore from './m_core'; -const EditingControllerExtender = extend({}, validatingModule.extenders.controllers.editing); -delete EditingControllerExtender.processItems; -delete EditingControllerExtender.processDataItem; +const editingControllerExtender = (Base: ModuleType) => class TreeListEditingControllerExtender extends validatingModule.extenders.controllers.editing(Base) { + processDataItem(item) { + super.processDataItemTreeListHack.apply(this, arguments as any); + } + + processItems(items, e) { + return super.processItemsTreeListHack.apply(this, arguments as any); + } +}; treeListCore.registerModule('validating', { defaultOptions: validatingModule.defaultOptions, controllers: validatingModule.controllers, extenders: { controllers: { - editing: EditingControllerExtender, + editing: editingControllerExtender, editorFactory: validatingModule.extenders.controllers.editorFactory, }, views: validatingModule.extenders.views, diff --git a/packages/devextreme/js/ui/calendar/ui.calendar.js b/packages/devextreme/js/ui/calendar/ui.calendar.js index bcd4aa16da1c..92493631e5ab 100644 --- a/packages/devextreme/js/ui/calendar/ui.calendar.js +++ b/packages/devextreme/js/ui/calendar/ui.calendar.js @@ -741,6 +741,8 @@ const Calendar = Editor.inherit({ } }, + _setAriaReadonly: noop, + _getKeyboardListeners() { return this.callBase().concat([this._view]); }, diff --git a/packages/devextreme/js/ui/editor/editor.js b/packages/devextreme/js/ui/editor/editor.js index 82847001fe6e..8c5a75f957d8 100644 --- a/packages/devextreme/js/ui/editor/editor.js +++ b/packages/devextreme/js/ui/editor/editor.js @@ -275,6 +275,10 @@ const Editor = Widget.inherit({ this._toggleBackspaceHandler(readOnly); this.$element().toggleClass(READONLY_STATE_CLASS, !!readOnly); + this._setAriaReadonly(readOnly); + }, + + _setAriaReadonly(readOnly) { this.setAria('readonly', readOnly || undefined); }, diff --git a/packages/devextreme/js/ui/switch.js b/packages/devextreme/js/ui/switch.js index 7a24da75440d..d5975cf87198 100644 --- a/packages/devextreme/js/ui/switch.js +++ b/packages/devextreme/js/ui/switch.js @@ -94,7 +94,7 @@ const Switch = Editor.inherit({ this._renderClick(); - this.setAria('role', 'button'); + this.setAria('role', 'switch'); this._renderSwipeable(); @@ -316,17 +316,17 @@ const Switch = Editor.inherit({ }); }, - _renderValue: function() { + _renderValue() { this._validateValue(); - const val = this.option('value'); - this._renderPosition(val, 0); + const value = this.option('value'); + this._renderPosition(value, 0); - this.$element().toggleClass(SWITCH_ON_VALUE_CLASS, val); - this._getSubmitElement().val(val); + this.$element().toggleClass(SWITCH_ON_VALUE_CLASS, value); + this._getSubmitElement().val(value); this.setAria({ - 'pressed': val, - 'label': val ? this.option('switchedOnText') : this.option('switchedOffText') + 'checked': value, + 'label': value ? this.option('switchedOnText') : this.option('switchedOffText') }); }, diff --git a/packages/devextreme/js/ui/tile_view.js b/packages/devextreme/js/ui/tile_view.js index 6ab3b55a1da7..f255a3a6a1f2 100644 --- a/packages/devextreme/js/ui/tile_view.js +++ b/packages/devextreme/js/ui/tile_view.js @@ -7,7 +7,7 @@ import { isDefined } from '../core/utils/type'; import { extend } from '../core/utils/extend'; import { hasWindow } from '../core/utils/window'; import { getPublicElement } from '../core/element'; -import { deferRender } from '../core/utils/common'; +import { noop, deferRender } from '../core/utils/common'; import { nativeScrolling } from '../core/utils/support'; import ScrollView from './scroll_view'; import CollectionWidget from './collection/ui.collection_widget.edit'; @@ -271,6 +271,8 @@ const TileView = CollectionWidget.inherit({ }).bind(this)); }, + _refreshActiveDescendant: noop, + _getItemPosition: function(item) { const config = this._config; const mainPosition = config.mainPosition; diff --git a/packages/devextreme/scss/widgets/base/textEditor/_mixins.scss b/packages/devextreme/scss/widgets/base/textEditor/_mixins.scss index 7adb5dfb0173..a37911b651f9 100644 --- a/packages/devextreme/scss/widgets/base/textEditor/_mixins.scss +++ b/packages/devextreme/scss/widgets/base/textEditor/_mixins.scss @@ -144,6 +144,7 @@ $texteditor-border-color, $texteditor-border-bottom-color, $texteditor-disabled-color, + $texteditor-readonly-color, $texteditor-hover-border-color, $texteditor-hover-border-bottom-color, $texteditor-focused-border-color, @@ -256,7 +257,7 @@ &.dx-state-readonly { @include dx-state-border-style($label-readonly-border-style); - @include dx-state-border-color($texteditor-disabled-color, $texteditor-disabled-color); + @include dx-state-border-color($texteditor-readonly-color, $texteditor-readonly-color); } &.dx-state-disabled { diff --git a/packages/devextreme/scss/widgets/fluent/textEditor/_index.scss b/packages/devextreme/scss/widgets/fluent/textEditor/_index.scss index 847b5c2a8349..0e2b483721dc 100644 --- a/packages/devextreme/scss/widgets/fluent/textEditor/_index.scss +++ b/packages/devextreme/scss/widgets/fluent/textEditor/_index.scss @@ -513,6 +513,7 @@ $fluent-editor-border-width: 1px; $texteditor-border-color, $texteditor-border-bottom-color, $texteditor-border-color-disabled, + $texteditor-border-color-disabled, $texteditor-border-color-hover, $texteditor-border-bottom-color-hover, $texteditor-border-color-focused, diff --git a/packages/devextreme/scss/widgets/generic/textEditor/_colors.scss b/packages/devextreme/scss/widgets/generic/textEditor/_colors.scss index d83e5abef68a..7c1d6fec7d56 100644 --- a/packages/devextreme/scss/widgets/generic/textEditor/_colors.scss +++ b/packages/devextreme/scss/widgets/generic/textEditor/_colors.scss @@ -52,6 +52,8 @@ $texteditor-invalid-faded-border-color: $base-invalid-faded-border-color !defaul $texteditor-filled-invalid-background: null !default; $texteditor-border-radius: $base-border-radius !default; $texteditor-input-border-radius: $base-border-radius !default; +$texteditor-disabled-border-color: color.change($texteditor-color, $alpha: 0.5) !default; +$texteditor-readonly-border-color: $base-border-color !default; $texteditor-label-transition: font-size 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms, transform 0.2s cubic-bezier(0, 0, 0.2, 1) 0ms, diff --git a/packages/devextreme/scss/widgets/generic/textEditor/_index.scss b/packages/devextreme/scss/widgets/generic/textEditor/_index.scss index 2b6893313191..2d010a18f8aa 100644 --- a/packages/devextreme/scss/widgets/generic/textEditor/_index.scss +++ b/packages/devextreme/scss/widgets/generic/textEditor/_index.scss @@ -255,7 +255,8 @@ $generic-texteditor-invalid-badge-size: $generic-invalid-badge-size + 2 * $gener none, $texteditor-border-color, $texteditor-border-color, - color.change($texteditor-color, $alpha: 0.5), + $texteditor-disabled-border-color, + $texteditor-readonly-border-color, $texteditor-hover-border-color, $texteditor-hover-border-color, $texteditor-focused-border-color, diff --git a/packages/devextreme/scss/widgets/material/textEditor/_index.scss b/packages/devextreme/scss/widgets/material/textEditor/_index.scss index 5e45166462a0..660625a685f9 100644 --- a/packages/devextreme/scss/widgets/material/textEditor/_index.scss +++ b/packages/devextreme/scss/widgets/material/textEditor/_index.scss @@ -375,6 +375,7 @@ $material-editor-custom-button-margin: 5px; $texteditor-border-color, $texteditor-border-color, $texteditor-disabled-color, + $texteditor-disabled-color, $texteditor-hover-border-color, $texteditor-hover-border-color, $texteditor-focused-border-color, diff --git a/packages/devextreme/testing/testcafe/helpers/machineTimezones.ts b/packages/devextreme/testing/testcafe/helpers/machineTimezones.ts new file mode 100644 index 000000000000..480f8fc7e34d --- /dev/null +++ b/packages/devextreme/testing/testcafe/helpers/machineTimezones.ts @@ -0,0 +1,17 @@ +export const MACHINE_TIMEZONES = { + EuropeBerlin: 'Europe/Berlin', + AmericaLosAngeles: 'America/Los_Angeles', +} as const; +export type MachineTimezonesType = typeof MACHINE_TIMEZONES[keyof typeof MACHINE_TIMEZONES]; + +export const getMachineTimezone = (): string => Intl.DateTimeFormat().resolvedOptions().timeZone; + +export const getTimezoneTest = (timezones: MachineTimezonesType[]): TestFn => { + const machineTimezone = getMachineTimezone(); + return timezones.includes(machineTimezone as MachineTimezonesType) ? test : test.skip; +}; + +export const getTimezoneFixture = (timezones: MachineTimezonesType[]): FixtureFn => { + const machineTimezone = getMachineTimezone(); + return timezones.includes(machineTimezone as MachineTimezonesType) ? fixture : fixture.skip; +}; diff --git a/packages/devextreme/testing/testcafe/helpers/widgetTypings.ts b/packages/devextreme/testing/testcafe/helpers/widgetTypings.ts index 669b62b1a431..2fafc5649994 100644 --- a/packages/devextreme/testing/testcafe/helpers/widgetTypings.ts +++ b/packages/devextreme/testing/testcafe/helpers/widgetTypings.ts @@ -41,6 +41,7 @@ export type WidgetName = | 'dxTextBox' | 'dxTextArea' | 'dxToolbar' + | 'dxTileView' | 'dxTreeView' | 'dxDateBox' | 'dxDateRangeBox' diff --git a/packages/devextreme/testing/testcafe/tests/accessibility/calendar.ts b/packages/devextreme/testing/testcafe/tests/accessibility/calendar.ts index 901cb1aeff35..54e54f6e2a7c 100644 --- a/packages/devextreme/testing/testcafe/tests/accessibility/calendar.ts +++ b/packages/devextreme/testing/testcafe/tests/accessibility/calendar.ts @@ -26,8 +26,6 @@ const a11yCheckConfig = { rules: { // NOTE: color-contrast issues 'color-contrast': { enabled: false }, - // NOTE: aria-allowed-attr issues - 'aria-allowed-attr': { enabled: false }, // NOTE: empty-table-header issues 'empty-table-header': { enabled: false }, }, diff --git a/packages/devextreme/testing/testcafe/tests/accessibility/switch.ts b/packages/devextreme/testing/testcafe/tests/accessibility/switch.ts index b8e97a4317f5..c46daeebd945 100644 --- a/packages/devextreme/testing/testcafe/tests/accessibility/switch.ts +++ b/packages/devextreme/testing/testcafe/tests/accessibility/switch.ts @@ -19,7 +19,6 @@ const a11yCheckConfig = { // NOTE: color-contrast issues rules: { 'color-contrast': { enabled: false }, - 'aria-allowed-attr': { enabled: false }, }, }; diff --git a/packages/devextreme/testing/testcafe/tests/accessibility/tileView.ts b/packages/devextreme/testing/testcafe/tests/accessibility/tileView.ts new file mode 100644 index 000000000000..38c59c3b7fde --- /dev/null +++ b/packages/devextreme/testing/testcafe/tests/accessibility/tileView.ts @@ -0,0 +1,35 @@ +import url from '../../helpers/getPageUrl'; +import { clearTestPage } from '../../helpers/clearPage'; +import { testAccessibility, Configuration } from '../../helpers/accessibility/test'; +import { Options } from '../../helpers/generateOptionMatrix'; +import { Properties } from '../../../../js/ui/tile_view.d'; + +fixture.disablePageReloads`Accessibility` + .page(url(__dirname, '../container.html')) + .afterEach(async () => clearTestPage()); + +const options: Options = { + items: [[{ text: 'test 1' }]], + focusStateEnabled: [true], +}; + +const created = async (t: TestController): Promise => { + await t.pressKey('tab'); +}; + +const a11yCheckConfig = { + rules: { + 'color-contrast': { + enabled: false, + }, + }, +}; + +const configuration: Configuration = { + component: 'dxTileView', + a11yCheckConfig, + options, + created, +}; + +testAccessibility(configuration); diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=filled (fluent-blue-light).png b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=filled (fluent-blue-light).png new file mode 100644 index 000000000000..31b1def81a8a Binary files /dev/null and b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=filled (fluent-blue-light).png differ diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=filled (generic-light).png b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=filled (generic-light).png new file mode 100644 index 000000000000..d1fff1db2bc8 Binary files /dev/null and b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=filled (generic-light).png differ diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=filled (material-blue-light).png b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=filled (material-blue-light).png new file mode 100644 index 000000000000..dd9a80a7f36c Binary files /dev/null and b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=filled (material-blue-light).png differ diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=outlined (fluent-blue-light).png b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=outlined (fluent-blue-light).png new file mode 100644 index 000000000000..72e082c59dd2 Binary files /dev/null and b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=outlined (fluent-blue-light).png differ diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=outlined (generic-light).png b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=outlined (generic-light).png new file mode 100644 index 000000000000..0f8b500a3b6f Binary files /dev/null and b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=outlined (generic-light).png differ diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=outlined (material-blue-light).png b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=outlined (material-blue-light).png new file mode 100644 index 000000000000..d5271d800f9b Binary files /dev/null and b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=outlined (material-blue-light).png differ diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=underlined (fluent-blue-light).png b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=underlined (fluent-blue-light).png new file mode 100644 index 000000000000..e7e683c3ef04 Binary files /dev/null and b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=underlined (fluent-blue-light).png differ diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=underlined (generic-light).png b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=underlined (generic-light).png new file mode 100644 index 000000000000..17d0565f7fe0 Binary files /dev/null and b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=underlined (generic-light).png differ diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=underlined (material-blue-light).png b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=underlined (material-blue-light).png new file mode 100644 index 000000000000..56b27d51fbc9 Binary files /dev/null and b/packages/devextreme/testing/testcafe/tests/editors/textBox/etalons/Textbox render readonly,stylingMode=underlined (material-blue-light).png differ diff --git a/packages/devextreme/testing/testcafe/tests/editors/textBox/label.ts b/packages/devextreme/testing/testcafe/tests/editors/textBox/label.ts index bce12d816b41..3a86cfe97213 100644 --- a/packages/devextreme/testing/testcafe/tests/editors/textBox/label.ts +++ b/packages/devextreme/testing/testcafe/tests/editors/textBox/label.ts @@ -24,6 +24,7 @@ const stylingModes = ['outlined', 'underlined', 'filled']; const TEXTBOX_CLASS = 'dx-textbox'; const HOVER_STATE_CLASS = 'dx-state-hover'; const FOCUSED_STATE_CLASS = 'dx-state-focused'; +const READONLY_STATE_CLASS = 'dx-state-readonly'; const INVALID_STATE_CLASS = 'dx-invalid'; [ @@ -73,7 +74,15 @@ stylingModes.forEach((stylingMode) => { await testScreenshot(t, takeScreenshot, `Textbox render stylingMode=${stylingMode}.png`); - for (const state of [HOVER_STATE_CLASS, FOCUSED_STATE_CLASS, INVALID_STATE_CLASS, `${INVALID_STATE_CLASS} ${FOCUSED_STATE_CLASS}`] as any[]) { + const states = [ + HOVER_STATE_CLASS, + FOCUSED_STATE_CLASS, + READONLY_STATE_CLASS, + INVALID_STATE_CLASS, + `${INVALID_STATE_CLASS} ${FOCUSED_STATE_CLASS}`, + ]; + + for (const state of states as any[]) { for (const id of t.ctx.ids) { await setClassAttribute(Selector(`#${id}`), state); } diff --git a/packages/devextreme/testing/testcafe/tests/scheduler/timezones/check.ts b/packages/devextreme/testing/testcafe/tests/scheduler/timezones/check.ts new file mode 100644 index 000000000000..ac3525da6f9a --- /dev/null +++ b/packages/devextreme/testing/testcafe/tests/scheduler/timezones/check.ts @@ -0,0 +1,20 @@ +import { ClientFunction } from 'testcafe'; +import { getTimezoneTest, MACHINE_TIMEZONES, MachineTimezonesType } from '../../../helpers/machineTimezones'; +import url from '../../../helpers/getPageUrl'; + +fixture + .disablePageReloads`Runner machine timezone checks` + .page(url(__dirname, '../../container.html')); + +type CheckType = [MachineTimezonesType, string]; +const checks: CheckType[] = [ + [MACHINE_TIMEZONES.AmericaLosAngeles, 'Mon Jan 01 2024 10:00:00 GMT-0800 (Pacific Standard Time)'], + [MACHINE_TIMEZONES.EuropeBerlin, 'Mon Jan 01 2024 10:00:00 GMT+0100 (Central European Standard Time)'], +]; + +checks.forEach(([timezone, expectedResult]) => { + getTimezoneTest([timezone])(`${timezone} check`, async (t) => { + const dateFromBrowser = await ClientFunction(() => new Date(2024, 0, 1, 10).toString())(); + await t.expect(dateFromBrowser).eql(expectedResult); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/calendar.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/calendar.tests.js index f5d79b5ead88..74a428452ce7 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/calendar.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/calendar.tests.js @@ -4707,6 +4707,15 @@ QUnit.module('Aria accessibility', { assert.equal($cell.attr('role'), 'gridcell', 'Cell: aria role is correct'); }); + QUnit.test('The calendar view wrapper does not have an aria-readonly attribute', function(assert) { + this.$element.dxCalendar({ + readOnly: true, + }); + const $viewsWrapper = $(this.$element.find(toSelector(CALENDAR_VIEWS_WRAPPER_CLASS))); + + assert.equal($viewsWrapper.attr('aria-readonly'), undefined); + }); + QUnit.test('aria id on contoured date cell', function(assert) { const calendar = this.$element.dxCalendar({ value: new Date(2015, 5, 1), diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/switch.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/switch.markup.tests.js index cd47c9e56b71..39d34f7411a9 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/switch.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/switch.markup.tests.js @@ -157,27 +157,59 @@ QUnit.module('Switch markup', () => { }); }); -QUnit.module('aria accessibility', () => { - QUnit.test('aria role', function(assert) { - const $element = $('#switch').dxSwitch({}); +QUnit.module('Accessibility', () => { + QUnit.test('Switch should have correct role attribute', function(assert) { + const $element = $('#switch').dxSwitch(); - assert.equal($element.attr('role'), 'button', 'aria role is correct'); + assert.strictEqual($element.attr('role'), 'switch', 'Element should have role=switch'); }); - QUnit.test('aria properties', function(assert) { + QUnit.test('Switch should have correct aria-checked attribute', function(assert) { + const $element = $('#switch').dxSwitch(); + const instance = $element.dxSwitch('instance'); + + assert.strictEqual($element.attr('aria-checked'), 'false', 'aria-checked must be false if the value is false'); + + instance.option({ value: true }); + + assert.strictEqual($element.attr('aria-checked'), 'true', 'aria-checked must be true if the value is true'); + }); + + QUnit.test('Switch should have correct aria-label attribute', function(assert) { const $element = $('#switch').dxSwitch({ switchedOnText: 'on test', switchedOffText: 'off test', - value: true }); const instance = $element.dxSwitch('instance'); - assert.equal($element.attr('aria-label'), 'on test', 'aria \'on state\' label is correct'); - assert.equal($element.attr('aria-pressed'), 'true', 'aria \'on state\' pressed attribute is correct'); + assert.strictEqual($element.attr('aria-label'), 'off test', 'aria-label must have switchOffText if the value is false'); + + instance.option({ value: true }); - instance.option('value', false); - assert.equal($element.attr('aria-label'), 'off test', 'aria \'off state\' label is correct'); - assert.equal($element.attr('aria-pressed'), 'false', 'aria \'off state\' pressed attribute is correct'); + assert.strictEqual($element.attr('aria-label'), 'on test', 'aria-label must have switchedOnText if the value if true'); }); -}); + QUnit.test('Switch should have correct aria-disabled and tabindex attributes', function(assert) { + const $element = $('#switch').dxSwitch({ focusStateEnabled: true }); + const instance = $element.dxSwitch('instance'); + + assert.strictEqual($element.attr('aria-disabled'), undefined, 'The element should not have an aria-disabled attribute unless it is disabled'); + assert.strictEqual($element.attr('tabindex'), '0', 'The element must have tabindex=0 if it is not disabled'); + + instance.option({ disabled: true }); + + assert.strictEqual($element.attr('aria-disabled'), 'true', 'The element must have aria-disabled=true if it is disabled'); + assert.strictEqual($element.attr('tabindex'), undefined, 'The element should not have an aria-disabled attribute if it is disabled'); + }); + + QUnit.test('Switch should have correct aria-readonly attribute', function(assert) { + const $element = $('#switch').dxSwitch(); + const instance = $element.dxSwitch('instance'); + + assert.strictEqual($element.attr('aria-readonly'), undefined, 'The element should not have aria-readonly attribute if readOnly is false'); + + instance.option({ readOnly: true }); + + assert.strictEqual($element.attr('aria-readonly'), 'true', 'The element must have aria-readonly=true if readOnly is true'); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/switch.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/switch.tests.js index 601574d141a4..3c80857a2593 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/switch.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/switch.tests.js @@ -638,4 +638,3 @@ QUnit.module('valueChanged handler should receive correct event parameter', { }); }); }); - diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/tileView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/tileView.tests.js index 7066ad96b393..fd557ec0ca3e 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/tileView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/tileView.tests.js @@ -276,6 +276,28 @@ QUnit.module('rendering', { assert.strictEqual($tile.outerHeight(), DEFAULT_ITEMSIZE, 'Tile height updated correctly'); assert.strictEqual($tile.outerWidth(), DEFAULT_ITEMSIZE, 'Tile width updated correctly'); }); + + QUnit.testInActiveWindow('aria-activedescendant should not be set for the component after tile focus (T1217255)', function(assert) { + const clock = sinon.useFakeTimers(); + + try { + this.$element.dxTileView({ + items: [{ text: 'test 1' }], + focusStateEnabled: true, + }); + + const $firstItem = this.$element.find(TILEVIEW_ITEM_SELECTOR).eq(0); + + $firstItem.trigger('dxpointerdown'); + + clock.tick(10); + + assert.strictEqual($firstItem.hasClass('dx-state-focused'), true); + assert.strictEqual(this.$element.attr('aria-activedescendant'), undefined); + } finally { + clock.restore(); + } + }); });