diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java index bc6d1f7caa..04105c4c75 100644 --- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java @@ -56,6 +56,9 @@ private FormConstants() { /** The resource type for date picker v1 */ public static final String RT_FD_FORM_DATE_PICKER_V1 = RT_FD_FORM_PREFIX + "datepicker/v1/datepicker"; + /** The resource type for date picker v2 */ + public static final String RT_FD_FORM_DATE_PICKER_V2 = RT_FD_FORM_PREFIX + "datepicker/v2/datepicker"; + /** The resource type for number input v1 */ public static final String RT_FD_FORM_NUMBER_INPUT_V1 = RT_FD_FORM_PREFIX + "numberinput/v1/numberinput"; diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/DatePickerImpl.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/DatePickerImpl.java index 205cc2fead..69ed63840d 100644 --- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/DatePickerImpl.java +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/DatePickerImpl.java @@ -44,7 +44,10 @@ adaptables = { SlingHttpServletRequest.class, Resource.class }, adapters = { DatePicker.class, ComponentExporter.class }, - resourceType = { FormConstants.RT_FD_FORM_DATE_PICKER_V1 }) + resourceType = { + FormConstants.RT_FD_FORM_DATE_PICKER_V1, + FormConstants.RT_FD_FORM_DATE_PICKER_V2 + }) @Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION) public class DatePickerImpl extends AbstractFieldImpl implements DatePicker { diff --git a/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/clientlibs/custom-forms-components-runtime-all/.content.xml b/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/clientlibs/custom-forms-components-runtime-all/.content.xml index 1b446738e2..3859abc906 100644 --- a/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/clientlibs/custom-forms-components-runtime-all/.content.xml +++ b/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/clientlibs/custom-forms-components-runtime-all/.content.xml @@ -5,4 +5,4 @@ cssProcessor="[default:none,min:none]" jsProcessor="[default:none,min:none]" categories="[core.forms.components.it.runtime.all]" - embed="[core.forms.components.runtime.base,core.forms.components.it.container.v1.runtime,core.forms.components.datePicker.v1.runtime,core.forms.components.textinput.v1.runtime,core.forms.components.numberinput.v1.runtime,core.forms.components.panelcontainer.v1.runtime,core.forms.components.radiobutton.v1.runtime,core.forms.components.text.v1.runtime,core.forms.components.checkboxgroup.v1.runtime,core.forms.components.button.v1.runtime,core.forms.components.image.v1.runtime,core.forms.components.dropdown.v1.runtime,core.forms.components.fileinput.v2.runtime,core.forms.components.accordion.v1.runtime,core.forms.components.tabs.v1.runtime,core.forms.components.wizard.v1.runtime,core.forms.components.verticaltabs.v1.runtime,core.forms.components.recaptcha.v1.runtime,core.forms.components.checkbox.v1.runtime,core.forms.components.fragment.v1.runtime,core.forms.components.switch.v1.runtime,core.forms.components.termsandconditions.v1.runtime, core.forms.components.it.textinput.v1.runtime, core.forms.components.hcaptcha.v1.runtime, core.forms.components.turnstile.v1.runtime]"/> + embed="[core.forms.components.runtime.base,core.forms.components.it.container.v1.runtime,core.forms.components.datePicker.v1.runtime,core.forms.components.datePicker.v2.runtime,core.forms.components.textinput.v1.runtime,core.forms.components.numberinput.v1.runtime,core.forms.components.panelcontainer.v1.runtime,core.forms.components.radiobutton.v1.runtime,core.forms.components.text.v1.runtime,core.forms.components.checkboxgroup.v1.runtime,core.forms.components.button.v1.runtime,core.forms.components.image.v1.runtime,core.forms.components.dropdown.v1.runtime,core.forms.components.fileinput.v2.runtime,core.forms.components.accordion.v1.runtime,core.forms.components.tabs.v1.runtime,core.forms.components.wizard.v1.runtime,core.forms.components.verticaltabs.v1.runtime,core.forms.components.recaptcha.v1.runtime,core.forms.components.checkbox.v1.runtime,core.forms.components.fragment.v1.runtime,core.forms.components.switch.v1.runtime,core.forms.components.termsandconditions.v1.runtime, core.forms.components.it.textinput.v1.runtime, core.forms.components.hcaptcha.v1.runtime, core.forms.components.turnstile.v1.runtime]"/> diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/resources/i18n/en.json b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/resources/i18n/en.json index 5587d80b82..d4312d0e34 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/resources/i18n/en.json +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/resources/i18n/en.json @@ -4,6 +4,12 @@ "FileNameInvalid" : "Do not attach files where filename starts with (.), contains \\ / : * ? \" < > | ; % $, or is a reserved keyword like nul, prn, con, lpt, or com.", "FileMimeTypeInvalid" : "File(s) ${0} are unsupported file types", "InternalFormSubmissionError" : "Encountered an internal error while submitting the form.", + "previousMonth" : "Previous month", + "nextMonth" : "Next month", + "previousYear" : "Previous year", + "nextYear" : "Next year", + "previousSetOfYears" : "Previous set of years", + "nextSetOfYears" : "Next set of years", "FileSizeZero" : "The uploaded file(s) ${0} are empty. Make sure you're uploading file(s) with content.", "type" : "Please enter a valid value.", "required" : "Please fill in this field.", @@ -22,6 +28,7 @@ "accept" : "The specified file type not supported.", "defaultError" : "There is an error in the field", "clearText" : "Clear", + "openCalendarText" : "Open calendar", "calendarSymbols" : { "monthNames" : ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], "abbrmonthNames" : ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/site/js/datepickerwidget.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/site/js/datepickerwidget.js index 0300afe7dd..fcf743359f 100644 --- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/site/js/datepickerwidget.js +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v1/datepicker/clientlibs/site/js/datepickerwidget.js @@ -144,9 +144,9 @@ if (typeof window.DatePickerWidget === 'undefined') { '
' + '
' + '' + - '
' + - '
' + - '
'; + '
' + + '
' + + '
'; constructor(view, widget, model) { this.#model = model; @@ -232,6 +232,7 @@ if (typeof window.DatePickerWidget === 'undefined') { } }); this.caption = this.#dp.getElementsByClassName("dp-caption")[0]; + this.caption.id = "dp-caption"; this.caption.addEventListener("click", function (evnt) { if (self.view && self.#curInstance) { @@ -847,12 +848,14 @@ if (typeof window.DatePickerWidget === 'undefined') { let rowEl = document.createElement("ul"); rowEl.removeAttribute("aria-label"); rowEl.classList.toggle("header", isHeader); + rowEl.setAttribute("role", "row"); $row = $view.appendChild(rowEl); } items = $row.getElementsByTagName("li").length; while (items++ < rowArray.length) { let listItemEl = document.createElement("li"); listItemEl.id = "li-" + elementAt(rowNum, items).gridId; + listItemEl.setAttribute("role", isHeader ? "columnheader" : "gridcell"); $tmp = $row.appendChild(listItemEl); if (!isHeader) { let cb = function (evnt) { diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/.content.xml new file mode 100644 index 0000000000..f75e671c00 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/.content.xml @@ -0,0 +1,5 @@ + + + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/.content.xml new file mode 100644 index 0000000000..8d9a662fbb --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/.content.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/README.md b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/README.md new file mode 100644 index 0000000000..22c6ded4be --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/README.md @@ -0,0 +1,100 @@ + +Adaptive Form Date Picker (v2) +==== +Adaptive Form Date Picker field component written in HTL. + +## Features + +* Provides the following type of input: + * date +* Allows replacing this component with other component (as mentioned below). + +### Use Object +The Form Date Picker component uses the `com.adobe.cq.forms.core.components.models.form.DatePicker` Sling Model for its Use-object. + +### Edit Dialog Properties +The following properties are written to JCR for this Form Date component and are expected to be available as `Resource` properties: + +1. `./jcr:title` - defines the label to use for this field +2. `./hideTitle` - if set to `true`, the label of this field will be hidden +3. `./name` - defines the name of the field, which will be submitted with the form data +4. `./default` - defines the default value of the field +5. `./description` - defines a help message that can be rendered in the field as a hint for the user +6. `./required` - if set to `true`, this field will be marked as required, not allowing the form to be submitted until the field has a value +7. `./requiredMessage` - defines the message displayed as tooltip when submitting the form if the value is left empty +8. `./readOnly` - if set to `true`, the filed will be read only +9. `./dataFormat` - defines the format in which the value exported/submitted +10. `./editFormat` - defines the format in which the value will be edited by the user +11. `./minimumDate` - define the minimum date input allowed for the field +12. `./maximumDate` - define the maximum date input allowed for the field +13. `./displayFormat` - define the template for display pattern (Reference can be found [here](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns)). + +## Client Libraries +The component provides a `core.forms.components.datePicker.v2.runtime` client library category that contains the Javascript runtime for the component. +It should be added to a relevant site client library using the `embed` property. + +## BEM Description +``` +BLOCK cmp-adaptiveform-datepicker + ELEMENT cmp-adaptiveform-datepicker__label + ELEMENT cmp-adaptiveform-datepicker__label-container + ELEMENT cmp-adaptiveform-datepicker__widget + ELEMENT cmp-adaptiveform-datepicker__questionmark + ELEMENT cmp-adaptiveform-datepicker__shortdescription + ELEMENT cmp-adaptiveform-datepicker__longdescription + ELEMENT cmp-adaptiveform-datepicker__errormessage +``` + +### Note +By placing the class names `cmp-adaptiveform-datepicker__label` and `cmp-adaptiveform-datepicker__questionmark` within the `cmp-adaptiveform-datepicker__label-container` class, you create a logical grouping of the label and question mark elements. This approach simplifies the process of maintaining a consistent styling for both elements. + +## JavaScript Data Attribute Bindings + +The following attributes must be added for the initialization of the date-picker component in the form view: + 1. `data-cmp-is="adaptiveFormDatePicker"` + 2. `data-cmp-adaptiveformcontainer-path="${formstructparser.formContainerPath}"` + + +The following are optional attributes that can be added to the component in the form view: +1. `data-cmp-valid` having a boolean value to indicate whether the field is currently valid or not +2. `data-cmp-required` having a boolean value to indicate whether the field is currently required or not +3. `data-cmp-readonly` having a boolean value to indicate whether the field is currently readonly or not +4. `data-cmp-active` having a boolean value to indicate whether the field is currently active or not +5. `data-cmp-visible` having a boolean value to indicate whether the field is currently visible or not +6. `data-cmp-enabled` having a boolean value to indicate whether the field is currently enabled or not + +## Replace feature: +We support replace feature that allows replacing Reset Button component to any of the below components: + +* Button +* Email Input +* Number Input +* Reset Button +* Submit Button +* Telephone Input +* Text Box +* Text Input + + +## Information +* **Vendor**: Adobe +* **Version**: v2 +* **Compatibility**: Cloud +* **Status**: production-ready + + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/.content.xml new file mode 100644 index 0000000000..491392d539 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/.content.xml @@ -0,0 +1,3 @@ + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/.content.xml new file mode 100644 index 0000000000..5a6d805da1 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/.content.xml @@ -0,0 +1,6 @@ + + diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/css.txt b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/css.txt new file mode 100644 index 0000000000..130f10b2e3 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/css.txt @@ -0,0 +1,19 @@ +############################################################################### +# 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. +############################################################################### + +#base=css +datepickerview.css +datepickerwidget.css diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/css/datepickerview.css b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/css/datepickerview.css new file mode 100644 index 0000000000..17de33f661 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/css/datepickerview.css @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +.cmp-adaptiveform-datepicker { + +} + +.cmp-adaptiveform-datepicker__widget { + +} + +.cmp-adaptiveform-datepicker__label { + +} +.cmp-adaptiveform-datepicker__label-container{ + +} + +.cmp-adaptiveform-datepicker__longdescription { + +} + +.cmp-adaptiveform-datepicker__shortdescription { + +} + +.cmp-adaptiveform-datepicker__questionmark { + +} + +.cmp-adaptiveform-datepicker__questionmark { + +} \ No newline at end of file diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/css/datepickerwidget.css b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/css/datepickerwidget.css new file mode 100644 index 0000000000..2e14ed020a --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/css/datepickerwidget.css @@ -0,0 +1,168 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ + +.datetimepicker { + border: none; + background-color: #FFF; + display: none; + position: absolute; + cursor: default; + z-index: 100; + outline: solid #CCCCCC 2px; + flex-direction: column; + width: 433px; +} + +.datetimepicker .dp-clear { + overflow: auto; + background-color: #F5F5F5; + text-align: center; +} + +.datetimepicker .dp-clear a { + cursor: pointer; + height: 40px; + line-height: 40px; + padding: 0px 5px 0px 5px; + text-align: center; + display: inline-block; + font-size: 0.875rem; + color: #6d6d6d; +} + +.datetimepicker-notouch .dp-close a:hover { + color: #c8bbff; +} + +.datetimepicker .dp-header { + height: 40px; + line-height: 40px; + color: #555555; + margin-bottom: 5px; + background-color: #E6E6E6; + display: flex; + justify-content: space-between; +} + +.datetimepicker .dp-header .dp-leftnav, +.datetimepicker .dp-header .dp-rightnav, +.datetimepicker .dp-header .dp-caption { + float: left; + text-align: center; + cursor: pointer; + height: 40px; +} + +.datetimepicker-notouch .dp-header .dp-caption:not(.disabled):hover { + color: #969696; +} + +.datetimepicker .dp-header .dp-rightnav { + float: right; + background: url("/etc.clientlibs/core/fd/components/form/datepicker/v1/datepicker/clientlibs/site/resources/rightnav.png") no-repeat center center; + width: 40px; +} + +.datetimepicker .dp-header .dp-leftnav { + width: 40px; + background: url("/etc.clientlibs/core/fd/components/form/datepicker/v1/datepicker/clientlibs/site/resources/leftnav.png") no-repeat center center; +} + +.datetimepicker .dp-header .dp-rightnav:hover { + background: url("/etc.clientlibs/core/fd/components/form/datepicker/v1/datepicker/clientlibs/site/resources/rightnav_hover.png") no-repeat center center; +} + +.datetimepicker .dp-header .dp-leftnav:hover { + background: url("/etc.clientlibs/core/fd/components/form/datepicker/v1/datepicker/clientlibs/site/resources/leftnav_hover.png") no-repeat center center; +} + +.datetimepicker .view { + display: none; +} + +.datetimepicker .view ul { + display: flex; + justify-content: space-around; + list-style: none; + margin: 0; + padding: 5px; + overflow: hidden; + box-sizing: border-box; + -moz-box-sizing: border-box; +} + +.datetimepicker .view ul li { + float: left; + padding: 5px; + text-align: center; + border: none; + box-sizing: border-box; + -moz-box-sizing: border-box; + color: #666666; + min-width:40px; +} + +.datetimepicker .view ul.header li { + color: #555555; +} + +.datetimepicker .view ul:not(.header) li:not(.disabled) { + cursor: pointer; +} + +.datetimepicker .view ul.header { + color: #000; + background-color: #FFF; + border-bottom: #E6E6E6 1px solid; + display: flex; + justify-content: space-around; +} + +.datetimepicker-notouch .view ul:not(.header) li:not(.disabled):hover { + color: black; + background-color: #E6E6E6; + opacity: 0.5; +} + +.datetimepicker .view ul li.disabled { + color: #CCCCCC; +} + +.datetimepicker .view ul li.dp-selected { + outline: none; + background-color: #666666; + color: #FFFFFF; + opacity: 1.0; +} + +.datetimepicker .view ul li.dp-focus { + border: 1px dashed black; +} + +.cmp-adaptiveform-datepicker__calendar-icon { + position: absolute; + top: 41px; + right: 30px; + z-index: 10; + background: url("/etc.clientlibs/core/fd/components/form/datepicker/v1/datepicker/clientlibs/site/resources/calendar.png") no-repeat center center; + background-size: contain; + width: 24px; + height: 24px; +} + +.datefieldwidget.widgetreadonly .cmp-adaptiveform-datepicker__calendar-icon { + display: none; +} \ No newline at end of file diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js.txt b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js.txt new file mode 100644 index 0000000000..c307501a59 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js.txt @@ -0,0 +1,20 @@ +############################################################################### +# 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. +############################################################################### + +#base=js +cache.js +datepickerview.js +datepickerwidget.js diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js/cache.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js/cache.js new file mode 100644 index 0000000000..97f9bd1e24 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js/cache.js @@ -0,0 +1,33 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +(function() { + + "use strict"; + class Cache { + + store = {}; + + put(el, key, value) { + this.store[el.id ] = {}; + this.store[el.id][key] = value; + } + + get(el, key) { + return this.store[el.id][key]; + } + } + window.afCache = new Cache(); +})(); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js/datepickerview.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js/datepickerview.js new file mode 100644 index 0000000000..27d30b1ac9 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js/datepickerview.js @@ -0,0 +1,140 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +(function() { + + "use strict"; + class DatePicker extends FormView.FormFieldBase { + + static NS = FormView.Constants.NS; + static IS = "adaptiveFormDatePicker"; + static bemBlock = 'cmp-adaptiveform-datepicker'; + static selectors = { + self: "[data-" + this.NS + '-is="' + this.IS + '"]', + widget: `.${DatePicker.bemBlock}__widget`, + label: `.${DatePicker.bemBlock}__label`, + description: `.${DatePicker.bemBlock}__longdescription`, + qm: `.${DatePicker.bemBlock}__questionmark`, + errorDiv: `.${DatePicker.bemBlock}__errormessage`, + tooltipDiv: `.${DatePicker.bemBlock}__shortdescription` + }; + + constructor(params) { + super(params); + } + + getWidget() { + return this.element.querySelector(DatePicker.selectors.widget); + } + + getDescription() { + return this.element.querySelector(DatePicker.selectors.description); + } + + getLabel() { + return this.element.querySelector(DatePicker.selectors.label); + } + + getErrorDiv() { + return this.element.querySelector(DatePicker.selectors.errorDiv); + } + + getQuestionMarkDiv() { + return this.element.querySelector(DatePicker.selectors.qm); + } + + getTooltipDiv() { + return this.element.querySelector(DatePicker.selectors.tooltipDiv); + } + + updateValue(value) { + if (this.widgetObject) { + if (this.isActive()) { + // Replacing undefined / null with empty string on reset + this.widgetObject.setValue(value || ''); + } else { + // Replacing undefined / null with empty string on reset + this.widgetObject.setDisplayValue(value || ''); + } + } else { + // Replacing undefined / null with empty string on reset + super.updateValue(value || ''); + } + } + + updateReadOnly(readOnly, state) { + super.updateReadOnly(readOnly, state); + if (this.widgetObject != null) { + this.widgetObject.markAsReadOnly(readOnly); + } + } + + + setModel(model) { + super.setModel(model); + if (!this.#noFormats()) { + if (this.widgetObject == null) { + this.widgetObject = new DatePickerWidget(this, this.getWidget(), model); + } + if (this.isActive()) { + this.widgetObject.setValue(model.value); + } else { + this.widgetObject.setDisplayValue(model.value); + } + this.widgetObject.addEventListener('blur', (e) => { + this.setModelValue(this.widgetObject.getValue()) + //setDisplayValue is required for cases where value remains same while focussing in and out. + this.widgetObject.setDisplayValue(this._model.value); + this.widgetObject.setCalendarWidgetValue(this._model.value); + this.setInactive(); + this.triggerExit(); + }, this.getWidget()); + this.widgetObject.addEventListener('focus', (e) => { + this.widgetObject.setValue(e.target.value); + this.setActive(); + this.triggerEnter(); + }, this.getWidget()); + this.widgetObject.addEventListener('input', (e) => { + if( e.target.value === '') { + // clear the value if user manually empties the value in date input box + this.setModelValue(""); + } + }, this.getWidget()); + } else { + if (this.widget.value !== '') { + this.setModelValue(this.widget.value); + } + this.widget.addEventListener('blur', (e) => { + this.setModelValue(e.target.value); + this.setInactive(); + this.triggerExit(); + }); + this.widget.addEventListener('focus', (e) => { + this.setActive(); + this.triggerEnter(); + }); + } + } + + #noFormats() { + return (this._model.editFormat == null || this._model.editFormat === 'date|short') && + (this._model.displayFormat == null || this._model.displayFormat === 'date|short') && (this._model.displayValueExpression == null) + } + } + + FormView.Utils.setupField(({element, formContainer}) => { + return new DatePicker({element, formContainer}) + }, DatePicker.selectors.self); +})(); diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js/datepickerwidget.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js/datepickerwidget.js new file mode 100644 index 0000000000..1a3633a3b6 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/clientlibs/site/js/datepickerwidget.js @@ -0,0 +1,1345 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ + +if (typeof window.DatePickerWidget === 'undefined') { + + window.DatePickerWidget = class { + + #widget = null; + #model = null // passed by reference + #options = null + + #lang = "en"; + + #dp = null; + #curInstance = null; + #calendarIcon = null; + #documentTouchListener = null; + static #visible = false; + static #clickedWindow; + + /** default configuration options + * + * yearsPerView: number of years to show in the yearset view + * + * width: with of the widget + * + * viewHeight: Height of the month,year and yearset view. This doesn't include + * the height of the header + * + * locale: locale information for the locale in which to show the datepicker which comprises of + * days: day names to display in the monthview + * months: month names to display in the yearview + * zero: string representation of zero in the locale. Numbers will be + * displayed in that locale only + * clearText: Text to display for the reset button + * name: name of the locale + * + * format: input format for the datepicker (not implemented) + * + * pickerType: type of the datetimepicker (date, datetime and time) + * + * positioning: element around which datepicker will be displayed. if null then it + * will be displayed around the input element + * + * showCalendarIcon: to show the Calendar on the right of the text field or not + */ + #defaultOptions = { + yearsPerView: 16, + width: 340, + viewHeight: 248, + locale: { + days: ["S","M","T","W","T","F","S"], + months: ["January","February","March","April","May","June","July","August","September","October","November","December"], + zero: "0", + clearText: "Clear", + openCalendarText: "Open calendar", + name: "en_US" + }, + format: "YYYY-MM-DD", + pickerType: "date", + positioning: null, + showCalendarIcon: true + } + + #dates = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + /* + * Actions to perform when clicked on the datepicker buttons + * for different views + * caption: view to show when clicked on caption + * (Year/YearSet/Month/null) null means don't change the view + * li: view to show when clicked on date, month or year element + * upDown: add(up key) or subtract(down key) current date (for monthview), + * month(Year View) or year(YearSetView) with the number provided + * key: identifies the key that needs to be changed for that view + */ + #viewAction = { + Month: { + caption: 'Year', + li: null, + key: "day", + upDown: 7 + }, + Year: { + caption: "Yearset", + li: "Month", + key: "month", + upDown: 3 + }, + Yearset: { + caption: null, + li: "Year", + key: "year", + upDown: 4 + } + } + + selectedDay = 0; + selectedMonth = 0; + selectedYear = 0; + currentDay = 0; + currentMonth = 0; + currentYear = 0; + //view="Month"; + #touchSupported = !!("ontouchstart" in window || window.DocumentTouch + && document instanceof DocumentTouch); + #defaultView = "Month"; + #keysEnabled = false; + #focusedOnLi = false; + #keyboardAccessibility = true; + #scriptFocus; + + /* template for the clear Button */ + #clearButtonTemplate = '
' + + '' + + '
'; + + /* template for the calendar + * header contains the navigation icons (left and right arrows) + * and the current caption (which can be date, year or month) + * + * monthview displays the grid for showing the dates for a particular + * month + * + * yearview displays all the months of that year + * + * yearsetview displays a grid of 16 years. This can be configured + * through the option: yearsPerView + * + */ + #calendarTemplate = '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + + constructor(view, widget, model) { + this.#model = model; + this.#lang = view.getModel().lang; + let editValFn = () => { + return model.editValue; + }; + let displayValueFn = () => { + return model.displayValue; + }; + this.#localizeDateElements(this.#defaultOptions, this.#lang); + this.#options = Object.assign( + {editValue: editValFn, displayValue: displayValueFn}, + this.#defaultOptions, this.#model._jsonModel); + + this.#initialiseCalenderElement(); + + //update widget markup to have input type=text element. + let newDatePickerElement = widget.cloneNode(true); + newDatePickerElement.setAttribute("type", "text"); + newDatePickerElement.id = model.id + '-widget'; + + widget.parentNode.replaceChild(newDatePickerElement, widget); + this.#widget = view.getWidget(); + this.#attachField(view.getWidget(), this.#options); + } + + #initialiseCalenderElement() { + let self = this, + html = ""; + if (this.#options.pickerType.match(/date/)) { + html += this.#calendarTemplate; + } + html += this.#clearButtonTemplate; + + if (!this.#dp) { + this.#dp = document.createElement("div"); + this.#dp.classList.add("datetimepicker", "datePickerTarget"); + this.#dp.innerHTML = html; + //Always inserting it in body + document.body.appendChild(this.#dp); + } + // attach click event on entire datePicker popup + this.#dp.addEventListener("click", + function (evnt) { + //focus only if the device doesn't support touch + // input otherwise on screen keyboard will popup + if (!self.#touchSupported) { + if (self.#curInstance) { + self.#curInstance.$field.focus(); + } + } + }, false); + "touchstart mousedown".split(" ").forEach(function (e) { + self.#dp.addEventListener(e, function (evt) { + self.#checkWindowClicked(evt) + }, false); + }); + + this.$month = this.#dp.getElementsByClassName("dp-monthview")[0]; + this.$year = this.#dp.getElementsByClassName("dp-yearview")[0]; + this.$yearset = this.#dp.getElementsByClassName("dp-yearsetview")[0]; + + this.clear = this.#dp.getElementsByClassName( + "dp-clear")[0].getElementsByTagName("a")[0]; + this.clear.addEventListener("click", function (evnt) { + if (self.view && self.#curInstance) { + self.#clearDate(self.view); + } + }); + this.prevNavWidthBtn = this.#dp.getElementsByClassName("dp-leftnav")[0]; + this.prevNavWidthBtn.setAttribute("role", "button"); + this.prevNavWidthBtn.addEventListener("click", + function (evnt) { + if (self.view && self.#curInstance) { + self.#adjustDate(-1, self.view, false) + } + }); + this.nextNavWidthBtn = this.#dp.getElementsByClassName("dp-rightnav")[0]; + this.nextNavWidthBtn.setAttribute("role", "button"); + this.nextNavWidthBtn.addEventListener("click", + function (evnt) { + if (self.view && self.#curInstance) { + self.#adjustDate(1, self.view, false) + } + }); + this.caption = this.#dp.getElementsByClassName("dp-caption")[0]; + this.caption.setAttribute("role", "button"); + this.caption.id = "dp-caption"; + this.caption.addEventListener("click", + function (evnt) { + if (self.view && self.#curInstance) { + if (!self.caption.classList.contains("disabled")) { + self.#layout(self.#viewAction[self.view].caption); + } + } + }); + if (this.#keyboardAccessibility) { + [this.prevNavWidthBtn, this.caption, this.nextNavWidthBtn, + this.clear].forEach(function (elem, i) { + elem.setAttribute("tabIndex", i + 1); + }); + } + + } + + /* + * attaches the date picker to the field. This is a one time operation + * First creates a configuration object and stores it in the field data attributes + * then attaches event handlers on click, focus (to show the picker) and blur (to hide) events + */ + #attachField(widget, options) { + let inst = this.#newInst(widget, options), + self = this, + activateField = function (evnt) { + if (!self.#curInstance) { + self.#activateField(evnt); + } + + if (self.#options.showCalendarIcon) { + if (evnt.type === self.#getEvent()) { + if (self._iconClicked) { + self._iconClicked = false; + if (DatePickerWidget.#visible) { + self.#hide(); // hide the calendar popup if visible when calendar icon is clicked + self.#curInstance.$field.focus(); // bring back focus in field + } else { + self.#show(); //// show the calendar popup if not visible when calendar icon is clicked + } + } + } + } else { + /*show the popup only if + 1. click/touch event + 2. focus event in case of non-touch devices and focus is not done using script + */ + if (evnt.type === self.#getEvent() || (evnt.type === "focus" + && !self.#touchSupported && !self.#scriptFocus)) { + self.#show(evnt); + } + } + + DatePickerWidget.#clickedWindow = true; + self.#scriptFocus = false; + }, + deactivateField = function (evnt) { + //deactivate only if clicked outside window + // on touch devices only keyboard or calendar should be active at once, touching keyboard should deactivate calendar + if ((DatePickerWidget.#clickedWindow && !self.#focusedOnLi) + && (self.#options.showCalendarIcon || !self.#touchSupported)) { + self.#hide(); + self.#deactivateField(); + DatePickerWidget.#clickedWindow = true; + } + }; + + window.afCache.put(widget, "datetimepicker", inst); + + widget.addEventListener(this.#getEvent(), activateField); + widget.onfocus = activateField; + widget.onblur = deactivateField; + + if (options.showCalendarIcon) { + let existingCalendarIcons = widget.parentNode.querySelectorAll('.cmp-adaptiveform-datepicker__calendar-icon'); + existingCalendarIcons.forEach(icon => icon.remove()); + + let calendarIcon = document.createElement("div"); + calendarIcon.classList.add("cmp-adaptiveform-datepicker__calendar-icon"); + + widget.parentNode.insertBefore(calendarIcon, widget.nextSibling); + + if (this.#keyboardAccessibility) { + calendarIcon.setAttribute("tabindex", 0); + } + // Accessibility attributes for calendar icon button + calendarIcon.setAttribute("role", "button"); + calendarIcon.setAttribute("aria-label", this.#options?.locale?.openCalendarText || "Open calendar"); + calendarIcon.addEventListener(this.#getEvent(), function (evnt) { + self._iconClicked = true; + widget.click(); + }); + calendarIcon.addEventListener("keydown", function (event) { + if (event.keyCode === 32 || event.keyCode === 13) { + event.preventDefault(); + event.stopPropagation(); + self._iconClicked = true; + widget.click(); + } + }); + this.#calendarIcon = calendarIcon; + if (options.readOnly) { + this.markAsReadOnly(true) + } + } + } + + markAsReadOnly(readonly) { + if (readonly) { + this.#calendarIcon.style.display = "none"; + } else { + this.#calendarIcon.style.display = ""; + } + } + + #newInst(widget, options) { + return { + $field: widget, + locale: options.locale, + positioning: options.positioning || widget, + access: options.access, + selectedDate: options.value, + editValue: options.editValue, + displayValue: options.displayValue, + minValidDate: options.minimum, + maxValidDate: options.maximum, + exclMinDate: options.exclusiveMinimum, + exclMaxDate: options.exclusiveMaximum + } + } + + /* + * To check where the click happened, if happened outside the datepicker + * then hide the picker. This is checked whether any ancestor of clicked target + * has a class datePickerTarget. This class is added to the attached element as well + */ + #checkWindowClicked(evnt) { + let self = this; + if (self.#curInstance) { + // datepickerTarget class depicts that the component is a part of the Date Field + // and on click of that class, we should not hide the datepicker or fire exit events. + if (!evnt.target.closest(".datePickerTarget")) { + //non-touch devices do not deactivate on blur. Hence needs to be done here + if (self.#touchSupported) { + self.#hide(); + //clicking outside a field doesn't blur the field in IPad. Doing it by script + self.#curInstance.$field.blur(); + self.#deactivateField(); + } else { + DatePickerWidget.#clickedWindow = true; + } + } else { + DatePickerWidget.#clickedWindow = false; + } + } + } + + /* + * handling of key strokes. All the key strokes prevent the default browser action + * unless specified otherwise + * tab: set focus on calendar icon when dateinput field is active, navigates through date picker buttons when datepicker is open, + * otherwise perform default browser action + * escape: hides the datepicker + * down arrow key: navigate the picker downwards by the number specified in actionView.upDown of the current View + * up arrow key: navigate the picker upwards by the number specified in actionView.upDown of the current View + * left arrow key: navigate the picker one unit of that view backward + * right arrow key: navigate the picker one unit of that view forward + * shift + up: perform the action that happens on clicking the caption (as specified in actionView.caption) + * shift + left: perform the action that happens on clicking the left navigation button + * shift + right: perform the action that happens on clicking the right navigation button + * space/enter: triggers the click event for the current focused element from datepicker/ opens datepicker when calendar icon is focused. + */ + #hotKeys(evnt) { + var handled = false, date; + switch (evnt.keyCode) { + case 9: //tab + // CQ-4239352 : Setting clickedWindow property to true on tabbing so that deactivateField logic gets executed + // When clicking on "x" on input field in in IE and when selecting the content and releasing the mouse select outside the field + // the click event is not trigerred on the field and hence activateField is not executed, so clickedWindow remains as false + DatePickerWidget.#clickedWindow = true; + handled = false; + break; + case 27://escape + if (DatePickerWidget.#visible) { + this.#hide(); + this.#curInstance.$field.focus(); + this.#deactivateField(); + handled = true; + } + break; + case 32: //space + case 13: // enter + if (evnt.target.classList.contains("cmp-adaptiveform-datepicker__calendar-icon")) { + if (!DatePickerWidget.#visible) { + this.#show(); + handled = true; + } else { + this.$focusedDate.classList.add("dp-focus"); + handled = true; + } + } + break; + case 40: //down arrow key + if (!DatePickerWidget.#visible) { + this.#show(); + return; + } + this.$focusedDate.classList.add("dp-focus"); + break; + } + + if (DatePickerWidget.#visible && this.#keysEnabled) { + var v = this.#viewAction[this.view].key, + updown = this.#viewAction[this.view].upDown; + switch (evnt.keyCode) { + case 9: // tab + if (evnt.shiftKey) { + if (evnt.target.classList.contains("dp-leftnav") + || evnt.target.classList.contains("dp-focus")) { + this.#hide(); + this.#curInstance.$field.focus(); + handled = true; + } else { + handled = false; + } + } else { + const buttonTabindex = evnt.target.getAttribute("tabindex"); + if (buttonTabindex === '0') { + if (evnt.target.tagName.toLocaleLowerCase() === "input") { + this.#hide(); + handled = false; + } else { + this.prevNavWidthBtn.focus(); + handled = true; + } + } else if (buttonTabindex === '4') { + this.#hide(); + this.#curInstance.$field.focus(); + handled = true; + } else { + handled = false; + } + } + break; + case 32: //select on space + case 13: // select on enter + this.hotKeyPressed = true; + this.#focusedOnLi = false; + if (!this.#focusedOnLi) { + evnt.target.click(); + } else { + if (this.$focusedDate) { + this.$focusedDate.click() //todo + } + } + this.hotKeyPressed = false; + handled = true; + break; + case 37: //left arrow key + if (evnt.shiftKey) { + this.prevNavWidthBtn.click(); + } else { + this.#adjustDate(-1, v, true); + } + handled = true; + break; + case 38: //up arrow key + if (evnt.shiftKey) { + this.caption.click(); + } else { + this.#adjustDate(-updown, v, true); + } + handled = true; + break; + case 39: //right arrow key + if (evnt.shiftKey) { + this.nextNavWidthBtn.click(); + } else { + this.#adjustDate(+1, v, true); + } + handled = true; + break; + case 40: //down arrow key + this.#adjustDate(updown, v, true); + handled = true; + break; + default: + } + } + if (handled) { + evnt.preventDefault(); + } + } + + /* + * show the datepicker. //TODO: migrate this to vanilla js + */ + #show() { + this.#options.locale = this.#curInstance.locale; + if (!DatePickerWidget.#visible) { + let date = new Date(), + validDate, + parsedDate; + if (this.#curInstance.selectedDate) { + validDate = this.#curInstance.selectedDate; + } else if (this.#model.valid) { + validDate = this.#model.value; + } + if (validDate) { + parsedDate = validDate ? new Date(validDate) : null; + date = (parsedDate == null || parsedDate == 'Invalid Date') ? date : parsedDate; + } + const timezoneOffset = date.getTimezoneOffset(); + date.setMinutes(date.getMinutes() + timezoneOffset); + this.selectedDay = this.currentDay = date.getDate(); + this.selectedMonth = this.currentMonth = date.getMonth(); + this.selectedYear = this.currentYear = date.getFullYear(); + this.maxValidDate = this.#options.maximum ? new Date( + this.#curInstance.maxValidDate) : null; + this.minValidDate = this.#options.minimum ? new Date( + this.#curInstance.minValidDate) : null; + this.exclMaxDate = this.#curInstance.exclMaxDate; + this.exclMinDate = this.#curInstance.exclMinDate; + this.#dp.getElementsByClassName("dp-clear")[0].getElementsByTagName( + "a")[0].innerText = this.#options.locale.clearText; + this.#layout(this.#defaultView); + this.#dp.style.display = "flex"; + this.#focusedOnLi = false; + DatePickerWidget.#visible = true; + this.#position(); + // Add document touch listener for mobile to close datepicker when tapping outside + if (this.#touchSupported && !this.#documentTouchListener) { + this.#documentTouchListener = (evt) => { + if (DatePickerWidget.#visible && this.#curInstance) { + this.#checkWindowClicked(evt); + } + }; + document.addEventListener("touchstart", this.#documentTouchListener, false); + } + if (this.#options.showCalendarIcon) { + this.#curInstance.$field.setAttribute('readonly', true); // when the datepicker is active, deactivate the field + } + } + + // Disabling the focus on ipad due to a bug where value of + // date picker is not being set + // Removing this code will only hamper one use case + // where on ipad if you click on the calander then + // the field becomes read only so + // there is no indication where the current focus is + // And if you remove this foucs code all together + // then what happens is that on desktop MF in iframe the exit event + // is not getting called hence calander getting remained open even + // when you click somewhere on window or focus into some other field + if (this.#options.showCalendarIcon && !this.#touchSupported) { + this.#curInstance.$field.focus(); // field loses focus after being marked readonly, causing blur event not to be fired later + } + } + + /* + * position the datepicker around the positioning element //TODO: migrate this to vanilla js + * provided in the options + */ + #position() { + let $elem = this.#curInstance.positioning, + windowScrollX = window.scrollX, + windowScrollY = window.scrollY, + windowInnerHeight = window.innerHeight, + windowInnerWidth = window.innerWidth, + inputRect = $elem.getBoundingClientRect(), + height = $elem.offsetHeight, + top = this.#getOffset($elem).top + height, + left = this.#getOffset($elem).left, + styles = {"top": (top + "px"), "left": (left + "px")}, + diffBottom = top + this.#dp.offsetHeight - windowInnerHeight - windowScrollY, //this.#dp.offsetHeight is the widget's height + newLeft, + newTop; + if (diffBottom > 0) { + //can't appear at the bottom + //check top + newTop = top - height - this.#dp.offsetHeight - 20; + if (newTop < windowScrollY) { + //can't appear at the top as well ... the datePicker pop up overlaps the date field + newTop = top - diffBottom; + } + styles.top = newTop + "px"; + } + if (left + this.#dp.offsetWidth > windowScrollX + windowInnerWidth) { + //align with the right edge + newLeft = windowScrollX + windowInnerWidth - this.#dp.offsetWidth - 20; + styles.left = newLeft + "px"; + } + this.#dp.style.top = styles.top; + this.#dp.style.left = styles.left; + const localeObj = new Intl.Locale(this.#lang); + if(localeObj?.textInfo?.direction == "rtl") { + let right = windowInnerWidth - (left + inputRect.width); // Calculate right offset + if (right + this.#dp.offsetWidth > windowInnerWidth) { // this.#dp.offsetWidth is the widget's width + // Align with the right edge of the viewport + right = windowScrollX + windowInnerWidth - this.#dp.offsetWidth - 20; // Adjust with a 20px margin + } + this.#dp.style.right = right + "px"; + this.#dp.style.left = "unset" + } + return this; + } + + #getOffset(elem) { + let box = elem.getBoundingClientRect(); + return { + top: box.top + window.scrollY, + left: box.left + window.scrollX + }; + } + + /* + * layout the nextView. if nextView is null return + * + */ + #layout(nextView) { + if (nextView == null) { + this.#hide(); + } else { + if (this.view) { + this['$' + this.view.toLowerCase()].style.display = "none"; + } + this.view = nextView; + this.caption.classList.toggle("disabled", + !this.#viewAction[this.view].caption); + let ariaLabel = ""; + if (this.view === "Month") { + ariaLabel = this.#options.locale.months[this.currentMonth] + " " + this.currentYear; + this.prevNavWidthBtn.setAttribute( + "aria-label", + this.#options.locale.previousMonth || "Previous month" + ); + this.nextNavWidthBtn.setAttribute( + "aria-label", + this.#options.locale.nextMonth || "Next month" + ); + } else if (this.view === "Year") { + ariaLabel = this.currentYear; + this.prevNavWidthBtn.setAttribute( + "aria-label", + this.#options.locale.previousYear || "Previous year" + ); + this.nextNavWidthBtn.setAttribute( + "aria-label", + this.#options.locale.nextYear || "Next year" + ); + } else if (this.view === "Yearset") { + const startYear = this.currentYear - this.#options.yearsPerView / 2; + const endYear = startYear + this.#options.yearsPerView - 1; + ariaLabel = startYear + "-" + endYear; + this.prevNavWidthBtn.setAttribute( + "aria-label", + this.#options.locale.previousSetOfYears || "Previous set of years" + ); + this.nextNavWidthBtn.setAttribute( + "aria-label", + this.#options.locale.nextSetOfYears || "Next set of years" + ); + } + this['$' + this.view.toLowerCase()].style.display = "block"; + this["show" + this.view](); + } + return this; + } + + /* + * show the month view + */ + showMonth() { + var self = this, + curDate = new Date(this.currentYear, this.currentMonth), + maxDay = this.#maxDate(this.currentMonth), + prevMaxDay = this.#maxDate((this.currentMonth + 11) % 12), + day1 = new Date(this.currentYear, this.currentMonth, 1).getDay(), + data, display; + var localizedYear = this.#getLocalizedYear(curDate); + + this.#tabulateView( + { + caption: this.#options.locale.months[this.currentMonth] + ", " + + this.#convertNumberToLocale(localizedYear), + header: this.#options.locale.days, + numRows: 7, + numColumns: 7, + elementAt: function (row, col) { + var day = (row - 1) * 7 + col - day1 + 1; + var monthVal = self.currentMonth + 1; + let gridId = "day-" + day; + display = self.#convertNumberToLocale(day); + data = day; + if (day < 1) { + display = self.#convertNumberToLocale(prevMaxDay + day); + data = -1; + monthVal = self.currentMonth; + } else if (day > maxDay) { + display = self.#convertNumberToLocale(day - maxDay); + data = -1; + monthVal = self.currentMonth + 2; + } else { + curDate.setDate(day); + // check if the currentdate is valid based on max and min valid date + if (self.#compareVal(curDate, self.maxValidDate, + self.exclMaxDate) || self.#compareVal(self.minValidDate, + curDate, self.exclMinDate)) { + data = -1; + } + } + return { + data: data, + gridId: gridId, + display: display, + ariaLabel: self.#options.editValue( + self.currentYear + "-" + self.#pad2(monthVal) + + "-" + self.#pad2(display)) + }; + } + }); + } + + /** + * returns boolean based on val1, val2, checkEqual + * For comparing date, use date object + */ + #compareVal(val1, val2, checkEqual) { + if (!val1 || !val2) { + return false; + } + + if (checkEqual) { + return val1 >= val2; + } else { + return val1 > val2; + } + } + + /* + * show the year view + */ + showYear() { + var self = this, + minDate = this.minValidDate ? new Date( + this.minValidDate.getFullYear(), this.minValidDate.getMonth()) + : null, + maxDate = this.maxValidDate ? new Date( + this.maxValidDate.getFullYear(), this.maxValidDate.getMonth()) + : null, + curDate = new Date(this.currentYear, 0), //can't omit month, if only one param present it is treated as millisecond + data, + month; + var localizedYear = this.#getLocalizedYear(curDate); + this.#tabulateView( + { + caption: this.#convertNumberToLocale(localizedYear), + numRows: 4, + numColumns: 3, + elementAt: function (row, col) { + data = month = row * 3 + col; + let gridId = "month-" + data; + curDate.setMonth(month); + if ((minDate && curDate < minDate) || (maxDate && curDate + > maxDate)) { + data = -1; + } + return { + data: data, + gridId: gridId, + display: self.#options.locale.months[month], + ariaLabel: self.#options.locale.months[month] + " " + + self.currentYear + }; + } + }); + } + + #getLocalizedYear(date) { + /* + // Only Thai language return year according to Buddhist calendar, rest all languages follows gregorian calendar in practice. + // The Buddhist Year in 2024 = 543 + 2024 = B.E. 2567 (reference https://wesak.org.my/calculating-b-e/) + // Intl.DateTimeFormat#formatToParts returns number (2024) for all languages except for Thai (2567) & for Persian ('۱۴۰۳') + // For Persian ('۱۴۰۳'), returned year is not in numbers that is breaking next flow. + */ + if(this.#lang === 'th') { + const dateFormat = new Intl.DateTimeFormat(this.#lang, { + year: 'numeric' + }); + const dateParts = dateFormat.formatToParts(date); + const yearObject = dateParts.find(yearObject => yearObject.type === "year"); + const localizedYear = yearObject.value; + return Number(localizedYear); + } + else { + return Number(date.getFullYear()); + } + } + + /* + * show the year set view + */ + showYearset() { + var year, + minDate = this.minValidDate ? new Date( + this.minValidDate.getFullYear(), 0) : null, + maxDate = this.maxValidDate ? new Date( + this.maxValidDate.getFullYear() + 1, 0) : null, + curDate = new Date(), + data, + self = this; + var localizedYear = this.#getLocalizedYear(new Date(this.currentYear, 0)), + localizedYearSet = this.#getLocalizedYear(new Date(self.currentYear, 0)); + + this.#tabulateView( + { + caption: this.#convertNumberToLocale( + localizedYear - this.#options.yearsPerView / 2) + "-" + + this.#convertNumberToLocale( + localizedYear - this.#options.yearsPerView / 2 + + this.#options.yearsPerView - 1), + numRows: 4, + numColumns: 4, + elementAt: function (row, col) { + data = year = localizedYearSet - 8 + (row * 4 + col); + let gridId = "year-" + data; + curDate.setFullYear(year); + if ((minDate && curDate < minDate) || (maxDate && curDate + > maxDate)) { + data = -1; + } + return { + "data": data, + "gridId": gridId, + "display": self.#convertNumberToLocale(year), + ariaLabel: year + }; + } + }); + } + + insertRow(rowNum, rowArray, isHeader, elementAt) { + let $view = this["$" + this.view.toLowerCase()], + $row = $view.getElementsByTagName("ul")[rowNum], + items, $li, element, $tmp, + self = this; + if (!$row) { + let rowEl = document.createElement("ul"); + rowEl.removeAttribute("aria-label"); + rowEl.classList.toggle("header", isHeader); + rowEl.setAttribute("role", "row"); + $row = $view.appendChild(rowEl); + } + items = $row.getElementsByTagName("li").length; + while (items++ < rowArray.length) { + let listItemEl = document.createElement("li"); + listItemEl.id = "li-" + elementAt(rowNum, items).gridId; + listItemEl.setAttribute("role", isHeader ? "columnheader" : "gridcell"); + $tmp = $row.appendChild(listItemEl); + if (!isHeader) { + let cb = function (evnt) { + self.#selectDate(evnt); + }; + $tmp.addEventListener("click", cb); + } + } + + rowArray.forEach(function (el, index) { + $li = $row.getElementsByTagName("li")[index]; + if (isHeader) { + $li.innerText = rowArray[index]; + } else { + element = rowArray[index]; + window.afCache.put($li, "value", element.data); + if (self.#checkDateIsFocussed(element.data)) { + if (self.$focusedDate) { + self.$focusedDate.classList.remove("dp-focus"); + self.$focusedDate.setAttribute("tabindex", "-1"); + } + self.$focusedDate = $li; + if (self.#keysEnabled) { + self.$focusedDate.classList.add("dp-focus") + } + } + $li.setAttribute("title", element.ariaLabel); + $li.setAttribute("aria-label", element.ariaLabel); + $li.setAttribute("tabindex", -1); + $li.classList.toggle("dp-selected", + self.#checkDateIsSelected(element.data)); + $li.classList.toggle("disabled", element.data === -1); + $li.innerText = element.display; + } + }); + return $row; + } + + /* + * creates a tabular view based on the options provided. The options that can be passed are + * numRows: number of rows that needs rendering + * numCols: number of columns that needs rendering + * caption: text for the datepickers caption element + * header: an array of elements that identifies the header row + * elementAt: a function(row, column) that returns an object (data: , display: ) where + * is the value to set for that view when the element at (row,column) is clicked and + * is the value that will be visible to the user + */ + #tabulateView(options) { + var r = 0, rows = 0, + row = [], + c; + this.caption.innerHTML = options.caption; + if (options.header) { + this.insertRow(r++, options.header, true, options.elementAt); + } + while (r < options.numRows) { + c = 0; + while (c < options.numColumns) { + row[c] = options.elementAt(r, c); + c++; + } + this.insertRow(r++, row, false, options.elementAt); + } + } + + #hotKeysCallBack = this.#hotKeys.bind(this); + + #activateField(evnt) { + let self = this; + this.#curInstance = window.afCache.get(evnt.target, "datetimepicker"); + // Issue LC-7049: + // datepickerTarget should be added when activate the field and should be removed + // after the fields gets deactivated. + if (this.#options.showCalendarIcon) { + this.#curInstance.$field.parentNode.classList.add("datePickerTarget"); + } + //enable hot keys only for non touch devices + if (!this.#touchSupported && !this.#keysEnabled) { + document.addEventListener("keydown", self.#hotKeysCallBack); + this.#keysEnabled = true; + } + } + + #deactivateField() { + let self = this; + if (self.#curInstance) { + if (this.#keysEnabled) { + document.removeEventListener("keydown", self.#hotKeysCallBack); + this.#keysEnabled = false; + } + // Remove document touch listener if it exists + if (this.#documentTouchListener) { + document.removeEventListener("touchstart", this.#documentTouchListener); + this.#documentTouchListener = null; + } + // Issue LC-7049: + // datepickerTarget should be added when activate the field and should be removed + // after the fields gets deactivated. Otherwise clicking on any other datefield + // will not hide the existing datepicker + if (this.#options.showCalendarIcon) { + self.#curInstance.$field.parentNode.classList.remove( + "datePickerTarget"); + } + self.#curInstance = null; + this.#hideAllViews(); + } + } + + #hide() { + if (DatePickerWidget.#visible) { + this.#dp.style.display = "none"; + this.#hideAllViews(); + DatePickerWidget.#visible = false; + if (this.#options.showCalendarIcon) { + this.#curInstance.$field.removeAttribute('readonly'); // when the datepicker is deactivated, activate the field + } + } + } + + #hideAllViews() { + this['$month'].replaceChildren(); + this['$year'].replaceChildren(); + this['$yearset'].replaceChildren(); + this['$month'].style.display = "none"; + this['$year'].style.display = "none"; + this['$yearset'].style.display = "none"; + } + + #adjustDate(step, view, focus) { + let maxDate, prevMaxDate; + let _focus = focus || false; + switch (view.toLowerCase()) { + case "day": + this.currentDay += step; + maxDate = this.#maxDate(this.currentMonth) + if (this.currentDay < 1) { + prevMaxDate = this.#maxDate((this.currentMonth - 1 + 12) % 12); + this.currentDay = prevMaxDate + this.currentDay; + return this.#adjustDate(-1, "month", _focus); + } + if (this.currentDay > maxDate) { + this.currentDay -= maxDate; + return this.#adjustDate(+1, "month", _focus); + } + break; + case "month": + this.currentMonth += step; + if (this.currentMonth > 11) { + this.currentYear++; + this.currentMonth = 0; + } + if (this.currentMonth < 0) { + this.currentYear--; + this.currentMonth = 11; + } + break; + case "year": + this.currentYear += step; + break; + case "yearset": + this.currentYear += step * this.#options.yearsPerView; + break; + } + this.#layout(this.view); + if (_focus) { + this.#focusedOnLi = true; + this.$focusedDate.setAttribute("tabindex", 0); + this.$focusedDate.focus(); + } + } + + #checkDateIsSelected(data) { + switch (this.view.toLowerCase()) { + case "month": + return this.currentYear === this.selectedYear && this.currentMonth + === this.selectedMonth && data === this.selectedDay; + case "year": + return this.currentYear === this.selectedYear && this.selectedMonth + === data; + case "yearset": + return this.selectedYear === data; + } + } + + #checkDateIsFocussed(data) { + switch (this.view.toLowerCase()) { + case "month": + return data === this.currentDay; + case "year": + return this.currentMonth === data; + case "yearset": + return this.currentYear === data; + } + } + + #convertNumberToLocale(number) { + const zeroCode = this.#options.locale.zero.charCodeAt(0); + number += ""; + let newNumber = []; + for (let i = 0; i < number.length; i++) { + newNumber.push( + String.fromCharCode(zeroCode + parseInt(number.charAt(i)))); + } + return newNumber.join(""); + } + + #clearDate(view) { + this.#model.dispatch(new FormView.Actions.UIChange({'value': ''})); + let existingSelectedItem = this['$' + view.toLowerCase()].getElementsByClassName("dp-selected")[0]; + if (existingSelectedItem) { + existingSelectedItem.classList.remove("dp-selected"); + } + } + + #getEvent() { + return "click";//this.#touchSupported ? "touchstart" : "click"; + } + + #pad2(m) { + return m = m < 10 ? "0" + m : m; + } + + toString() { + if(this.selectedYear === -1 || this.selectedMonth === -1 || this.selectedDay === -1){ + return ""; + } + const formattedDate = `${this.selectedYear}-${this.#pad2(this.selectedMonth + 1)}-${this.#pad2(this.selectedDay)}`; + return formattedDate; + } + + #selectDate(evnt) { + let val = window.afCache.get(evnt.target, "value"), + nextView = this.#viewAction[this.view].li; + + //disabled dates have a value of -1. Do nothing in that case + if (val === -1) { + return; + } + switch (this.view.toLowerCase()) { + case "month": + this.selectedMonth = this.currentMonth; + this.selectedYear = this.currentYear; + this.selectedDay = val; + this.setValue(this.toString()); + this.#model.dispatch(new FormView.Actions.UIChange({'value': this.toString()})); // updating model value to the latest when calender is changed + this.#curInstance.$field.focus(); + let existingSelectedElement = this['$' + + this.view.toLowerCase()].getElementsByClassName("dp-selected")[0]; + if (existingSelectedElement) { + existingSelectedElement.classList.remove("dp-selected"); + } + evnt.target.classList.add("dp-selected"); + break; + case "year": + this.currentMonth = val; + break; + case "yearset": + this.currentYear = val; + break; + } + this.#layout(nextView); + //manually focus on the field if clicked on the popup buttons for non-touch device + if (!this.#touchSupported) { + //No need to focus if selection is made by pressing space. + if (!this.hotKeyPressed) { + this.#scriptFocus = true; + } + } else if (nextView == null) { + //For touch devices, deactivate the field if a selection is made + this.#deactivateField() + } + } + + #leapYear() { + return this.currentYear % 400 === 0 || (this.currentYear % 100 !== 0 + && this.currentYear % 4 === 0); + } + + #maxDate(m) { + if (this.#leapYear() && m === 1) { + return 29; + } else { + return this.#dates[m]; + } + } + + #localizeDateElements(defaultOptions, locale) { + var calendarSymbols = FormView.LanguageUtils.getTranslatedString(locale, "calendarSymbols"); + if (calendarSymbols && calendarSymbols.abbrdayNames) { + defaultOptions.locale.days = calendarSymbols.abbrdayNames; + } + if (calendarSymbols && calendarSymbols.abbrdayNames) { + defaultOptions.locale.months = calendarSymbols.monthNames; + } + var clearText = FormView.LanguageUtils.getTranslatedString(locale, "clearText"); + if (clearText) { + defaultOptions.locale.clearText = clearText; + } + var openCalendarText = FormView.LanguageUtils.getTranslatedString(locale, "openCalendarText"); + if (openCalendarText) { + defaultOptions.locale.openCalendarText = openCalendarText; + } + var zero = FormView.LanguageUtils.getTranslatedString(locale, "0"); + if (zero) { + defaultOptions.locale.zero = zero; + } + var previousMonth = FormView.LanguageUtils.getTranslatedString(locale, "previousMonth"); + if (previousMonth) { + defaultOptions.locale.previousMonth = previousMonth; + } + var nextMonth = FormView.LanguageUtils.getTranslatedString(locale, "nextMonth"); + if (nextMonth) { + defaultOptions.locale.nextMonth = nextMonth; + } + var previousYear = FormView.LanguageUtils.getTranslatedString(locale, "previousYear"); + if (previousYear) { + defaultOptions.locale.previousYear = previousYear; + } + var nextYear = FormView.LanguageUtils.getTranslatedString(locale, "nextYear"); + if (nextYear) { + defaultOptions.locale.nextYear = nextYear; + } + var previousSetOfYears = FormView.LanguageUtils.getTranslatedString(locale, "previousSetOfYears"); + if (previousSetOfYears) { + defaultOptions.locale.previousSetOfYears = previousSetOfYears; + } + var nextSetOfYears = FormView.LanguageUtils.getTranslatedString(locale, "nextSetOfYears"); + if (nextSetOfYears) { + defaultOptions.locale.nextSetOfYears = nextSetOfYears; + } + } + + #isEditValueOrDisplayValue(value) { + return (this.#model.editValue === value || this.#model.displayValue + === value); + } + + /** + * returns the original value of format YYYY-MM-DD + * @returns {*|string} + */ + getValue() { + if (this.#isEditValueOrDisplayValue(this.#widget.value)) { + return this.#model.value; // if the widget has edit/display value thiss method should return model value + } + return this.#widget.value; + } + + /** + * Sets the formatted display value on the widget. + * value should be in YYYY-MM-DD format + * @param value + */ + setDisplayValue(value) { + if (this.#curInstance != null) { + this.#curInstance.$field.value = this.#curInstance.displayValue( + this.toString()) || value; + } else { + this.#widget.value = this.#model.displayValue || value; + } + } + + setCalendarWidgetValue(value) { + if (this.#curInstance === null && this.#widget != null) { + this.#curInstance = window.afCache.get(this.#widget, "datetimepicker"); + } + if (this.#curInstance != null) { + // also change the date of the calendar widget + this.#curInstance.selectedDate = value; + } + } + + /** + * Sets the formatted edit value on the widget + * @param value + */ + + setValue(value) { + let currDate; + if (value instanceof Date && !isNaN(value)) { + // If value is already a Date object and it's valid, use it as is + currDate = value; + } else { + // Check if value is empty + if (!value || value.trim() === '') { + currDate = new Date(); + } else { + let displayFormat = this.#model._jsonModel?.displayFormat; + // If displayFormat is null/undefined, use default parsing + if (!displayFormat) { + currDate = new Date(value); + } else { + // Use FormView.Formatters.parseDate for custom formats + currDate = FormView.Formatters.parseDate(value, this.#lang || 'en', displayFormat); + // If parseDate failed (returned null), fallback to default parsing + if (currDate === null) { + currDate = new Date(value); + } + const timezoneOffset = currDate.getTimezoneOffset(); + currDate.setMinutes(currDate.getMinutes() + timezoneOffset); + } + } + } + + if (!isNaN(currDate) && value != null) { + //in case the value is directly updated from the field without using calendar widget + this.selectedMonth = currDate.getMonth(); + this.selectedYear = currDate.getFullYear(); + this.selectedDay = currDate.getDate(); + } else { + this.selectedYear + = this.selectedMonth + = this.selectedDay + = -1; + } + if (this.#curInstance != null) { + if (!this.#isEditValueOrDisplayValue(value)) { + this.#curInstance.selectedDate = value; // prevent edit/display value from getting set in calender + } + this.#curInstance.$field.value = this.#curInstance.editValue() || value; + } else { + this.#widget.value = value; + } + } + + addEventListener(event, handler, widget) { + let inst = window.afCache.get(widget, "datetimepicker"); + inst.$field.addEventListener(event, function (e) { + switch (e.type) { + case 'blur': + if (!DatePickerWidget.#visible) { + handler(e); + } + break; + case 'focus': + handler(e); + break; + case 'input': + handler(e); + break; + + } + }); + } + + } +} \ No newline at end of file diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/datepicker.html b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/datepicker.html new file mode 100644 index 0000000000..6cff5a1b85 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/datepicker.html @@ -0,0 +1,59 @@ + + +
+
+
+
+
+ +
+
+
+
+<<<<<<< HEAD +>>>>>>> 454c70bfe (added FORMS-18631, FORMS-18630, FORMS-18625 and FORMS-18686 changes to datepicker v2) +======= +>>>>>>> 6705383ed (FORMS-18631: extended v2) +
\ No newline at end of file diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/datepicker.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/datepicker.js new file mode 100644 index 0000000000..953cc77046 --- /dev/null +++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/datepicker/v2/datepicker/datepicker.js @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +use(function () { + var clientlibsArr = ['core.forms.components.base.v1.editor']; + var labelPath = 'core/fd/components/af-commons/v1/fieldTemplates/label.html'; + var shortDescriptionPath = "core/fd/components/af-commons/v1/fieldTemplates/shortDescription.html"; + var longDescriptionPath = "core/fd/components/af-commons/v1/fieldTemplates/longDescription.html"; + var questionMarkPath = "core/fd/components/af-commons/v1/fieldTemplates/questionMark.html" + var errorMessagePath = "core/fd/components/af-commons/v1/fieldTemplates/errorMessage.html"; + return { + labelPath: labelPath, + shortDescriptionPath: shortDescriptionPath, + longDescriptionPath: longDescriptionPath, + questionMarkPath: questionMarkPath, + errorMessagePath: errorMessagePath, + clientlibs: clientlibsArr + } +}); \ No newline at end of file diff --git a/ui.tests/test-module/specs/datepicker/datepickerv2.runtime.cy.js b/ui.tests/test-module/specs/datepicker/datepickerv2.runtime.cy.js new file mode 100644 index 0000000000..454d29a8fd --- /dev/null +++ b/ui.tests/test-module/specs/datepicker/datepickerv2.runtime.cy.js @@ -0,0 +1,54 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ + +describe("Form Runtime with Date Picker v2", () => { + + const pagePath = "content/forms/af/core-components-it/samples/datepicker/basic.html"; + const bemBlock = "cmp-adaptiveform-datepicker"; + let formContainer = null; + + beforeEach(() => { + cy.previewForm(pagePath).then(p => { + formContainer = p; + }); + }); + + const openFirstDatepickerCalendar = () => { + cy.get(`.${bemBlock}__calendar-icon`).first().as("calendarIcon"); + cy.get("@calendarIcon").should("exist").click({ force: true }); + cy.get(".datetimepicker").should("be.visible"); + cy.get(".dp-monthview").should("exist"); + }; + + it("should open calendar with keyboard on calendar icon (Space/Enter)", () => { + expect(formContainer, "formcontainer is initialized").to.not.be.null; + cy.get(`.${bemBlock}__calendar-icon`).first().as("calendarIcon"); + cy.get("@calendarIcon").should("exist").focus(); + + cy.get("@calendarIcon").type(" ", { force: true }); + cy.get(".datetimepicker").should("be.visible"); + cy.get(".dp-monthview").should("exist"); + + cy.get("body").click(0, 0, { force: true }); + cy.get(".datetimepicker").should("not.be.visible"); + + cy.get("@calendarIcon").focus().type("{enter}", { force: true }); + cy.get(".datetimepicker").should("be.visible"); + cy.get(".dp-monthview").should("exist"); + }); +}); + +