diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/af-commons/v1/clientlibs/editor/utils/utils.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/af-commons/v1/clientlibs/editor/utils/utils.js
index 5100f7e758..a14e722179 100644
--- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/af-commons/v1/clientlibs/editor/utils/utils.js
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/af-commons/v1/clientlibs/editor/utils/utils.js
@@ -316,6 +316,138 @@
hiddenFields[i].remove();
}
}
+
+ /**
+ * Returns an initializeEditDialog-compatible handler that validates min < max in real time.
+ * Works for both wrapperClass selectors (class on a wrapper div) and granite:class selectors
+ * (class on the coral field itself) by looking for a coral-numberinput or coral-datepicker
+ * inside the selector before falling back to the selector element itself.
+ *
+ * @param {String} minSelector Dialog-prefixed CSS selector for the min field or its wrapper
+ * @param {String} maxSelector Dialog-prefixed CSS selector for the max field or its wrapper
+ * @param {String} minMsg Error message shown on the min field when min > max
+ * @param {String} maxMsg Error message shown on the max field when min > max
+ * @param {Function} [compareFn] Optional (a, b) => boolean. Defaults to numeric comparison.
+ */
+ static handleMinMaxValidation(minSelector, maxSelector, minMsg, maxMsg, compareFn) {
+ return function(dialog) {
+ function getCoralField(selector) {
+ var container = dialog.find(selector);
+ if (!container.length) return null;
+ var inner = container.find("coral-numberinput, coral-datepicker");
+ return (inner.length ? inner : container)[0];
+ }
+ var minField = getCoralField(minSelector);
+ var maxField = getCoralField(maxSelector);
+ if (!minField || !maxField) return;
+ var compare = compareFn || INT_COMPARE;
+ function validate() {
+ var minVal = minField.value, maxVal = maxField.value;
+ var invalid = !!(minVal && maxVal && compare(minVal, maxVal));
+ var minErrMsg = Granite.I18n.getMessage(minMsg);
+ var maxErrMsg = Granite.I18n.getMessage(maxMsg);
+ if (invalid) {
+ minField.invalid = true;
+ maxField.invalid = true;
+ minField.errorMessage = minErrMsg;
+ maxField.errorMessage = maxErrMsg;
+ } else {
+ // Only clear invalid if this listener set it — avoids clobbering
+ // required/pattern errors that another validator placed on the field.
+ if (minField.errorMessage === minErrMsg) {
+ minField.invalid = false;
+ minField.errorMessage = "";
+ }
+ if (maxField.errorMessage === maxErrMsg) {
+ maxField.invalid = false;
+ maxField.errorMessage = "";
+ }
+ }
+ }
+ validate();
+ minField.addEventListener("change", validate);
+ maxField.addEventListener("change", validate);
+ };
+ }
+
+ /**
+ * Registers a foundation.validation.validator that blocks dialog submission when min > max.
+ * Must be called once at module scope (not inside initializeEditDialog) to avoid stacking
+ * duplicate validators on every dialog open.
+ *
+ * @param {String} minFieldSelector Bare CSS selector targeting the coral field for min
+ * @param {String} maxFieldSelector Bare CSS selector targeting the coral field for max
+ * @param {String} minMsg Error message for the min field
+ * @param {String} maxMsg Error message for the max field
+ * @param {Function} [compareFn] Optional (a, b) => boolean. Defaults to numeric comparison.
+ */
+ static registerMinMaxValidator(minFieldSelector, maxFieldSelector, minMsg, maxMsg, compareFn) {
+ var compare = compareFn || INT_COMPARE;
+ $(window).adaptTo("foundation-registry").register("foundation.validation.validator", {
+ selector: minFieldSelector + ", " + maxFieldSelector,
+ validate: function(el) {
+ var dialog = $(el).closest("coral-dialog");
+ var minField = dialog.find(minFieldSelector)[0];
+ var maxField = dialog.find(maxFieldSelector)[0];
+ if (!minField || !maxField) return;
+ var minVal = minField.value, maxVal = maxField.value;
+ if (minVal && maxVal && compare(minVal, maxVal)) {
+ if (el === minField) {
+ return Granite.I18n.getMessage(minMsg);
+ }
+ return Granite.I18n.getMessage(maxMsg);
+ }
+ }
+ });
+ }
}
+ // ─── Shared min/max validation for container components ──────────────────
+ // panelcontainer__minOccur / __maxOccur is shared across accordion, wizard,
+ // tabsontop, verticaltabs, and fragment — none of which have their own
+ // editDialog.js — so registration is centralised here.
+ var INT_COMPARE = function(a, b) { return parseInt(a, 10) > parseInt(b, 10); };
+
+ var MIN_MAX_PAIRS = [
+ {
+ minSelector: ".cmp-adaptiveform-panelcontainer__minOccur coral-numberinput",
+ maxSelector: ".cmp-adaptiveform-panelcontainer__maxOccur coral-numberinput",
+ minMsg: "Minimum occurrence cannot be greater than maximum occurrence",
+ maxMsg: "Maximum occurrence cannot be less than minimum occurrence",
+ compareFn: INT_COMPARE
+ }
+ ];
+
+ var Utils = window.CQ.FormsCoreComponents.Utils.v1;
+
+ // Register all foundation submit-time validators once at page load.
+ MIN_MAX_PAIRS.forEach(function(pair) {
+ Utils.registerMinMaxValidator(
+ pair.minSelector,
+ pair.maxSelector,
+ pair.minMsg,
+ pair.maxMsg,
+ pair.compareFn
+ );
+ });
+
+ // Auto-wire real-time change listeners whenever any dialog opens.
+ channel.on("foundation-contentloaded", function(e) {
+ if (!$(e.target).find(".cq-dialog-content").length) return;
+ Coral.commons.ready(e.target, function() {
+ var dialog = $(e.target);
+ MIN_MAX_PAIRS.forEach(function(pair) {
+ if (dialog.find(pair.minSelector).length && dialog.find(pair.maxSelector).length) {
+ Utils.handleMinMaxValidation(
+ pair.minSelector,
+ pair.maxSelector,
+ pair.minMsg,
+ pair.maxMsg,
+ pair.compareFn
+ )(dialog);
+ }
+ });
+ });
+ });
+
})(jQuery, jQuery(document), Coral);
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/editor/js/editDialog.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/editor/js/editDialog.js
index 9e040385d9..d4c4598169 100644
--- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/editor/js/editDialog.js
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/editor/js/editDialog.js
@@ -26,6 +26,10 @@
DATEPICKER_DEFAULTDATE = EDIT_DIALOG + " .cmp-adaptiveform-datepicker__defaultdate",
DATEPICKER_MINDATE = EDIT_DIALOG + " .cmp-adaptiveform-datepicker__mindate",
DATEPICKER_MAXDATE = EDIT_DIALOG + " .cmp-adaptiveform-datepicker__maxdate",
+ DATEPICKER_MIN_FIELD = ".cmp-adaptiveform-datepicker__mindate coral-datepicker",
+ DATEPICKER_MAX_FIELD = ".cmp-adaptiveform-datepicker__maxdate coral-datepicker",
+ DATEPICKER_MIN_MSG = "Minimum date cannot be after maximum date",
+ DATEPICKER_MAX_MSG = "Maximum date cannot be before minimum date",
Utils = window.CQ.FormsCoreComponents.Utils.v1;
@@ -69,5 +73,28 @@
maxDateTooltip.innerHTML = fieldDescription;
}
- Utils.initializeEditDialog(EDIT_DIALOG)(handleDisplayPatternDropDown,handleDisplayFormat,handleEditPatternDropDown,handleEditFormat,handleLang,handleDatePlaceholders);
+ var DATE_COMPARE = function(a, b) {
+ var da = new Date(a), db = new Date(b);
+ return !isNaN(da) && !isNaN(db) && da > db;
+ };
+
+ Utils.registerMinMaxValidator(
+ DATEPICKER_MIN_FIELD, DATEPICKER_MAX_FIELD,
+ DATEPICKER_MIN_MSG, DATEPICKER_MAX_MSG,
+ DATE_COMPARE
+ );
+
+ Utils.initializeEditDialog(EDIT_DIALOG)(
+ handleDisplayPatternDropDown,
+ handleDisplayFormat,
+ handleEditPatternDropDown,
+ handleEditFormat,
+ handleLang,
+ handleDatePlaceholders,
+ Utils.handleMinMaxValidation(
+ DATEPICKER_MIN_FIELD, DATEPICKER_MAX_FIELD,
+ DATEPICKER_MIN_MSG, DATEPICKER_MAX_MSG,
+ DATE_COMPARE
+ )
+ );
})(jQuery);
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datetime/v1/datetime/_cq_dialog/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datetime/v1/datetime/_cq_dialog/.content.xml
index daabf7d72e..70413bd31d 100644
--- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datetime/v1/datetime/_cq_dialog/.content.xml
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datetime/v1/datetime/_cq_dialog/.content.xml
@@ -77,6 +77,7 @@
type="datetime"
emptyText="YYYY-MM-DD HH:mm"
name="./minimumDateTime"
+ wrapperClass="cmp-adaptiveform-datetime__minimumDateTime"
valueFormat="YYYY-MM-DD[T]HH:mm:ss.000-00:00"/>
db;
+ };
+
+ Utils.registerMinMaxValidator(
+ DATETIME_MIN_FIELD, DATETIME_MAX_FIELD,
+ DATETIME_MIN_MSG, DATETIME_MAX_MSG,
+ DATE_COMPARE
+ );
+
+ Utils.initializeEditDialog(EDIT_DIALOG)(
+ Utils.handleMinMaxValidation(
+ DATETIME_MIN_FIELD, DATETIME_MAX_FIELD,
+ DATETIME_MIN_MSG, DATETIME_MAX_MSG,
+ DATE_COMPARE
+ )
+ );
})(jQuery);
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v1/fileinput/_cq_dialog/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v1/fileinput/_cq_dialog/.content.xml
index 37fc33697b..84bb4314a4 100644
--- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v1/fileinput/_cq_dialog/.content.xml
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/fileinput/v1/fileinput/_cq_dialog/.content.xml
@@ -101,6 +101,7 @@
Number(b); };
+
+ Utils.registerMinMaxValidator(
+ NUMERICINPUT_MIN_FIELD, NUMERICINPUT_MAX_FIELD,
+ NUMERICINPUT_MIN_MSG, NUMERICINPUT_MAX_MSG,
+ NUMBER_COMPARE
+ );
+
+ Utils.initializeEditDialog(EDIT_DIALOG)(
+ handleDisplayPatternDropDown,
+ handleDisplayFormat,
+ handleLang,
+ handleTypeDropdown,
+ Utils.handleMinMaxValidation(
+ NUMERICINPUT_MIN_FIELD, NUMERICINPUT_MAX_FIELD,
+ NUMERICINPUT_MIN_MSG, NUMERICINPUT_MAX_MSG,
+ NUMBER_COMPARE
+ )
+ );
})(jQuery);
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/textinput/v1/textinput/clientlibs/editor/js/editDialog.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/textinput/v1/textinput/clientlibs/editor/js/editDialog.js
index 25312b21b8..cc5ed34239 100644
--- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/textinput/v1/textinput/clientlibs/editor/js/editDialog.js
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/textinput/v1/textinput/clientlibs/editor/js/editDialog.js
@@ -20,6 +20,10 @@
TEXTINPUT_ALLOWRICHTEXT = EDIT_DIALOG + " .cmp-adaptiveform-textinput__allowrichtext",
TEXTINPUT_MAXLENGTH = EDIT_DIALOG + " .cmp-adaptiveform-textinput__maxlength",
TEXTINPUT_MINLENGTH = EDIT_DIALOG + " .cmp-adaptiveform-textinput__minlength",
+ TEXTINPUT_MIN_FIELD = ".cmp-adaptiveform-textinput__minlength coral-numberinput",
+ TEXTINPUT_MIN_MSG = "Minimum length cannot be greater than maximum length",
+ TEXTINPUT_MAX_MSG = "Maximum length cannot be less than minimum length",
+ TEXTINPUT_MAX_FIELD = ".cmp-adaptiveform-textinput__maxlength coral-numberinput",
BASE_PLACEHOLDER = EDIT_DIALOG + " .cmp-adaptiveform-base__placeholder",
TEXTINPUT_VALUE = EDIT_DIALOG + " .cmp-adaptiveform-textinput__value",
TEXTINPUT_RICHTEXTVALUE = EDIT_DIALOG + " .cmp-adaptiveform-textinput__richtextvalue",
@@ -76,6 +80,21 @@
patternComponent.addEventListener("change", updateDisplayValueExpression);
}
- Utils.initializeEditDialog(EDIT_DIALOG)(handleValidationPatternDropDown,handleValidationFormat,handleDisplayPatternDropDown,handleDisplayFormat,handleDisplayValueExpression);
+ Utils.registerMinMaxValidator(
+ TEXTINPUT_MIN_FIELD, TEXTINPUT_MAX_FIELD,
+ TEXTINPUT_MIN_MSG, TEXTINPUT_MAX_MSG
+ );
+
+ Utils.initializeEditDialog(EDIT_DIALOG)(
+ handleValidationPatternDropDown,
+ handleValidationFormat,
+ handleDisplayPatternDropDown,
+ handleDisplayFormat,
+ handleDisplayValueExpression,
+ Utils.handleMinMaxValidation(
+ TEXTINPUT_MIN_FIELD, TEXTINPUT_MAX_FIELD,
+ TEXTINPUT_MIN_MSG, TEXTINPUT_MAX_MSG
+ )
+ );
})(jQuery);
diff --git a/ui.tests/test-module/specs/datepicker/datepicker.minmax.authoring.cy.js b/ui.tests/test-module/specs/datepicker/datepicker.minmax.authoring.cy.js
new file mode 100644
index 0000000000..70238b5e97
--- /dev/null
+++ b/ui.tests/test-module/specs/datepicker/datepicker.minmax.authoring.cy.js
@@ -0,0 +1,92 @@
+/*******************************************************************************
+ * Copyright 2022 Adobe
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+
+const afConstants = require("../../libs/commons/formsConstants");
+const sitesSelectors = require("../../libs/commons/sitesSelectors");
+
+describe('Page - Authoring', function () {
+ const dropDatePickerInContainer = function () {
+ const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*",
+ responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']";
+ cy.selectLayer("Edit");
+ cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Date Picker", afConstants.components.forms.resourceType.datepicker);
+ cy.get('body').click(0, 0);
+ };
+
+ const setDatePickerValue = function (selector, isoDate) {
+ cy.get(selector).then(($el) => {
+ $el[0].value = isoDate;
+ $el[0].dispatchEvent(new Event('change', { bubbles: true }));
+ });
+ };
+
+ context('Open Forms Editor', function () {
+ const pagePath = "/content/forms/af/core-components-it/blank",
+ datePickerEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/datepicker",
+ datePickerEditPathSelector = "[data-path='" + datePickerEditPath + "']",
+ datePickerDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.datepicker.split("/").pop(),
+ editDialogConfigurationSelector = "[data-action='CONFIGURE']",
+ minField = '.cmp-adaptiveform-datepicker__mindate coral-datepicker',
+ maxField = '.cmp-adaptiveform-datepicker__maxdate coral-datepicker';
+
+ beforeEach(function () {
+ cy.openAuthoring(pagePath);
+ });
+
+ it('shows inline error when minimum date is set after maximum date', function () {
+ dropDatePickerInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + datePickerEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+ cy.get('.cmp-adaptiveform-datepicker__editdialog').contains('Validation').click();
+
+ // Set max = 2024-01-10, then min = 2024-01-20 (invalid: min after max)
+ setDatePickerValue(maxField, '2024-01-10');
+ setDatePickerValue(minField, '2024-01-20');
+
+ // Both fields should be marked invalid
+ cy.get(minField).should('have.attr', 'invalid');
+ cy.get(maxField).should('have.attr', 'invalid');
+
+ // Fix by setting min before max
+ setDatePickerValue(minField, '2024-01-05');
+
+ // Both fields should no longer be invalid
+ cy.get(minField).should('not.have.attr', 'invalid');
+ cy.get(maxField).should('not.have.attr', 'invalid');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(datePickerDrop);
+ });
+
+ it('blocks dialog save when minimum date is after maximum date', function () {
+ dropDatePickerInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + datePickerEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+ cy.get('.cmp-adaptiveform-datepicker__editdialog').contains('Validation').click();
+
+ // Set an invalid state: min after max
+ setDatePickerValue(maxField, '2024-01-10');
+ setDatePickerValue(minField, '2024-01-20');
+
+ // Attempt to save — dialog should remain open
+ cy.get('.cq-dialog-submit').click();
+ cy.get('coral-dialog[open]').should('exist');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(datePickerDrop);
+ });
+ });
+});
diff --git a/ui.tests/test-module/specs/datetime/datetime.minmax.authoring.cy.js b/ui.tests/test-module/specs/datetime/datetime.minmax.authoring.cy.js
new file mode 100644
index 0000000000..4a50b8f326
--- /dev/null
+++ b/ui.tests/test-module/specs/datetime/datetime.minmax.authoring.cy.js
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright 2025 Adobe
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+
+const afConstants = require("../../libs/commons/formsConstants");
+const sitesSelectors = require("../../libs/commons/sitesSelectors");
+
+describe('Page - Authoring', function () {
+ const dropDateTimeInContainer = function () {
+ const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*",
+ responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']";
+ cy.selectLayer("Edit");
+ cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Date and Time", afConstants.components.forms.resourceType.datetime);
+ cy.get('body').click(0, 0);
+ };
+
+ const setDatePickerValue = function (selector, isoDate) {
+ cy.get(selector).then(($el) => {
+ $el[0].value = isoDate;
+ $el[0].dispatchEvent(new Event('change', { bubbles: true }));
+ });
+ };
+
+ context('Open Forms Editor', function () {
+ const pagePath = "/content/forms/af/core-components-it/blank",
+ dateTimeEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/datetime",
+ dateTimeEditPathSelector = "[data-path='" + dateTimeEditPath + "']",
+ dateTimeDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.datetime.split("/").pop(),
+ editDialogConfigurationSelector = "[data-action='CONFIGURE']",
+ minField = '.cmp-adaptiveform-datetime__minimumDateTime coral-datepicker',
+ maxField = '.cmp-adaptiveform-datetime__maximumDateTime coral-datepicker';
+
+ beforeEach(function () {
+ cy.openAuthoring(pagePath);
+ });
+
+ it('shows inline error when minimum date-time is set after maximum date-time', function () {
+ dropDateTimeInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + dateTimeEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+
+ // min/max fields are on the Basic tab (open by default)
+ // Set max = 2024-01-10, then min = 2024-01-20 (invalid: min after max)
+ setDatePickerValue(maxField, '2024-01-10');
+ setDatePickerValue(minField, '2024-01-20');
+
+ // Both fields should be marked invalid
+ cy.get(minField).should('have.attr', 'invalid');
+ cy.get(maxField).should('have.attr', 'invalid');
+
+ // Fix by setting min before max
+ setDatePickerValue(minField, '2024-01-05');
+
+ // Both fields should no longer be invalid
+ cy.get(minField).should('not.have.attr', 'invalid');
+ cy.get(maxField).should('not.have.attr', 'invalid');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(dateTimeDrop);
+ });
+
+ it('blocks dialog save when minimum date-time is after maximum date-time', function () {
+ dropDateTimeInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + dateTimeEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+
+ // Set an invalid state: min after max
+ setDatePickerValue(maxField, '2024-01-10');
+ setDatePickerValue(minField, '2024-01-20');
+
+ // Attempt to save — dialog should remain open
+ cy.get('.cq-dialog-submit').click();
+ cy.get('coral-dialog[open]').should('exist');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(dateTimeDrop);
+ });
+ });
+});
diff --git a/ui.tests/test-module/specs/fileinput/fileinput.minmax.authoring.cy.js b/ui.tests/test-module/specs/fileinput/fileinput.minmax.authoring.cy.js
new file mode 100644
index 0000000000..31ddcb3054
--- /dev/null
+++ b/ui.tests/test-module/specs/fileinput/fileinput.minmax.authoring.cy.js
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * Copyright 2022 Adobe
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+
+const afConstants = require("../../libs/commons/formsConstants");
+const sitesSelectors = require("../../libs/commons/sitesSelectors");
+
+describe('Page - Authoring', function () {
+ const dropFileInputInContainer = function () {
+ const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*",
+ responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']";
+ cy.selectLayer("Edit");
+ cy.insertComponent(responsiveGridDropZoneSelector, "File Attachment", afConstants.components.forms.resourceType.formfileinput);
+ cy.get('body').click(0, 0);
+ };
+
+ const setNumberInputValue = function (selector, value) {
+ cy.get(selector).then(($el) => {
+ $el[0].value = value;
+ $el[0].dispatchEvent(new Event('change', { bubbles: true }));
+ });
+ };
+
+ context('Open Forms Editor', function () {
+ const pagePath = "/content/forms/af/core-components-it/blank",
+ fileInputEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/fileinput",
+ fileInputEditPathSelector = "[data-path='" + fileInputEditPath + "']",
+ fileInputDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.formfileinput.split("/").pop(),
+ editDialogConfigurationSelector = "[data-action='CONFIGURE']",
+ minField = '.cmp-adaptiveform-fileinput__minimumFiles coral-numberinput',
+ maxField = '.cmp-adaptiveform-fileinput__maximumFiles coral-numberinput';
+
+ beforeEach(function () {
+ cy.openAuthoring(pagePath);
+ });
+
+ it('shows inline error when minimum files is set greater than maximum files', function () {
+ dropFileInputInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + fileInputEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+
+ // Enable multi-selection (Basic tab) to reveal min/max files fields on the Validation tab
+ cy.get("[name='./multiSelection'][type='checkbox']").should('exist').check({ force: true });
+ // Navigate to Validation tab where min/max fields live
+ cy.get('.cmp-adaptiveform-fileinput__editdialog').contains('Validation').click();
+ cy.get(minField).should('be.visible');
+
+ // Set maximum = 5, then minimum = 10 (invalid: min > max)
+ setNumberInputValue(maxField, 5);
+ setNumberInputValue(minField, 10);
+
+ // Both fields should be marked invalid
+ cy.get(minField).should('have.attr', 'invalid');
+ cy.get(maxField).should('have.attr', 'invalid');
+
+ // Fix by lowering minimum below maximum
+ setNumberInputValue(minField, 3);
+
+ // Both fields should no longer be invalid
+ cy.get(minField).should('not.have.attr', 'invalid');
+ cy.get(maxField).should('not.have.attr', 'invalid');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(fileInputDrop);
+ });
+
+ it('blocks dialog save when minimum files is greater than maximum files', function () {
+ dropFileInputInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + fileInputEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+
+ // Enable multi-selection (Basic tab) to reveal min/max files fields on the Validation tab
+ cy.get("[name='./multiSelection'][type='checkbox']").should('exist').check({ force: true });
+ // Navigate to Validation tab where min/max fields live
+ cy.get('.cmp-adaptiveform-fileinput__editdialog').contains('Validation').click();
+ cy.get(minField).should('be.visible');
+
+ // Set an invalid state: minimum > maximum
+ setNumberInputValue(maxField, 5);
+ setNumberInputValue(minField, 10);
+
+ // Attempt to save — dialog should remain open
+ cy.get('.cq-dialog-submit').click();
+ cy.get('coral-dialog[open]').should('exist');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(fileInputDrop);
+ });
+ });
+});
diff --git a/ui.tests/test-module/specs/numberinput/numberinput.minmax.authoring.cy.js b/ui.tests/test-module/specs/numberinput/numberinput.minmax.authoring.cy.js
new file mode 100644
index 0000000000..3a5a28c9b7
--- /dev/null
+++ b/ui.tests/test-module/specs/numberinput/numberinput.minmax.authoring.cy.js
@@ -0,0 +1,89 @@
+/*******************************************************************************
+ * Copyright 2022 Adobe
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+
+const afConstants = require("../../libs/commons/formsConstants");
+const sitesSelectors = require("../../libs/commons/sitesSelectors");
+
+describe('Page - Authoring', function () {
+ const dropNumberInputInContainer = function () {
+ const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*",
+ responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']";
+ cy.selectLayer("Edit");
+ cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Number Input", afConstants.components.forms.resourceType.formnumberinput);
+ cy.get('body').click(0, 0);
+ }
+
+ context('Open Forms Editor', function () {
+ const pagePath = "/content/forms/af/core-components-it/blank",
+ numberInputEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/numberinput",
+ numberInputEditPathSelector = "[data-path='" + numberInputEditPath + "']",
+ numberInputDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.formnumberinput.split("/").pop(),
+ numberInputBlockBemSelector = '.cmp-adaptiveform-numberinput',
+ editDialogConfigurationSelector = "[data-action='CONFIGURE']";
+
+ beforeEach(function () {
+ cy.openAuthoring(pagePath);
+ });
+
+ it('shows inline error when minimum is set greater than maximum', function () {
+ dropNumberInputInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + numberInputEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+ cy.get(numberInputBlockBemSelector + '__editdialog').contains('Validation').click();
+
+ // Set maximum = 5, then minimum = 10 (invalid: min > max)
+ cy.get(numberInputBlockBemSelector + '__maximum').find('input').clear().type('5');
+ cy.focused().blur();
+ cy.get(numberInputBlockBemSelector + '__minimum').find('input').clear().type('10');
+ cy.focused().blur();
+
+ // Both fields should be marked invalid
+ cy.get(numberInputBlockBemSelector + '__minimum').should('have.attr', 'invalid');
+ cy.get(numberInputBlockBemSelector + '__maximum').should('have.attr', 'invalid');
+
+ // Fix by lowering minimum below maximum
+ cy.get(numberInputBlockBemSelector + '__minimum').find('input').clear().type('3');
+ cy.focused().blur();
+
+ // Both fields should no longer be invalid
+ cy.get(numberInputBlockBemSelector + '__minimum').should('not.have.attr', 'invalid');
+ cy.get(numberInputBlockBemSelector + '__maximum').should('not.have.attr', 'invalid');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(numberInputDrop);
+ });
+
+ it('blocks dialog save when minimum is greater than maximum', function () {
+ dropNumberInputInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + numberInputEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+ cy.get(numberInputBlockBemSelector + '__editdialog').contains('Validation').click();
+
+ // Set an invalid state: minimum > maximum
+ cy.get(numberInputBlockBemSelector + '__maximum').find('input').clear().type('5');
+ cy.focused().blur();
+ cy.get(numberInputBlockBemSelector + '__minimum').find('input').clear().type('10');
+ cy.focused().blur();
+
+ // Attempt to save — dialog should remain open
+ cy.get('.cq-dialog-submit').click();
+ cy.get('coral-dialog[open]').should('exist');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(numberInputDrop);
+ });
+ });
+});
diff --git a/ui.tests/test-module/specs/panelcontainer/panelcontainer.minmax.authoring.cy.js b/ui.tests/test-module/specs/panelcontainer/panelcontainer.minmax.authoring.cy.js
new file mode 100644
index 0000000000..ef8106fed2
--- /dev/null
+++ b/ui.tests/test-module/specs/panelcontainer/panelcontainer.minmax.authoring.cy.js
@@ -0,0 +1,98 @@
+/*******************************************************************************
+ * Copyright 2022 Adobe
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+
+const afConstants = require("../../libs/commons/formsConstants");
+const sitesSelectors = require("../../libs/commons/sitesSelectors");
+
+describe('Page - Authoring', function () {
+ const dropPanelContainerInContainer = function () {
+ const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*",
+ responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']";
+ cy.selectLayer("Edit");
+ cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Panel", afConstants.components.forms.resourceType.panelcontainer);
+ cy.get('body').click(0, 0);
+ };
+
+ context('Open Forms Editor', function () {
+ const pagePath = "/content/forms/af/core-components-it/blank",
+ panelContainerEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/panelcontainer",
+ panelContainerEditPathSelector = "[data-path='" + panelContainerEditPath + "']",
+ panelContainerDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.panelcontainer.split("/").pop(),
+ editDialogConfigurationSelector = "[data-action='CONFIGURE']",
+ minField = '.cmp-adaptiveform-panelcontainer__minOccur coral-numberinput',
+ maxField = '.cmp-adaptiveform-panelcontainer__maxOccur coral-numberinput';
+
+ beforeEach(function () {
+ cy.openAuthoring(pagePath);
+ });
+
+ it('shows inline error when minimum occurrences is set greater than maximum occurrences', function () {
+ dropPanelContainerInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + panelContainerEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+ cy.get('.cmp-adaptiveform-panelcontainer__editdialog').contains('Repeat Panel').click();
+
+ // Enable repeatability to unlock min/max occurrence fields
+ cy.get('.cmp-adaptiveform-panelcontainer__repeatable coral-switch').click();
+ cy.get(maxField).should('not.have.attr', 'disabled');
+
+ // Set maximum = 5, then minimum = 10 (invalid: min > max)
+ cy.get(maxField).find('input').clear().type('5');
+ cy.focused().blur();
+ cy.get(minField).find('input').clear().type('10');
+ cy.focused().blur();
+
+ // Both fields should be marked invalid
+ cy.get(minField).should('have.attr', 'invalid');
+ cy.get(maxField).should('have.attr', 'invalid');
+
+ // Fix by lowering minimum below maximum
+ cy.get(minField).find('input').clear().type('3');
+ cy.focused().blur();
+
+ // Both fields should no longer be invalid
+ cy.get(minField).should('not.have.attr', 'invalid');
+ cy.get(maxField).should('not.have.attr', 'invalid');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(panelContainerDrop);
+ });
+
+ it('blocks dialog save when minimum occurrences is greater than maximum occurrences', function () {
+ dropPanelContainerInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + panelContainerEditPathSelector);
+ cy.invokeEditableAction(editDialogConfigurationSelector);
+ cy.get('.cmp-adaptiveform-panelcontainer__editdialog').contains('Repeat Panel').click();
+
+ // Enable repeatability to unlock min/max occurrence fields
+ cy.get('.cmp-adaptiveform-panelcontainer__repeatable coral-switch').click();
+ cy.get(maxField).should('not.have.attr', 'disabled');
+
+ // Set an invalid state: minimum > maximum
+ cy.get(maxField).find('input').clear().type('5');
+ cy.focused().blur();
+ cy.get(minField).find('input').clear().type('10');
+ cy.focused().blur();
+
+ // Attempt to save — dialog should remain open
+ cy.get('.cq-dialog-submit').click();
+ cy.get('coral-dialog[open]').should('exist');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(panelContainerDrop);
+ });
+ });
+});
diff --git a/ui.tests/test-module/specs/textinput/textinput.minmax.authoring.cy.js b/ui.tests/test-module/specs/textinput/textinput.minmax.authoring.cy.js
new file mode 100644
index 0000000000..04ffd0efaa
--- /dev/null
+++ b/ui.tests/test-module/specs/textinput/textinput.minmax.authoring.cy.js
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022 Adobe Systems Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const sitesSelectors = require('../../libs/commons/sitesSelectors'),
+ afConstants = require('../../libs/commons/formsConstants');
+
+describe('Page - Authoring', function () {
+ const dropTextInputInContainer = function () {
+ const dataPath = "/content/forms/af/core-components-it/blank/jcr:content/guideContainer/*",
+ responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']";
+ cy.selectLayer("Edit");
+ cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Text Box", afConstants.components.forms.resourceType.formtextinput);
+ cy.get('body').click(0, 0);
+ }
+
+ context('Open Forms Editor', function () {
+ const pagePath = "/content/forms/af/core-components-it/blank",
+ textInputEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/textinput",
+ textInputEditPathSelector = "[data-path='" + textInputEditPath + "']",
+ textInputDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + afConstants.components.forms.resourceType.formtextinput.split("/").pop();
+
+ beforeEach(function () {
+ cy.openAuthoring(pagePath);
+ });
+
+ it('shows inline error when minLength is set greater than maxLength', function () {
+ dropTextInputInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + textInputEditPathSelector);
+ cy.invokeEditableAction("[data-action='CONFIGURE']");
+ cy.get('.cmp-adaptiveform-textinput__editdialog').contains('Validation').click({force: true});
+
+ // Set maxLength = 5, then minLength = 10 (invalid: min > max)
+ cy.get('.cmp-adaptiveform-textinput__maxlength coral-numberinput').find('input').clear().type('5');
+ cy.focused().blur();
+ cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').find('input').clear().type('10');
+ cy.focused().blur();
+
+ // Both fields should be marked invalid
+ cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').should('have.attr', 'invalid');
+ cy.get('.cmp-adaptiveform-textinput__maxlength coral-numberinput').should('have.attr', 'invalid');
+
+ // Fix by lowering minLength below maxLength
+ cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').find('input').clear().type('3');
+ cy.focused().blur();
+
+ // Both fields should no longer be invalid
+ cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').should('not.have.attr', 'invalid');
+ cy.get('.cmp-adaptiveform-textinput__maxlength coral-numberinput').should('not.have.attr', 'invalid');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(textInputDrop);
+ });
+
+ it('blocks dialog save when minLength is greater than maxLength', function () {
+ dropTextInputInContainer();
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + textInputEditPathSelector);
+ cy.invokeEditableAction("[data-action='CONFIGURE']");
+ cy.get('.cmp-adaptiveform-textinput__editdialog').contains('Validation').click({force: true});
+
+ // Set an invalid state: minLength > maxLength
+ cy.get('.cmp-adaptiveform-textinput__maxlength coral-numberinput').find('input').clear().type('5');
+ cy.focused().blur();
+ cy.get('.cmp-adaptiveform-textinput__minlength coral-numberinput').find('input').clear().type('10');
+ cy.focused().blur();
+
+ // Attempt to save — dialog should remain open
+ cy.get('.cq-dialog-submit').click();
+ cy.get('coral-dialog[open]').should('exist');
+
+ cy.get('.cq-dialog-cancel').should('be.visible').click();
+ cy.deleteComponentByPath(textInputDrop);
+ });
+ });
+});