diff --git a/.changeset/add-datetime-input-component.md b/.changeset/add-datetime-input-component.md new file mode 100644 index 00000000000..835c482f7c4 --- /dev/null +++ b/.changeset/add-datetime-input-component.md @@ -0,0 +1,5 @@ +--- +'@siemens/ix': minor +--- + +Added `ix-datetime-input` component - a combined date and time input field with integrated picker functionality. diff --git a/packages/angular-standalone-test-app/src/app/app.routes.ts b/packages/angular-standalone-test-app/src/app/app.routes.ts index c7839187576..946749976bf 100644 --- a/packages/angular-standalone-test-app/src/app/app.routes.ts +++ b/packages/angular-standalone-test-app/src/app/app.routes.ts @@ -333,6 +333,53 @@ export const routes: Routes = [ loadComponent: () => import('../preview-examples/datetimepicker').then((m) => m.default), }, + { + path: 'datetime-input', + loadComponent: () => + import('../preview-examples/datetime-input').then((m) => m.default), + }, + { + path: 'datetime-input-disabled', + loadComponent: () => + import('../preview-examples/datetime-input-disabled').then( + (m) => m.default + ), + }, + { + path: 'datetime-input-label', + loadComponent: () => + import('../preview-examples/datetime-input-label').then( + (m) => m.default + ), + }, + { + path: 'datetime-input-min-max-date', + loadComponent: () => + import('../preview-examples/datetime-input-min-max-date').then( + (m) => m.default + ), + }, + { + path: 'datetime-input-readonly', + loadComponent: () => + import('../preview-examples/datetime-input-readonly').then( + (m) => m.default + ), + }, + { + path: 'datetime-input-validation', + loadComponent: () => + import('../preview-examples/datetime-input-validation').then( + (m) => m.default + ), + }, + { + path: 'datetime-input-with-slots', + loadComponent: () => + import('../preview-examples/datetime-input-with-slots').then( + (m) => m.default + ), + }, { path: 'divider', loadComponent: () => diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-disabled.html b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-disabled.html new file mode 100644 index 00000000000..245218b1a1b --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-disabled.html @@ -0,0 +1,10 @@ + + + diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-disabled.ts b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-disabled.ts new file mode 100644 index 00000000000..dc54f7d1497 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-disabled.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; +import { IxDatetimeInput } from '@siemens/ix-angular/standalone'; + +@Component({ + selector: 'app-example', + imports: [IxDatetimeInput], + templateUrl: './datetime-input-disabled.html', +}) +export default class DatetimeInputDisabled {} diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-label.html b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-label.html new file mode 100644 index 00000000000..522618d42cd --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-label.html @@ -0,0 +1,15 @@ + + + diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-label.ts b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-label.ts new file mode 100644 index 00000000000..dcd54813c85 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-label.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; +import { IxDatetimeInput } from '@siemens/ix-angular/standalone'; + +@Component({ + selector: 'app-example', + imports: [IxDatetimeInput], + templateUrl: './datetime-input-label.html', +}) +export default class DatetimeInputLabel {} diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-min-max-date.html b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-min-max-date.html new file mode 100644 index 00000000000..83e6ad5e567 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-min-max-date.html @@ -0,0 +1,14 @@ + + + diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-min-max-date.ts b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-min-max-date.ts new file mode 100644 index 00000000000..b185bd681c6 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-min-max-date.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; +import { IxDatetimeInput } from '@siemens/ix-angular/standalone'; + +@Component({ + selector: 'app-example', + imports: [IxDatetimeInput], + templateUrl: './datetime-input-min-max-date.html', +}) +export default class DatetimeInputMinMaxDate {} diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-readonly.html b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-readonly.html new file mode 100644 index 00000000000..76968c17a54 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-readonly.html @@ -0,0 +1,10 @@ + + + diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-readonly.ts b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-readonly.ts new file mode 100644 index 00000000000..eff43405ca1 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-readonly.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; +import { IxDatetimeInput } from '@siemens/ix-angular/standalone'; + +@Component({ + selector: 'app-example', + imports: [IxDatetimeInput], + templateUrl: './datetime-input-readonly.html', +}) +export default class DatetimeInputReadonly {} diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-validation.html b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-validation.html new file mode 100644 index 00000000000..564a83aa3c0 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-validation.html @@ -0,0 +1,40 @@ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-validation.ts b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-validation.ts new file mode 100644 index 00000000000..8121958aad4 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-validation.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; +import { IxDatetimeInput } from '@siemens/ix-angular/standalone'; + +@Component({ + selector: 'app-example', + imports: [IxDatetimeInput], + templateUrl: './datetime-input-validation.html', +}) +export default class DatetimeInputValidation {} diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-with-slots.html b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-with-slots.html new file mode 100644 index 00000000000..b368034f7ea --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-with-slots.html @@ -0,0 +1,13 @@ + + + + + Slot + diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input-with-slots.ts b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-with-slots.ts new file mode 100644 index 00000000000..1e454411f7d --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input-with-slots.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; +import { IxDatetimeInput, IxIcon, IxTypography } from '@siemens/ix-angular/standalone'; + +@Component({ + selector: 'app-example', + imports: [IxDatetimeInput, IxIcon, IxTypography], + templateUrl: './datetime-input-with-slots.html', +}) +export default class DatetimeInputWithSlots {} diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input.html b/packages/angular-standalone-test-app/src/preview-examples/datetime-input.html new file mode 100644 index 00000000000..61d7dd18674 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input.html @@ -0,0 +1,10 @@ + + + diff --git a/packages/angular-standalone-test-app/src/preview-examples/datetime-input.ts b/packages/angular-standalone-test-app/src/preview-examples/datetime-input.ts new file mode 100644 index 00000000000..b2a219e3335 --- /dev/null +++ b/packages/angular-standalone-test-app/src/preview-examples/datetime-input.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; +import { IxDatetimeInput } from '@siemens/ix-angular/standalone'; + +@Component({ + selector: 'app-example', + imports: [IxDatetimeInput], + templateUrl: './datetime-input.html', +}) +export default class DatetimeInput {} diff --git a/packages/angular-test-app/src/app/app-routing.module.ts b/packages/angular-test-app/src/app/app-routing.module.ts index 35125b2f761..8365b3780c2 100644 --- a/packages/angular-test-app/src/app/app-routing.module.ts +++ b/packages/angular-test-app/src/app/app-routing.module.ts @@ -64,6 +64,13 @@ import Datepicker from '../preview-examples/datepicker'; import DatepickerLocale from '../preview-examples/datepicker-locale'; import DatepickerRange from '../preview-examples/datepicker-range'; import Datetimepicker from '../preview-examples/datetimepicker'; +import DatetimeInput from '../preview-examples/datetime-input'; +import DatetimeInputDisabled from '../preview-examples/datetime-input-disabled'; +import DatetimeInputLabel from '../preview-examples/datetime-input-label'; +import DatetimeInputMinMaxDate from '../preview-examples/datetime-input-min-max-date'; +import DatetimeInputReadonly from '../preview-examples/datetime-input-readonly'; +import DatetimeInputValidation from '../preview-examples/datetime-input-validation'; +import DatetimeInputWithSlots from '../preview-examples/datetime-input-with-slots'; import Divider from '../preview-examples/divider'; import Drawer from '../preview-examples/drawer'; import DrawerFullHeight from '../preview-examples/drawer-full-height'; @@ -486,6 +493,34 @@ const routes: Routes = [ path: 'datetimepicker', component: Datetimepicker, }, + { + path: 'datetime-input', + component: DatetimeInput, + }, + { + path: 'datetime-input-disabled', + component: DatetimeInputDisabled, + }, + { + path: 'datetime-input-label', + component: DatetimeInputLabel, + }, + { + path: 'datetime-input-min-max-date', + component: DatetimeInputMinMaxDate, + }, + { + path: 'datetime-input-readonly', + component: DatetimeInputReadonly, + }, + { + path: 'datetime-input-validation', + component: DatetimeInputValidation, + }, + { + path: 'datetime-input-with-slots', + component: DatetimeInputWithSlots, + }, { path: 'divider', component: Divider, diff --git a/packages/angular-test-app/src/app/app.module.ts b/packages/angular-test-app/src/app/app.module.ts index 8fc5ae07c34..c9e8db293d7 100644 --- a/packages/angular-test-app/src/app/app.module.ts +++ b/packages/angular-test-app/src/app/app.module.ts @@ -73,6 +73,13 @@ import Datepicker from '../preview-examples/datepicker'; import DatepickerLocale from '../preview-examples/datepicker-locale'; import DatepickerRange from '../preview-examples/datepicker-range'; import Datetimepicker from '../preview-examples/datetimepicker'; +import DatetimeInput from '../preview-examples/datetime-input'; +import DatetimeInputDisabled from '../preview-examples/datetime-input-disabled'; +import DatetimeInputLabel from '../preview-examples/datetime-input-label'; +import DatetimeInputMinMaxDate from '../preview-examples/datetime-input-min-max-date'; +import DatetimeInputReadonly from '../preview-examples/datetime-input-readonly'; +import DatetimeInputValidation from '../preview-examples/datetime-input-validation'; +import DatetimeInputWithSlots from '../preview-examples/datetime-input-with-slots'; import Divider from '../preview-examples/divider'; import Drawer from '../preview-examples/drawer'; import DrawerFullHeight from '../preview-examples/drawer-full-height'; @@ -307,6 +314,13 @@ import WorkflowVertical from '../preview-examples/workflow-vertical'; Datepicker, DatepickerLocale, Datetimepicker, + DatetimeInput, + DatetimeInputDisabled, + DatetimeInputLabel, + DatetimeInputMinMaxDate, + DatetimeInputReadonly, + DatetimeInputValidation, + DatetimeInputWithSlots, Divider, DrawerFullHeight, Drawer, diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-disabled.html b/packages/angular-test-app/src/preview-examples/datetime-input-disabled.html new file mode 100644 index 00000000000..edb1f65af1f --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-disabled.html @@ -0,0 +1,10 @@ + + + diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-disabled.ts b/packages/angular-test-app/src/preview-examples/datetime-input-disabled.ts new file mode 100644 index 00000000000..28b8a9a3136 --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-disabled.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; + +@Component({ + standalone: false, + selector: 'app-example', + templateUrl: './datetime-input-disabled.html', +}) +export default class DatetimeInputDisabled {} diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-label.html b/packages/angular-test-app/src/preview-examples/datetime-input-label.html new file mode 100644 index 00000000000..ba233691711 --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-label.html @@ -0,0 +1,15 @@ + + + diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-label.ts b/packages/angular-test-app/src/preview-examples/datetime-input-label.ts new file mode 100644 index 00000000000..43440fb969a --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-label.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; + +@Component({ + standalone: false, + selector: 'app-example', + templateUrl: './datetime-input-label.html', +}) +export default class DatetimeInputLabel {} diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-min-max-date.html b/packages/angular-test-app/src/preview-examples/datetime-input-min-max-date.html new file mode 100644 index 00000000000..a35b93cd446 --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-min-max-date.html @@ -0,0 +1,14 @@ + + + diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-min-max-date.ts b/packages/angular-test-app/src/preview-examples/datetime-input-min-max-date.ts new file mode 100644 index 00000000000..b9782ea1413 --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-min-max-date.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; + +@Component({ + standalone: false, + selector: 'app-example', + templateUrl: './datetime-input-min-max-date.html', +}) +export default class DatetimeInputMinMaxDate {} diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-readonly.html b/packages/angular-test-app/src/preview-examples/datetime-input-readonly.html new file mode 100644 index 00000000000..babbef3c98f --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-readonly.html @@ -0,0 +1,10 @@ + + + diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-readonly.ts b/packages/angular-test-app/src/preview-examples/datetime-input-readonly.ts new file mode 100644 index 00000000000..6975affe296 --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-readonly.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; + +@Component({ + standalone: false, + selector: 'app-example', + templateUrl: './datetime-input-readonly.html', +}) +export default class DatetimeInputReadonly {} diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-validation.html b/packages/angular-test-app/src/preview-examples/datetime-input-validation.html new file mode 100644 index 00000000000..21b27bc867d --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-validation.html @@ -0,0 +1,40 @@ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-validation.ts b/packages/angular-test-app/src/preview-examples/datetime-input-validation.ts new file mode 100644 index 00000000000..98d6eca1e0f --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-validation.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; + +@Component({ + standalone: false, + selector: 'app-example', + templateUrl: './datetime-input-validation.html', +}) +export default class DatetimeInputValidation {} diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-with-slots.html b/packages/angular-test-app/src/preview-examples/datetime-input-with-slots.html new file mode 100644 index 00000000000..9bb7babcd0b --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-with-slots.html @@ -0,0 +1,13 @@ + + + + + Slot + diff --git a/packages/angular-test-app/src/preview-examples/datetime-input-with-slots.ts b/packages/angular-test-app/src/preview-examples/datetime-input-with-slots.ts new file mode 100644 index 00000000000..387fea52e20 --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input-with-slots.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; + +@Component({ + standalone: false, + selector: 'app-example', + templateUrl: './datetime-input-with-slots.html', +}) +export default class DatetimeInputWithSlots {} diff --git a/packages/angular-test-app/src/preview-examples/datetime-input.html b/packages/angular-test-app/src/preview-examples/datetime-input.html new file mode 100644 index 00000000000..464ae64b6ed --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input.html @@ -0,0 +1,10 @@ + + + diff --git a/packages/angular-test-app/src/preview-examples/datetime-input.ts b/packages/angular-test-app/src/preview-examples/datetime-input.ts new file mode 100644 index 00000000000..54735028fe8 --- /dev/null +++ b/packages/angular-test-app/src/preview-examples/datetime-input.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { Component } from '@angular/core'; + +@Component({ + standalone: false, + selector: 'app-example', + templateUrl: './datetime-input.html', +}) +export default class DatetimeInput {} diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index 27500eaf4c9..45ed09c728d 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -715,6 +715,53 @@ The locale applied is always `en-US`. } +@ProxyCmp({ + inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'dateFormat', 'disabled', 'helperText', 'i18nDone', 'i18nErrorDateTimeUnparsable', 'i18nTime', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'timeFormat', 'validText', 'value', 'warningText', 'weekStartIndex'] +}) +@Component({ + selector: 'ix-datetime-input', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'dateFormat', 'disabled', 'helperText', 'i18nDone', 'i18nErrorDateTimeUnparsable', 'i18nTime', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'timeFormat', 'validText', 'value', 'warningText', 'weekStartIndex'], + outputs: ['valueChange', 'validityStateChange', 'ixFocus', 'ixBlur'], + standalone: false +}) +export class IxDatetimeInput { + protected el: HTMLIxDatetimeInputElement; + @Output() valueChange = new EventEmitter>(); + @Output() validityStateChange = new EventEmitter>(); + @Output() ixFocus = new EventEmitter>(); + @Output() ixBlur = new EventEmitter>(); + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +import type { DateTimeInputValidityState as IIxDatetimeInputDateTimeInputValidityState } from '@siemens/ix'; + +export declare interface IxDatetimeInput extends Components.IxDatetimeInput { + /** + * Emitted when the datetime value changes. Payload is display format or undefined + */ + valueChange: EventEmitter>; + /** + * Emitted when validation state changes + */ + validityStateChange: EventEmitter>; + /** + * Emitted when the input receives focus + */ + ixFocus: EventEmitter>; + /** + * Emitted when the input loses focus + */ + ixBlur: EventEmitter>; +} + + @ProxyCmp({ inputs: ['ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'dateFormat', 'from', 'i18nDone', 'i18nTime', 'locale', 'maxDate', 'minDate', 'showTimeReference', 'showWeekNumbers', 'singleSelection', 'time', 'timeFormat', 'timeReference', 'to', 'weekStartIndex'] }) diff --git a/packages/angular/src/declare-components.ts b/packages/angular/src/declare-components.ts index 6365ae1037e..98cdb33e145 100644 --- a/packages/angular/src/declare-components.ts +++ b/packages/angular/src/declare-components.ts @@ -26,6 +26,7 @@ export const DIRECTIVES = [ d.IxDateDropdown, d.IxDateInput, d.IxDatePicker, + d.IxDatetimeInput, d.IxDatetimePicker, d.IxDivider, d.IxDrawer, diff --git a/packages/angular/standalone/src/components.ts b/packages/angular/standalone/src/components.ts index fabb174b735..ca36af4d800 100644 --- a/packages/angular/standalone/src/components.ts +++ b/packages/angular/standalone/src/components.ts @@ -30,6 +30,7 @@ import { defineCustomElement as defineIxCustomField } from '@siemens/ix/componen import { defineCustomElement as defineIxDateDropdown } from '@siemens/ix/components/ix-date-dropdown.js'; import { defineCustomElement as defineIxDateInput } from '@siemens/ix/components/ix-date-input.js'; import { defineCustomElement as defineIxDatePicker } from '@siemens/ix/components/ix-date-picker.js'; +import { defineCustomElement as defineIxDatetimeInput } from '@siemens/ix/components/ix-datetime-input.js'; import { defineCustomElement as defineIxDatetimePicker } from '@siemens/ix/components/ix-datetime-picker.js'; import { defineCustomElement as defineIxDivider } from '@siemens/ix/components/ix-divider.js'; import { defineCustomElement as defineIxDrawer } from '@siemens/ix/components/ix-drawer.js'; @@ -815,6 +816,53 @@ The locale applied is always `en-US`. } +@ProxyCmp({ + defineCustomElementFn: defineIxDatetimeInput, + inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'dateFormat', 'disabled', 'helperText', 'i18nDone', 'i18nErrorDateTimeUnparsable', 'i18nTime', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'timeFormat', 'validText', 'value', 'warningText', 'weekStartIndex'] +}) +@Component({ + selector: 'ix-datetime-input', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'dateFormat', 'disabled', 'helperText', 'i18nDone', 'i18nErrorDateTimeUnparsable', 'i18nTime', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'timeFormat', 'validText', 'value', 'warningText', 'weekStartIndex'], + outputs: ['valueChange', 'validityStateChange', 'ixFocus', 'ixBlur'], +}) +export class IxDatetimeInput { + protected el: HTMLIxDatetimeInputElement; + @Output() valueChange = new EventEmitter>(); + @Output() validityStateChange = new EventEmitter>(); + @Output() ixFocus = new EventEmitter>(); + @Output() ixBlur = new EventEmitter>(); + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +import type { DateTimeInputValidityState as IIxDatetimeInputDateTimeInputValidityState } from '@siemens/ix/components'; + +export declare interface IxDatetimeInput extends Components.IxDatetimeInput { + /** + * Emitted when the datetime value changes. Payload is display format or undefined + */ + valueChange: EventEmitter>; + /** + * Emitted when validation state changes + */ + validityStateChange: EventEmitter>; + /** + * Emitted when the input receives focus + */ + ixFocus: EventEmitter>; + /** + * Emitted when the input loses focus + */ + ixBlur: EventEmitter>; +} + + @ProxyCmp({ defineCustomElementFn: defineIxDatetimePicker, inputs: ['ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'dateFormat', 'from', 'i18nDone', 'i18nTime', 'locale', 'maxDate', 'minDate', 'showTimeReference', 'showWeekNumbers', 'singleSelection', 'time', 'timeFormat', 'timeReference', 'to', 'weekStartIndex'] diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index ee8dcbeb8b9..ed1fd9aa705 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -25,6 +25,7 @@ import { DateDropdownOption, DateRangeChangeEvent } from "./components/date-drop import { DateInputValidityState } from "./components/date-input/date-input.types"; import { DateTimeCardCorners } from "./components/date-time-card/date-time-card.types"; import { DateChangeEvent } from "./components/date-picker/date-picker.events"; +import { DateTimeInputValidityState } from "./components/datetime-input/datetime-input.types"; import { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datetime-picker/datetime-picker.types"; import { ElementReference } from "./components/utils/element-reference"; import { CloseBehavior } from "./components/dropdown/dropdown-controller"; @@ -75,6 +76,7 @@ export { DateDropdownOption, DateRangeChangeEvent } from "./components/date-drop export { DateInputValidityState } from "./components/date-input/date-input.types"; export { DateTimeCardCorners } from "./components/date-time-card/date-time-card.types"; export { DateChangeEvent } from "./components/date-picker/date-picker.events"; +export { DateTimeInputValidityState } from "./components/datetime-input/datetime-input.types"; export { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datetime-picker/datetime-picker.types"; export { ElementReference } from "./components/utils/element-reference"; export { CloseBehavior } from "./components/dropdown/dropdown-controller"; @@ -1198,12 +1200,172 @@ export namespace Components { * @default false */ "hideHeader": boolean; + /** + * Remove content padding + * @default false + */ + "noPadding": boolean; /** * Timepicker specific styling * @default false */ "timePickerAppearance": boolean; } + /** + * @form-ready + */ + interface IxDatetimeInput { + /** + * ARIA label for the calendar icon button + */ + "ariaLabelCalendarButton"?: string; + /** + * ARIA label for next month navigation button + */ + "ariaLabelNextMonthButton"?: string; + /** + * ARIA label for previous month navigation button + */ + "ariaLabelPreviousMonthButton"?: string; + /** + * Luxon date format for display (e.g., 'yyyy/LL/dd' → "2026/01/20") + * @default 'yyyy/LL/dd' + */ + "dateFormat": string; + /** + * Whether the input is disabled + * @default false + */ + "disabled": boolean; + /** + * Focus the native input element + */ + "focusInput": () => Promise; + /** + * Returns the associated HTML form element. + */ + "getAssociatedFormElement": () => Promise; + /** + * Get the native input element + */ + "getNativeInputElement": () => Promise; + /** + * Returns the validity state of the input. + */ + "getValidityState": () => Promise; + /** + * Returns whether the input has a value. + */ + "hasValidValue": () => Promise; + /** + * Helper text displayed below the input + */ + "helperText"?: string; + /** + * Text for confirm button in picker (prop name matches datetime-picker) + * @default 'Confirm' + */ + "i18nDone": string; + /** + * Error message when datetime cannot be parsed + * @default 'Date time is not valid' + */ + "i18nErrorDateTimeUnparsable": string; + /** + * Header text for time picker section + * @default 'Time' + */ + "i18nTime": string; + /** + * Informational message + */ + "infoText"?: string; + /** + * Validation message for invalid state + */ + "invalidText"?: string; + /** + * Returns whether the input field has been touched. + */ + "isTouched": () => Promise; + /** + * Label text displayed above the input + */ + "label"?: string; + /** + * Locale for date/time formatting (e.g., 'en-US', 'de-DE') + */ + "locale"?: string; + /** + * Maximum allowed date in date format (matching dateFormat, e.g., "2026/12/31") + */ + "maxDate"?: string; + /** + * Minimum allowed date in date format (matching dateFormat, e.g., "2026/01/20") + */ + "minDate"?: string; + /** + * Name of the form control for form submission + */ + "name"?: string; + /** + * Placeholder text when input is empty + */ + "placeholder"?: string; + /** + * Whether the input is read-only (calendar icon hidden) + * @default false + */ + "readonly": boolean; + /** + * Whether the field is required + * @default false + */ + "required": boolean; + /** + * Show helper text as tooltip instead of below input + * @default false + */ + "showTextAsTooltip": boolean; + /** + * Show week numbers in date picker + * @default false + */ + "showWeekNumbers": boolean; + /** + * Prevent form submission when Enter is pressed + * @default false + */ + "suppressSubmitOnEnter": boolean; + /** + * Text alignment within the input field + * @default 'start' + */ + "textAlignment": 'start' | 'end'; + /** + * Luxon time format for display (e.g., 'HH:mm:ss' → "13:07:04") + * @default 'HH:mm:ss' + */ + "timeFormat": string; + /** + * Success/valid message + */ + "validText"?: string; + /** + * Value in display format (e.g., "2026/01/21 13:07:04" for default dateFormat + timeFormat) + * @default '' + */ + "value"?: string; + /** + * Warning message + */ + "warningText"?: string; + /** + * First day of week (0=Sunday, 1=Monday, etc.) + * @default 0 + */ + "weekStartIndex": number; + } interface IxDatetimePicker { /** * ARIA label for the next month icon button Will be set as aria-label on the nested HTML button element @@ -1218,6 +1380,10 @@ export namespace Components { * @default 'yyyy/LL/dd' */ "dateFormat": string; + /** + * @default false + */ + "embedded": boolean; /** * The selected starting date. If the picker is not in range mode this is the selected date. Format has to match the `format` property. */ @@ -4301,6 +4467,10 @@ export interface IxDatePickerCustomEvent extends CustomEvent { detail: T; target: HTMLIxDatePickerElement; } +export interface IxDatetimeInputCustomEvent extends CustomEvent { + detail: T; + target: HTMLIxDatetimeInputElement; +} export interface IxDatetimePickerCustomEvent extends CustomEvent { detail: T; target: HTMLIxDatetimePickerElement; @@ -4834,6 +5004,29 @@ declare global { prototype: HTMLIxDateTimeCardElement; new (): HTMLIxDateTimeCardElement; }; + interface HTMLIxDatetimeInputElementEventMap { + "valueChange": string | undefined; + "validityStateChange": DateTimeInputValidityState; + "ixFocus": void; + "ixBlur": void; + } + /** + * @form-ready + */ + interface HTMLIxDatetimeInputElement extends Components.IxDatetimeInput, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLIxDatetimeInputElement, ev: IxDatetimeInputCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLIxDatetimeInputElement, ev: IxDatetimeInputCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLIxDatetimeInputElement: { + prototype: HTMLIxDatetimeInputElement; + new (): HTMLIxDatetimeInputElement; + }; interface HTMLIxDatetimePickerElementEventMap { "timeChange": string; "dateChange": DateTimeDateChangeEvent; @@ -5950,6 +6143,7 @@ declare global { "ix-date-input": HTMLIxDateInputElement; "ix-date-picker": HTMLIxDatePickerElement; "ix-date-time-card": HTMLIxDateTimeCardElement; + "ix-datetime-input": HTMLIxDatetimeInputElement; "ix-datetime-picker": HTMLIxDatetimePickerElement; "ix-divider": HTMLIxDividerElement; "ix-drawer": HTMLIxDrawerElement; @@ -7199,12 +7393,164 @@ declare namespace LocalJSX { * @default false */ "hideHeader"?: boolean; + /** + * Remove content padding + * @default false + */ + "noPadding"?: boolean; /** * Timepicker specific styling * @default false */ "timePickerAppearance"?: boolean; } + /** + * @form-ready + */ + interface IxDatetimeInput { + /** + * ARIA label for the calendar icon button + */ + "ariaLabelCalendarButton"?: string; + /** + * ARIA label for next month navigation button + */ + "ariaLabelNextMonthButton"?: string; + /** + * ARIA label for previous month navigation button + */ + "ariaLabelPreviousMonthButton"?: string; + /** + * Luxon date format for display (e.g., 'yyyy/LL/dd' → "2026/01/20") + * @default 'yyyy/LL/dd' + */ + "dateFormat"?: string; + /** + * Whether the input is disabled + * @default false + */ + "disabled"?: boolean; + /** + * Helper text displayed below the input + */ + "helperText"?: string; + /** + * Text for confirm button in picker (prop name matches datetime-picker) + * @default 'Confirm' + */ + "i18nDone"?: string; + /** + * Error message when datetime cannot be parsed + * @default 'Date time is not valid' + */ + "i18nErrorDateTimeUnparsable"?: string; + /** + * Header text for time picker section + * @default 'Time' + */ + "i18nTime"?: string; + /** + * Informational message + */ + "infoText"?: string; + /** + * Validation message for invalid state + */ + "invalidText"?: string; + /** + * Label text displayed above the input + */ + "label"?: string; + /** + * Locale for date/time formatting (e.g., 'en-US', 'de-DE') + */ + "locale"?: string; + /** + * Maximum allowed date in date format (matching dateFormat, e.g., "2026/12/31") + */ + "maxDate"?: string; + /** + * Minimum allowed date in date format (matching dateFormat, e.g., "2026/01/20") + */ + "minDate"?: string; + /** + * Name of the form control for form submission + */ + "name"?: string; + /** + * Emitted when the input loses focus + */ + "onIxBlur"?: (event: IxDatetimeInputCustomEvent) => void; + /** + * Emitted when the input receives focus + */ + "onIxFocus"?: (event: IxDatetimeInputCustomEvent) => void; + /** + * Emitted when validation state changes + */ + "onValidityStateChange"?: (event: IxDatetimeInputCustomEvent) => void; + /** + * Emitted when the datetime value changes. Payload is display format or undefined + */ + "onValueChange"?: (event: IxDatetimeInputCustomEvent) => void; + /** + * Placeholder text when input is empty + */ + "placeholder"?: string; + /** + * Whether the input is read-only (calendar icon hidden) + * @default false + */ + "readonly"?: boolean; + /** + * Whether the field is required + * @default false + */ + "required"?: boolean; + /** + * Show helper text as tooltip instead of below input + * @default false + */ + "showTextAsTooltip"?: boolean; + /** + * Show week numbers in date picker + * @default false + */ + "showWeekNumbers"?: boolean; + /** + * Prevent form submission when Enter is pressed + * @default false + */ + "suppressSubmitOnEnter"?: boolean; + /** + * Text alignment within the input field + * @default 'start' + */ + "textAlignment"?: 'start' | 'end'; + /** + * Luxon time format for display (e.g., 'HH:mm:ss' → "13:07:04") + * @default 'HH:mm:ss' + */ + "timeFormat"?: string; + /** + * Success/valid message + */ + "validText"?: string; + /** + * Value in display format (e.g., "2026/01/21 13:07:04" for default dateFormat + timeFormat) + * @default '' + */ + "value"?: string; + /** + * Warning message + */ + "warningText"?: string; + /** + * First day of week (0=Sunday, 1=Monday, etc.) + * @default 0 + */ + "weekStartIndex"?: number; + } interface IxDatetimePicker { /** * ARIA label for the next month icon button Will be set as aria-label on the nested HTML button element @@ -7219,6 +7565,10 @@ declare namespace LocalJSX { * @default 'yyyy/LL/dd' */ "dateFormat"?: string; + /** + * @default false + */ + "embedded"?: boolean; /** * The selected starting date. If the picker is not in range mode this is the selected date. Format has to match the `format` property. */ @@ -10437,6 +10787,7 @@ declare namespace LocalJSX { "ix-date-input": IxDateInput; "ix-date-picker": IxDatePicker; "ix-date-time-card": IxDateTimeCard; + "ix-datetime-input": IxDatetimeInput; "ix-datetime-picker": IxDatetimePicker; "ix-divider": IxDivider; "ix-drawer": IxDrawer; @@ -10562,6 +10913,10 @@ declare module "@stencil/core" { "ix-date-input": LocalJSX.IxDateInput & JSXBase.HTMLAttributes; "ix-date-picker": LocalJSX.IxDatePicker & JSXBase.HTMLAttributes; "ix-date-time-card": LocalJSX.IxDateTimeCard & JSXBase.HTMLAttributes; + /** + * @form-ready + */ + "ix-datetime-input": LocalJSX.IxDatetimeInput & JSXBase.HTMLAttributes; "ix-datetime-picker": LocalJSX.IxDatetimePicker & JSXBase.HTMLAttributes; "ix-divider": LocalJSX.IxDivider & JSXBase.HTMLAttributes; /** diff --git a/packages/core/src/components/date-time-card/date-time-card.scss b/packages/core/src/components/date-time-card/date-time-card.scss index 179bf6b1bde..a2419329046 100644 --- a/packages/core/src/components/date-time-card/date-time-card.scss +++ b/packages/core/src/components/date-time-card/date-time-card.scss @@ -71,6 +71,10 @@ &--time-picker { padding: 0 vars.$default-space; } + + &--no-padding { + padding: 0; + } } .footer-container { diff --git a/packages/core/src/components/date-time-card/date-time-card.tsx b/packages/core/src/components/date-time-card/date-time-card.tsx index 910d993b624..6bdb7090dd9 100644 --- a/packages/core/src/components/date-time-card/date-time-card.tsx +++ b/packages/core/src/components/date-time-card/date-time-card.tsx @@ -40,6 +40,11 @@ export class DateTimeCard { */ @Prop() corners: DateTimeCardCorners = 'rounded'; + /** + * Remove content padding + */ + @Prop() noPadding: boolean = false; + private cardClasses() { return { card: true, @@ -68,6 +73,7 @@ export class DateTimeCard { class={{ content: true, 'content--time-picker': this.timePickerAppearance, + 'content--no-padding': this.noPadding, }} > diff --git a/packages/core/src/components/datetime-input/datetime-input.scss b/packages/core/src/components/datetime-input/datetime-input.scss new file mode 100644 index 00000000000..45d32071708 --- /dev/null +++ b/packages/core/src/components/datetime-input/datetime-input.scss @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +@use 'mixins/shadow-dom/component'; +@use 'mixins/input'; +@use './../input/input.mixins.scss' as input-mixins; + +@include input-mixins.input-field; + +:host { + display: inline-block; + position: relative; + + @include component.ix-component; + + input { + width: 100%; + height: 100%; + } + + .calendar-hidden { + display: none; + } +} + +:host(.readonly) { + input { + pointer-events: none; + } +} diff --git a/packages/core/src/components/datetime-input/datetime-input.tsx b/packages/core/src/components/datetime-input/datetime-input.tsx new file mode 100644 index 00000000000..f8b9376fb7e --- /dev/null +++ b/packages/core/src/components/datetime-input/datetime-input.tsx @@ -0,0 +1,643 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * SPDX-License-Identifier: MIT + */ + +import { iconCalendar } from '@siemens/ix-icons/icons'; +import { + AttachInternals, + Component, + Element, + Event, + EventEmitter, + Host, + Method, + Prop, + State, + Watch, + h, +} from '@stencil/core'; +import { DateTime } from 'luxon'; +import { SlotEnd, SlotStart } from '../input/input.fc'; +import { + DisposableChangesAndVisibilityObservers, + addDisposableChangesAndVisibilityObservers, + adjustPaddingForStartAndEnd, + handleSubmitOnEnterKeydown, +} from '../input/input.util'; +import { + ClassMutationObserver, + HookValidationLifecycle, + IxInputFieldComponent, + ValidationResults, + createClassMutationObserver, + getValidationText, +} from '../utils/input'; +import { + closeDropdown as closeDropdownUtil, + createValidityState, + handleIconClick, + openDropdown as openDropdownUtil, +} from '../utils/input/picker-input.util'; +import { makeRef } from '../utils/make-ref'; +import { DateTimeInputValidityState } from './datetime-input.types'; + +/** + * @form-ready + * + * @slot start - Element will be displayed at the start of the input + * @slot end - Element will be displayed at the end of the input + */ + +@Component({ + tag: 'ix-datetime-input', + styleUrl: 'datetime-input.scss', + shadow: true, + formAssociated: true, +}) +export class DatetimeInput + implements IxInputFieldComponent +{ + @Element() hostElement!: HTMLIxDatetimeInputElement; + @AttachInternals() formInternals!: ElementInternals; + + /** Name of the form control for form submission */ + @Prop({ reflect: true }) name?: string; + + /** Placeholder text when input is empty */ + @Prop({ reflect: true }) placeholder?: string; + + /** Value in display format (e.g., "2026/01/21 13:07:04" for default dateFormat + timeFormat) */ + @Prop({ reflect: true, mutable: true }) value?: string = ''; + + /** Luxon date format for display (e.g., 'yyyy/LL/dd' → "2026/01/20") */ + @Prop() dateFormat: string = 'yyyy/LL/dd'; + + /** Luxon time format for display (e.g., 'HH:mm:ss' → "13:07:04") */ + @Prop() timeFormat: string = 'HH:mm:ss'; + + /** Locale for date/time formatting (e.g., 'en-US', 'de-DE') */ + @Prop() locale?: string; + + /** Whether the field is required */ + @Prop() required: boolean = false; + + /** Whether the input is disabled */ + @Prop() disabled: boolean = false; + + /** Whether the input is read-only (calendar icon hidden) */ + @Prop() readonly: boolean = false; + + /** Minimum allowed date in date format (matching dateFormat, e.g., "2026/01/20") */ + @Prop() minDate?: string; + + /** Maximum allowed date in date format (matching dateFormat, e.g., "2026/12/31") */ + @Prop() maxDate?: string; + + /** Label text displayed above the input */ + @Prop() label?: string; + + /** Helper text displayed below the input */ + @Prop() helperText?: string; + + /** Validation message for invalid state */ + @Prop({ reflect: true }) invalidText?: string; + + /** Informational message */ + @Prop() infoText?: string; + + /** Warning message */ + @Prop() warningText?: string; + + /** Success/valid message */ + @Prop() validText?: string; + + /** Show helper text as tooltip instead of below input */ + @Prop() showTextAsTooltip: boolean = false; + + /** Error message when datetime cannot be parsed */ + @Prop() i18nErrorDateTimeUnparsable: string = 'Date time is not valid'; + + /** Text for confirm button in picker (prop name matches datetime-picker) */ + @Prop() i18nDone: string = 'Confirm'; + + /** Header text for time picker section */ + @Prop() i18nTime: string = 'Time'; + + /** ARIA label for previous month navigation button */ + @Prop() ariaLabelPreviousMonthButton?: string; + + /** ARIA label for next month navigation button */ + @Prop() ariaLabelNextMonthButton?: string; + + /** ARIA label for the calendar icon button */ + @Prop() ariaLabelCalendarButton?: string; + + /** Show week numbers in date picker */ + @Prop() showWeekNumbers: boolean = false; + + /** First day of week (0=Sunday, 1=Monday, etc.) */ + @Prop() weekStartIndex: number = 0; + + /** Prevent form submission when Enter is pressed */ + @Prop({ reflect: true }) suppressSubmitOnEnter: boolean = false; + + /** Text alignment within the input field */ + @Prop() textAlignment: 'start' | 'end' = 'start'; + + /** Emitted when the datetime value changes. Payload is display format or undefined */ + @Event() valueChange!: EventEmitter; + + /** Emitted when validation state changes */ + @Event() validityStateChange!: EventEmitter; + + /** Emitted when the input receives focus */ + @Event() ixFocus!: EventEmitter; + + /** Emitted when the input loses focus */ + @Event() ixBlur!: EventEmitter; + + /** Whether the current input value is invalid */ + @State() isInputInvalid: boolean = false; + + /** Validation state: Invalid */ + @State() isInvalid: boolean = false; + + /** Validation state: Valid */ + @State() isValid: boolean = false; + + /** Validation state: Info */ + @State() isInfo: boolean = false; + + /** Validation state: Warning */ + @State() isWarning: boolean = false; + + private readonly slotStartRef = makeRef(); + private readonly slotEndRef = makeRef(); + private readonly inputElementRef = makeRef(); + private readonly dropdownElementRef = makeRef(); + private readonly datetimePickerRef = makeRef(); + + /** Dropdown open/closed state */ + @State() show: boolean = false; + + /** Date value for picker (in picker's format) */ + @State() from?: string | null = null; + + /** Time value for picker (in picker's format) */ + @State() time?: string | null = null; + + private classObserver?: ClassMutationObserver; + private invalidReason?: string; + private touched = false; + private disposableChangesAndVisibilityObservers?: DisposableChangesAndVisibilityObservers; + + @Watch('value') + watchValuePropHandler(newValue: string) { + this.onInput(newValue); + this.syncPickerState(); + } + + private get combinedFormat(): string { + return `${this.dateFormat} ${this.timeFormat}`; + } + + private syncPickerState() { + if (!this.value) { + this.from = null; + this.time = null; + return; + } + + const dateTime = DateTime.fromFormat(this.value, this.combinedFormat, { + locale: this.locale, + }); + + if (dateTime.isValid) { + this.from = dateTime.toFormat(this.dateFormat); + this.time = dateTime.toFormat(this.timeFormat); + } else { + this.from = null; + this.time = null; + } + } + + async onInput(value: string | undefined) { + this.value = value; + if (!value) { + this.isInputInvalid = false; + this.invalidReason = undefined; + this.formInternals.setFormValue(null); + this.valueChange.emit(value); + return; + } + + if (!this.dateFormat || !this.timeFormat) { + return; + } + + const dateTime = DateTime.fromFormat(value, this.combinedFormat, { + locale: this.locale, + }); + + const minDateTime = this.parseConstraintDate(this.minDate, 'start'); + const maxDateTime = this.parseConstraintDate(this.maxDate, 'end'); + + const validationResult = this.validateConstraints( + dateTime, + minDateTime, + maxDateTime + ); + + this.isInputInvalid = validationResult.isInvalid; + this.invalidReason = validationResult.reason; + + if (this.isInputInvalid) { + this.from = null; + this.time = null; + this.formInternals.setFormValue(null); + } else { + this.formInternals.setFormValue(value); + this.closeDropdown(); + } + + this.valueChange.emit(value); + } + + private parseConstraintDate( + dateString: string | undefined, + boundary: 'start' | 'end' + ): DateTime | null { + if (!dateString) { + return null; + } + + const parsed = DateTime.fromFormat(dateString, this.dateFormat, { + locale: this.locale, + }); + + if (!parsed.isValid) { + return null; + } + + return boundary === 'start' ? parsed.startOf('day') : parsed.endOf('day'); + } + + private validateConstraints( + dateTime: DateTime, + minDateTime: DateTime | null, + maxDateTime: DateTime | null + ): { isInvalid: boolean; reason: string | undefined } { + const isFormatInvalid = !dateTime.isValid; + const isBeforeMin = !!( + minDateTime?.isValid && + dateTime.isValid && + dateTime < minDateTime + ); + const isAfterMax = !!( + maxDateTime?.isValid && + dateTime.isValid && + dateTime > maxDateTime + ); + + const isInvalid = isFormatInvalid || isBeforeMin || isAfterMax; + + let reason: string | undefined; + if (isBeforeMin) { + reason = 'rangeUnderflow'; + } else if (isAfterMax) { + reason = 'rangeOverflow'; + } else if (isFormatInvalid) { + reason = dateTime.invalidReason || undefined; + } + + return { isInvalid, reason }; + } + + private handleInputKeyDown(event: KeyboardEvent) { + handleSubmitOnEnterKeydown( + event, + this.suppressSubmitOnEnter, + this.formInternals.form + ); + } + + private initPickerValues() { + this.syncPickerState(); + + // If no value is set, initialize picker to current datetime for better UX + // (shows current time scrolled into view, but nothing is selected until user clicks) + if (!this.value) { + const now = DateTime.now(); + if (now.isValid) { + this.from = now.toFormat(this.dateFormat); + this.time = now.toFormat(this.timeFormat); + } + } + } + + private readonly onCalendarClick = (event: Event) => { + handleIconClick( + event, + this.show, + () => this.openDropdown(), + this.inputElementRef + ); + }; + + private async openDropdown() { + this.initPickerValues(); + return openDropdownUtil(this.dropdownElementRef); + } + + private async closeDropdown() { + return closeDropdownUtil(this.dropdownElementRef); + } + + updateFormInternalValue(value: string | undefined): void { + if (value) { + this.formInternals.setFormValue(value); + } else { + this.formInternals.setFormValue(null); + } + } + + /** + * Returns whether the input has a value. + * @internal + */ + @Method() + hasValidValue(): Promise { + return Promise.resolve(!!this.value); + } + + /** + * Returns the associated HTML form element. + * @internal + */ + @Method() + getAssociatedFormElement(): Promise { + return Promise.resolve(this.formInternals.form); + } + + /** + * Get the native input element + * @internal + */ + @Method() + getNativeInputElement(): Promise { + return this.inputElementRef.waitForCurrent(); + } + + /** + * Focus the native input element + * @internal + */ + @Method() + async focusInput(): Promise { + return (await this.getNativeInputElement()).focus(); + } + + /** + * Returns whether the input field has been touched. + * @internal + */ + @Method() + isTouched(): Promise { + return Promise.resolve(this.touched); + } + + /** + * Returns the validity state of the input. + * @internal + */ + @Method() + getValidityState(): Promise { + return Promise.resolve( + createValidityState(this.isInputInvalid, !!this.required, this.value) + ); + } + + @Watch('isInputInvalid') + async onInputValidationChange() { + this.isInvalid = this.isInputInvalid; + + const state = await this.getValidityState(); + const validityState: DateTimeInputValidityState = { + valid: state.valid, + valueMissing: state.valueMissing, + rangeUnderflow: this.invalidReason === 'rangeUnderflow', + rangeOverflow: this.invalidReason === 'rangeOverflow', + typeMismatch: !!( + this.invalidReason && + this.invalidReason !== 'rangeUnderflow' && + this.invalidReason !== 'rangeOverflow' + ), + customError: state.customError, + badInput: state.badInput, + patternMismatch: false, + stepMismatch: state.stepMismatch, + tooLong: state.tooLong, + tooShort: state.tooShort, + invalidReason: this.invalidReason, + }; + this.validityStateChange.emit(validityState); + } + + @HookValidationLifecycle() + hookValidationLifecycle({ + isInfo, + isInvalid, + isInvalidByRequired, + isValid, + isWarning, + }: ValidationResults) { + this.isInvalid = isInvalid || isInvalidByRequired || this.isInputInvalid; + this.isInfo = isInfo; + this.isValid = isValid; + this.isWarning = isWarning; + } + + connectedCallback(): void { + this.classObserver = createClassMutationObserver(this.hostElement, () => + this.checkClassList() + ); + + this.disposableChangesAndVisibilityObservers = + addDisposableChangesAndVisibilityObservers( + this.hostElement, + this.updatePaddings.bind(this) + ); + } + + componentWillLoad(): void { + this.onInput(this.value); + if (this.isInputInvalid) { + this.from = null; + this.time = null; + } else { + this.syncPickerState(); + } + + this.checkClassList(); + this.updateFormInternalValue(this.value); + } + + private updatePaddings() { + adjustPaddingForStartAndEnd( + this.slotStartRef.current, + this.slotEndRef.current, + this.inputElementRef.current + ); + } + + disconnectedCallback(): void { + this.classObserver?.destroy(); + this.disposableChangesAndVisibilityObservers?.(); + } + + private checkClassList() { + this.isInvalid = this.hostElement.classList.contains('ix-invalid'); + } + + private readonly handleDateSelect = (event: CustomEvent) => { + const { from, time } = event.detail; + + if (!from || !time) { + return; + } + + const displayValue = `${from} ${time}`; + this.onInput(displayValue); + }; + + private renderInput() { + return ( +
+ this.updatePaddings()} + > + { + this.ixBlur.emit(); + this.touched = true; + }} + onClick={(event) => { + if (this.show) { + event.stopPropagation(); + event.preventDefault(); + } + }} + onFocus={async () => { + this.openDropdown(); + this.ixFocus.emit(); + }} + onInput={(event) => { + const target = event.target as HTMLInputElement; + this.onInput(target.value); + }} + onKeyDown={(event) => this.handleInputKeyDown(event)} + > + this.updatePaddings()} + > + this.onCalendarClick(event)} + > + +
+ ); + } + + render() { + const invalidText = getValidationText( + this.isInputInvalid, + this.invalidText, + this.i18nErrorDateTimeUnparsable + ); + + return ( + + + {this.renderInput()} + + { + this.show = event.detail; + }} + > + + + + ); + } +} diff --git a/packages/core/src/components/datetime-input/datetime-input.types.ts b/packages/core/src/components/datetime-input/datetime-input.types.ts new file mode 100644 index 00000000000..097b4b2dc6d --- /dev/null +++ b/packages/core/src/components/datetime-input/datetime-input.types.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 Siemens AG + * SPDX-License-Identifier: MIT + */ + +export interface DateTimeInputValidityState extends ValidityState { + invalidReason?: string; +} diff --git a/packages/core/src/components/datetime-input/test/datetime-input.ct.ts b/packages/core/src/components/datetime-input/test/datetime-input.ct.ts new file mode 100644 index 00000000000..6ec2dce0d20 --- /dev/null +++ b/packages/core/src/components/datetime-input/test/datetime-input.ct.ts @@ -0,0 +1,605 @@ +/* + * SPDX-FileCopyrightText: 2026 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import { Locator, expect } from '@playwright/test'; +import { + getFormValue, + preventFormSubmission, + regressionTest, +} from '@utils/test'; + +const createDateTimeInputAccessor = async (dateTimeInput: Locator) => { + return { + openByCalendar: async () => { + const trigger = dateTimeInput.getByTestId('open-datetime-picker'); + await trigger.click(); + }, + selectDay: async (day: number) => { + const dayButton = dateTimeInput + .locator(`ix-date-picker .calendar-item[date-calender-day]`) + .filter({ hasText: new RegExp(`^${day}$`) }); + await dayButton.click(); + }, + selectTime: async (hour: number, minute: number, second: number) => { + await dateTimeInput + .locator(`[data-element-container-id="hour-${hour}"]`) + .click(); + await dateTimeInput + .locator(`[data-element-container-id="minute-${minute}"]`) + .click(); + await dateTimeInput + .locator(`[data-element-container-id="second-${second}"]`) + .click(); + }, + confirm: async () => { + const confirmButton = dateTimeInput.locator( + 'ix-datetime-picker .btn-select-date' + ); + await confirmButton.click(); + }, + }; +}; + +regressionTest('renders', async ({ mount, page }) => { + await mount( + `` + ); + const dateTimeInputElement = page.locator('ix-datetime-input'); + await expect(dateTimeInputElement).toHaveClass(/hydrated/); +}); + +regressionTest('displays initial value correctly', async ({ mount, page }) => { + await mount( + `` + ); + const input = page.locator('ix-datetime-input input'); + await expect(input).toHaveValue('2024/05/05 09:10:11'); +}); + +regressionTest('handles empty initial state', async ({ mount, page }) => { + await mount( + `` + ); + const input = page.locator('ix-datetime-input input'); + await expect(input).toHaveValue(''); + await expect(input).toHaveAttribute('placeholder', 'Select date and time'); +}); + +regressionTest( + 'select date and time by open calendar trigger', + async ({ mount, page }) => { + await mount( + `` + ); + const dateTimeInputElement = page.locator('ix-datetime-input'); + await expect(dateTimeInputElement).toHaveClass(/hydrated/); + + const dateTimeInput = + await createDateTimeInputAccessor(dateTimeInputElement); + await dateTimeInput.openByCalendar(); + + await dateTimeInput.selectDay(15); + await dateTimeInput.selectTime(14, 30, 45); + await dateTimeInput.confirm(); + + await expect(dateTimeInputElement).toHaveAttribute( + 'value', + '2024/05/15 14:30:45' + ); + await expect( + dateTimeInputElement.getByTestId('datetime-dropdown') + ).not.toHaveClass(/show/); + } +); + +regressionTest('calendar button toggles picker', async ({ mount, page }) => { + await mount( + `` + ); + + const dropdown = page.locator('ix-dropdown[data-testid="datetime-dropdown"]'); + const calendarButton = page.locator( + 'ix-icon-button[data-testid="open-datetime-picker"]' + ); + + await calendarButton.click(); + await expect(dropdown).toHaveClass(/show/); + + await calendarButton.click(); + await expect(dropdown).not.toHaveClass(/show/); +}); + +regressionTest( + 'calendar button hidden when readonly', + async ({ mount, page }) => { + await mount( + `` + ); + + const calendarButton = page.locator( + 'ix-icon-button[data-testid="open-datetime-picker"]' + ); + await expect(calendarButton).toHaveClass(/calendar-hidden/); + } +); + +regressionTest( + 'calendar button disabled when disabled', + async ({ mount, page }) => { + await mount( + `` + ); + + const input = page.locator('ix-datetime-input input'); + const calendarButton = page.locator( + 'ix-icon-button[data-testid="open-datetime-picker"]' + ); + + await expect(input).toBeDisabled(); + await expect(calendarButton).toHaveClass(/calendar-hidden/); + } +); + +regressionTest('select date and time by input', async ({ mount, page }) => { + await mount( + `` + ); + const dateTimeInputElement = page.locator('ix-datetime-input'); + await expect(dateTimeInputElement).toHaveClass(/hydrated/); + + const dateTimeInput = await createDateTimeInputAccessor(dateTimeInputElement); + await dateTimeInputElement.locator('input').focus(); + await expect( + dateTimeInputElement.getByTestId('datetime-dropdown') + ).toHaveClass(/show/); + + await dateTimeInputElement.locator('input').fill('2025/10/10 14:30:45'); + + await expect( + dateTimeInputElement.getByTestId('datetime-dropdown') + ).not.toHaveClass(/show/); + await expect(dateTimeInputElement).toHaveAttribute( + 'value', + '2025/10/10 14:30:45' + ); + + await dateTimeInput.openByCalendar(); + + await expect( + dateTimeInputElement.locator('.calendar-item.selected') + ).toHaveText('10'); + await expect( + dateTimeInputElement.locator( + '[data-element-container-id="hour-14"].selected' + ) + ).toBeVisible(); +}); + +regressionTest('valid input closes dropdown', async ({ mount, page }) => { + await mount( + `` + ); + + const dateTimeInputElement = page.locator('ix-datetime-input'); + const input = dateTimeInputElement.locator('input'); + const calendarButton = page.locator( + 'ix-icon-button[data-testid="open-datetime-picker"]' + ); + const dropdown = page.locator('ix-dropdown[data-testid="datetime-dropdown"]'); + + await calendarButton.click(); + await expect(dropdown).toHaveClass(/show/); + + await input.fill('2025/10/10 14:30:45'); + await expect(dropdown).not.toHaveClass(/show/); + await expect(dateTimeInputElement).toHaveAttribute( + 'value', + '2025/10/10 14:30:45' + ); +}); + +regressionTest('invalid input shows error', async ({ mount, page }) => { + await mount( + `` + ); + + const input = page.locator('ix-datetime-input input'); + await input.fill('invalid-datetime'); + + await expect(input).toHaveClass(/is-invalid/); + await expect(page.locator('ix-field-wrapper')).toContainText( + 'Date time is not valid' + ); +}); + +regressionTest('select date and time by focus', async ({ mount, page }) => { + await mount( + `` + ); + const dateTimeInputElement = page.locator('ix-datetime-input'); + await expect(dateTimeInputElement).toHaveClass(/hydrated/); + + const dateTimeInput = await createDateTimeInputAccessor(dateTimeInputElement); + await dateTimeInputElement.locator('input').focus(); + + await dateTimeInput.selectDay(20); + await dateTimeInput.selectTime(15, 45, 30); + await dateTimeInput.confirm(); + + await expect(dateTimeInputElement).toHaveAttribute( + 'value', + '2024/05/20 15:45:30' + ); + await expect( + dateTimeInputElement.getByTestId('datetime-dropdown') + ).not.toHaveClass(/show/); +}); + +regressionTest('select date and time from picker', async ({ mount, page }) => { + await mount( + `` + ); + + const calendarButton = page.locator( + 'ix-icon-button[data-testid="open-datetime-picker"]' + ); + await calendarButton.click(); + + const dayButton = page + .locator('ix-date-picker .calendar-item[date-calender-day]') + .filter({ hasText: /^15$/ }); + await dayButton.click(); + + await page + .locator('ix-time-picker [data-element-container-id="hour-14"]') + .click(); + await page + .locator('ix-time-picker [data-element-container-id="minute-30"]') + .click(); + await page + .locator('ix-time-picker [data-element-container-id="second-45"]') + .click(); + + await page.locator('ix-datetime-picker .btn-select-date').click(); + + const dateTimeInput = page.locator('ix-datetime-input'); + await expect(dateTimeInput).toHaveAttribute('value', '2024/05/15 14:30:45'); + + const dropdown = page.locator('ix-dropdown[data-testid="datetime-dropdown"]'); + await expect(dropdown).not.toHaveClass(/show/); +}); + +regressionTest('picker syncs with input value', async ({ mount, page }) => { + await mount( + `` + ); + + const calendarButton = page.locator( + 'ix-icon-button[data-testid="open-datetime-picker"]' + ); + await calendarButton.click(); + + const selectedDay = page.locator('ix-date-picker .calendar-item.selected'); + await expect(selectedDay).toHaveText('15'); + + const hourElement = page.locator( + 'ix-time-picker [data-element-container-id="hour-14"]' + ); + const minuteElement = page.locator( + 'ix-time-picker [data-element-container-id="minute-30"]' + ); + const secondElement = page.locator( + 'ix-time-picker [data-element-container-id="second-45"]' + ); + + await expect(hourElement).toHaveClass(/selected/); + await expect(minuteElement).toHaveClass(/selected/); + await expect(secondElement).toHaveClass(/selected/); +}); + +regressionTest('input changes reflect in picker', async ({ mount, page }) => { + await mount( + `` + ); + + const input = page.locator('ix-datetime-input input'); + const calendarButton = page.locator( + 'ix-icon-button[data-testid="open-datetime-picker"]' + ); + + await input.fill('2024/05/20 15:45:30'); + + await calendarButton.click(); + + const selectedDay = page.locator('ix-date-picker .calendar-item.selected'); + await expect(selectedDay).toHaveText('20'); + + const hourElement = page.locator( + 'ix-time-picker [data-element-container-id="hour-15"]' + ); + const minuteElement = page.locator( + 'ix-time-picker [data-element-container-id="minute-45"]' + ); + const secondElement = page.locator( + 'ix-time-picker [data-element-container-id="second-30"]' + ); + + await expect(hourElement).toHaveClass(/selected/); + await expect(minuteElement).toHaveClass(/selected/); + await expect(secondElement).toHaveClass(/selected/); +}); + +regressionTest('invalid date format shows error', async ({ mount, page }) => { + await mount( + `` + ); + + const input = page.locator('ix-datetime-input input'); + await input.fill('2025/13/45 25:70:99'); + + await expect(input).toHaveClass(/is-invalid/); + await expect(page.locator('ix-field-wrapper')).toContainText( + 'Date time is not valid' + ); +}); + +regressionTest('validates minDate constraint', async ({ mount, page }) => { + await mount(` + + `); + + const input = page.locator('ix-datetime-input input'); + await expect(input).toHaveClass(/is-invalid/); +}); + +regressionTest('validates maxDate constraint', async ({ mount, page }) => { + await mount(` + + `); + + const input = page.locator('ix-datetime-input input'); + await expect(input).toHaveClass(/is-invalid/); +}); + +regressionTest( + 'validates minDate with date boundary', + async ({ mount, page }) => { + await mount(` + + `); + + const input = page.locator('ix-datetime-input input'); + await expect(input).toHaveClass(/is-invalid/); + } +); + +regressionTest( + 'validates maxDate with date boundary', + async ({ mount, page }) => { + await mount(` + + `); + + const input = page.locator('ix-datetime-input input'); + await expect(input).not.toHaveClass(/is-invalid/); + } +); + +regressionTest( + 'validates minDate with date-only format', + async ({ mount, page }) => { + await mount(` + + `); + + const input = page.locator('ix-datetime-input input'); + await expect(input).toHaveClass(/is-invalid/); + } +); + +regressionTest('required field validation', async ({ mount, page }) => { + await mount( + `` + ); + + const dateTimeInput = page.locator('ix-datetime-input'); + const input = dateTimeInput.locator('input'); + + await expect(dateTimeInput).toHaveAttribute('required'); + await expect(dateTimeInput.locator('ix-field-label')).toHaveText( + 'Appointment*' + ); + + await input.focus(); + await input.blur(); + + await expect(dateTimeInput).toHaveClass(/ix-invalid--required/); +}); + +regressionTest('recovers from invalid state', async ({ mount, page }) => { + await mount( + `` + ); + + const dateTimeInputElement = page.locator('ix-datetime-input'); + const input = dateTimeInputElement.locator('input'); + + await input.fill('invalid-datetime'); + await expect(input).toHaveClass(/is-invalid/); + + await input.fill('2024/06/15 10:30:00'); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateTimeInputElement).toHaveAttribute( + 'value', + '2024/06/15 10:30:00' + ); +}); + +regressionTest('form-ready - basic submission', async ({ mount, page }) => { + await mount(` +
+ +
+ `); + + const formElement = page.locator('form'); + preventFormSubmission(formElement); + + const input = page.locator('ix-datetime-input input'); + await input.fill('2024/05/05 14:30:00'); + await input.blur(); + + const formData = await getFormValue(formElement, 'appointment-time', page); + expect(formData).toBe('2024/05/05 14:30:00'); +}); + +regressionTest('form-ready - initial value', async ({ mount, page }) => { + await mount(` +
+ +
+ `); + + const formElement = page.locator('form'); + preventFormSubmission(formElement); + + const formData = await getFormValue(formElement, 'appointment-time', page); + expect(formData).toBe('2024/12/25 10:00:00'); +}); + +regressionTest( + 'updating value attribute updates validity', + async ({ mount, page }) => { + await mount( + `` + ); + + const dateTimeInput = page.locator('ix-datetime-input'); + const input = page.locator('input'); + + await dateTimeInput.evaluateHandle((el) => { + el.setAttribute('value', 'invalid-datetime'); + }); + await expect(input).toHaveClass(/is-invalid/); + + await dateTimeInput.evaluateHandle((el) => { + el.setAttribute('value', '2024/06/10 12:30:45'); + }); + await expect(input).not.toHaveClass(/is-invalid/); + } +); + +regressionTest('respects custom format props', async ({ mount, page }) => { + await mount(` + + `); + + const input = page.locator('ix-datetime-input input'); + await expect(input).toHaveValue('15-06-2024 14:30:45'); +}); + +regressionTest('respects locale prop', async ({ mount, page }) => { + await mount(` + + `); + + const input = page.locator('ix-datetime-input input'); + await expect(input).toHaveValue(/2024/); +}); + +regressionTest( + 'shows default error message for invalid input', + async ({ mount, page }) => { + await mount( + `` + ); + + const input = page.locator('ix-datetime-input input'); + await input.fill('invalid-datetime'); + + await expect(page.locator('ix-field-wrapper')).toContainText( + 'Date time is not valid' + ); + } +); + +regressionTest( + 'invalidText takes precedence over i18n', + async ({ mount, page }) => { + await mount(` + + `); + + const input = page.locator('ix-datetime-input input'); + await input.fill('invalid-datetime'); + await input.blur(); + + await expect( + page + .locator('ix-field-wrapper') + .locator('ix-typography') + .filter({ hasText: 'Custom error message' }) + ).toHaveText('Custom error message'); + } +); + +regressionTest('handles empty value', async ({ mount, page }) => { + await mount(``); + + const calendarButton = page.locator( + 'ix-icon-button[data-testid="open-datetime-picker"]' + ); + await calendarButton.click(); + + const dropdown = page.locator('ix-dropdown[data-testid="datetime-dropdown"]'); + await expect(dropdown).toHaveClass(/show/); +}); + +regressionTest('handles rapid value changes', async ({ mount, page }) => { + await mount( + `` + ); + + const input = page.locator('ix-datetime-input input'); + + await input.fill('2024/01/01 00:00:00'); + await input.fill('2024/06/15 12:30:45'); + await input.fill('2024/12/31 23:59:59'); + + await expect(input).toHaveValue('2024/12/31 23:59:59'); + await expect(input).not.toHaveClass(/is-invalid/); +}); diff --git a/packages/core/src/components/datetime-picker/datetime-picker.scss b/packages/core/src/components/datetime-picker/datetime-picker.scss index dee44c54f85..b8f177605a3 100644 --- a/packages/core/src/components/datetime-picker/datetime-picker.scss +++ b/packages/core/src/components/datetime-picker/datetime-picker.scss @@ -12,18 +12,11 @@ :host { display: block; - background-color: var(--theme-menu--background); - border-radius: 4px; position: relative; width: min-content; @include component.ix-component; - ix-layout-grid { - border-radius: 0.25rem; - box-shadow: var(--theme-shadow-4); - } - ix-time-picker { width: 100%; } @@ -44,12 +37,8 @@ flex-direction: column; } - .row-separator, .col-separator { border-bottom: 0.0625rem solid var(--theme-datepicker-separator--background); - } - - .col-separator { border-right: none; } @@ -87,7 +76,8 @@ } .btn-select-date-container { - padding: vars.$default-space; + display: flex; + justify-content: flex-end; @media (max-width: 576px) { & .btn-select-date { diff --git a/packages/core/src/components/datetime-picker/datetime-picker.tsx b/packages/core/src/components/datetime-picker/datetime-picker.tsx index 119cf404c57..97143f55074 100644 --- a/packages/core/src/components/datetime-picker/datetime-picker.tsx +++ b/packages/core/src/components/datetime-picker/datetime-picker.tsx @@ -123,6 +123,9 @@ export class DatetimePicker */ @Prop() showWeekNumbers = false; + /** @internal */ + @Prop() embedded = false; + /** * Time change */ @@ -171,53 +174,57 @@ export class DatetimePicker render() { return ( - - - - (this.datePickerElement = ref)} - corners="left" - singleSelection={this.singleSelection} - onDateChange={(event) => this.onDateChange(event)} - from={this.from} - to={this.to} - format={this.dateFormat} - minDate={this.minDate} - maxDate={this.maxDate} - weekStartIndex={this.weekStartIndex} - embedded - locale={this.locale} - showWeekNumbers={this.showWeekNumbers} - ariaLabelPreviousMonthButton={this.ariaLabelPreviousMonthButton} - ariaLabelNextMonthButton={this.ariaLabelNextMonthButton} - > - - - - (this.timePickerElement = ref)} - embedded - dateTimePickerAppearance={true} - onTimeChange={(event) => this.onTimeChange(event)} - format={this.timeFormat} - time={this.time} - > - - - - -
- this.onDone()} - > - {this.i18nDone} - -
-
-
-
+ + + + + (this.datePickerElement = ref)} + corners="left" + singleSelection={this.singleSelection} + onDateChange={(event) => this.onDateChange(event)} + from={this.from} + to={this.to} + format={this.dateFormat} + minDate={this.minDate} + maxDate={this.maxDate} + weekStartIndex={this.weekStartIndex} + embedded + locale={this.locale} + showWeekNumbers={this.showWeekNumbers} + ariaLabelPreviousMonthButton={ + this.ariaLabelPreviousMonthButton + } + ariaLabelNextMonthButton={this.ariaLabelNextMonthButton} + > + + + + (this.timePickerElement = ref)} + embedded + dateTimePickerAppearance={true} + onTimeChange={(event) => this.onTimeChange(event)} + format={this.timeFormat} + time={this.time} + > + + + + +
+ this.onDone()}> + {this.i18nDone} + +
+
); } diff --git a/packages/core/src/components/time-picker/time-picker.tsx b/packages/core/src/components/time-picker/time-picker.tsx index eb091f66459..1ae9cf4bac1 100644 --- a/packages/core/src/components/time-picker/time-picker.tsx +++ b/packages/core/src/components/time-picker/time-picker.tsx @@ -530,23 +530,34 @@ export class TimePicker { } private setupVisibilityObserver() { - let dropdownElement: Element | null = this.hostElement; - while (dropdownElement && dropdownElement.tagName !== 'IX-DROPDOWN') { - dropdownElement = dropdownElement.parentElement; - } + let currentElement: Element | null = this.hostElement; - if (!dropdownElement) { - return; - } + while (currentElement) { + if (currentElement.tagName === 'IX-DROPDOWN') { + this.visibilityObserver = new MutationObserver((mutations) => + this.mutationObserverCallback(mutations) + ); - this.visibilityObserver = new MutationObserver((mutations) => - this.mutationObserverCallback(mutations) - ); + this.visibilityObserver.observe(currentElement, { + attributes: true, + attributeFilter: ['class', 'style'], + }); + return; + } - this.visibilityObserver.observe(dropdownElement, { - attributes: true, - attributeFilter: ['class', 'style'], - }); + // Try to go up via parentElement first + if (currentElement.parentElement) { + currentElement = currentElement.parentElement; + } else { + // We hit a shadow boundary, try to get the host element + const rootNode = currentElement.getRootNode(); + if (rootNode && (rootNode as ShadowRoot).host) { + currentElement = (rootNode as ShadowRoot).host as Element; + } else { + break; + } + } + } } private mutationObserverCallback(mutations: MutationRecord[]) { diff --git a/packages/html-test-app/src/preview-examples/datetime-input-disabled.html b/packages/html-test-app/src/preview-examples/datetime-input-disabled.html new file mode 100644 index 00000000000..473dd0faa3f --- /dev/null +++ b/packages/html-test-app/src/preview-examples/datetime-input-disabled.html @@ -0,0 +1,22 @@ + + + + + + + + DateTime input disabled example + + + + + + + diff --git a/packages/html-test-app/src/preview-examples/datetime-input-label.html b/packages/html-test-app/src/preview-examples/datetime-input-label.html new file mode 100644 index 00000000000..27bee91fc5b --- /dev/null +++ b/packages/html-test-app/src/preview-examples/datetime-input-label.html @@ -0,0 +1,27 @@ + + + + + + + + DateTime input with label and custom format example + + + + + + + diff --git a/packages/html-test-app/src/preview-examples/datetime-input-min-max-date.html b/packages/html-test-app/src/preview-examples/datetime-input-min-max-date.html new file mode 100644 index 00000000000..3890952ce73 --- /dev/null +++ b/packages/html-test-app/src/preview-examples/datetime-input-min-max-date.html @@ -0,0 +1,26 @@ + + + + + + + + DateTime input with min/max date example + + + + + + + diff --git a/packages/html-test-app/src/preview-examples/datetime-input-readonly.html b/packages/html-test-app/src/preview-examples/datetime-input-readonly.html new file mode 100644 index 00000000000..6199a6096da --- /dev/null +++ b/packages/html-test-app/src/preview-examples/datetime-input-readonly.html @@ -0,0 +1,22 @@ + + + + + + + + DateTime input readonly example + + + + + + + diff --git a/packages/html-test-app/src/preview-examples/datetime-input-validation.html b/packages/html-test-app/src/preview-examples/datetime-input-validation.html new file mode 100644 index 00000000000..9efd22f0849 --- /dev/null +++ b/packages/html-test-app/src/preview-examples/datetime-input-validation.html @@ -0,0 +1,57 @@ + + + + + + + + DateTime input validation example + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + + diff --git a/packages/html-test-app/src/preview-examples/datetime-input-with-slots.html b/packages/html-test-app/src/preview-examples/datetime-input-with-slots.html new file mode 100644 index 00000000000..71c84e15cd8 --- /dev/null +++ b/packages/html-test-app/src/preview-examples/datetime-input-with-slots.html @@ -0,0 +1,33 @@ + + + + + + + + DateTime input with slots example + + + + + + Slot + + + + + diff --git a/packages/html-test-app/src/preview-examples/datetime-input.html b/packages/html-test-app/src/preview-examples/datetime-input.html new file mode 100644 index 00000000000..acd5a99311d --- /dev/null +++ b/packages/html-test-app/src/preview-examples/datetime-input.html @@ -0,0 +1,22 @@ + + + + + + + + DateTime input basic example + + + + + + + diff --git a/packages/html-test-app/src/preview-examples/quick-test.js b/packages/html-test-app/src/preview-examples/quick-test.js new file mode 100644 index 00000000000..d0c31261ffe --- /dev/null +++ b/packages/html-test-app/src/preview-examples/quick-test.js @@ -0,0 +1,34 @@ +// Quick DateTime Input Test - Copy and paste into browser console + +console.clear(); +console.log('🧪 Quick DateTime Input Test\n'); + +const inputs = document.querySelectorAll('ix-datetime-input'); +console.log(`Found ${inputs.length} datetime-input components\n`); + +inputs.forEach((input, i) => { + const label = input.getAttribute('label') || `Input ${i + 1}`; + const nativeInput = input.shadowRoot?.querySelector('input'); + const value = nativeInput?.value || '(empty)'; + const disabled = input.hasAttribute('disabled'); + const readonly = input.hasAttribute('readonly'); + const required = input.hasAttribute('required'); + + console.log(`${i + 1}. ${label}`); + console.log(` Value: ${value}`); + console.log(` States: ${disabled ? 'DISABLED' : ''} ${readonly ? 'READONLY' : ''} ${required ? 'REQUIRED' : ''}`); + console.log(''); +}); + +// Test typing in first input +console.log('Testing manual input...'); +const firstInput = inputs[0]?.shadowRoot?.querySelector('input'); +if (firstInput) { + firstInput.value = '2026/01/21 14:30:00'; + firstInput.dispatchEvent(new Event('input', { bubbles: true })); + console.log('✅ Typed: 2026/01/21 14:30:00'); +} else { + console.log('❌ Could not find input element'); +} + +console.log('\n✅ Test complete! Check the components in the page.'); diff --git a/packages/react-test-app/src/main.tsx b/packages/react-test-app/src/main.tsx index e8d188f7b42..dcfa604272b 100644 --- a/packages/react-test-app/src/main.tsx +++ b/packages/react-test-app/src/main.tsx @@ -63,6 +63,13 @@ import DateInputWithSlots from './preview-examples/date-input-with-slots'; import Datepicker from './preview-examples/datepicker'; import DatepickerLocale from './preview-examples/datepicker-locale'; import DatepickerRange from './preview-examples/datepicker-range'; +import DatetimeInput from './preview-examples/datetime-input'; +import DatetimeInputDisabled from './preview-examples/datetime-input-disabled'; +import DatetimeInputLabel from './preview-examples/datetime-input-label'; +import DatetimeInputMinMaxDate from './preview-examples/datetime-input-min-max-date'; +import DatetimeInputReadonly from './preview-examples/datetime-input-readonly'; +import DatetimeInputValidation from './preview-examples/datetime-input-validation'; +import DatetimeInputWithSlots from './preview-examples/datetime-input-with-slots'; import Datetimepicker from './preview-examples/datetimepicker'; import Divider from './preview-examples/divider'; import Drawer from './preview-examples/drawer'; @@ -115,11 +122,11 @@ import GroupCustomEntry from './preview-examples/group-custom-entry'; import GroupHeaderSuppressed from './preview-examples/group-header-suppressed'; import HtmlTable from './preview-examples/html-table'; import HtmlTableStriped from './preview-examples/html-table-striped'; -import IconToggleButtonTertiary from './preview-examples/icon-toggle-button-tertiary'; -import IconToggleButtonSubtleTertiary from './preview-examples/icon-toggle-button-subtle-tertiary'; import IconToggleButtonSecondary from './preview-examples/icon-toggle-button-secondary'; -import IconToggleButtonSubtleSecondary from './preview-examples/icon-toggle-button-subtle-secondary'; import IconToggleButtonSubtlePrimary from './preview-examples/icon-toggle-button-subtle-primary'; +import IconToggleButtonSubtleSecondary from './preview-examples/icon-toggle-button-subtle-secondary'; +import IconToggleButtonSubtleTertiary from './preview-examples/icon-toggle-button-subtle-tertiary'; +import IconToggleButtonTertiary from './preview-examples/icon-toggle-button-tertiary'; import Input from './preview-examples/input'; import InputDisabled from './preview-examples/input-disabled'; import InputLabel from './preview-examples/input-label'; @@ -210,17 +217,17 @@ import Toast from './preview-examples/toast'; import ToastCustom from './preview-examples/toast-custom'; import ToastPosition from './preview-examples/toast-position'; import Toggle from './preview-examples/toggle'; -import ToggleButtonTertiary from './preview-examples/toggle-button-tertiary'; -import ToggleButtonSubtlePrimary from './preview-examples/toggle-button-subtle-primary'; import ToggleButtonSecondary from './preview-examples/toggle-button-secondary'; +import ToggleButtonSubtlePrimary from './preview-examples/toggle-button-subtle-primary'; import ToggleButtonSubtleSecondary from './preview-examples/toggle-button-subtle-secondary'; import ToggleButtonSubtleTertiary from './preview-examples/toggle-button-subtle-tertiary'; -import TooltipWithIcon from './preview-examples/tooltip-with-icon'; +import ToggleButtonTertiary from './preview-examples/toggle-button-tertiary'; import ToggleChecked from './preview-examples/toggle-checked'; import ToggleCustomLabel from './preview-examples/toggle-custom-label'; import ToggleDisabled from './preview-examples/toggle-disabled'; import ToggleIndeterminate from './preview-examples/toggle-indeterminate'; import Tooltip from './preview-examples/tooltip'; +import TooltipWithIcon from './preview-examples/tooltip-with-icon'; import Tree from './preview-examples/tree'; import TreeCustom from './preview-examples/tree-custom'; import Upload from './preview-examples/upload'; @@ -692,6 +699,31 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( path="/preview/date-input-with-slots" element={} /> + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> } /> } /> } /> diff --git a/packages/react-test-app/src/preview-examples/datetime-input-disabled.tsx b/packages/react-test-app/src/preview-examples/datetime-input-disabled.tsx new file mode 100644 index 00000000000..42e478b0438 --- /dev/null +++ b/packages/react-test-app/src/preview-examples/datetime-input-disabled.tsx @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { IxDatetimeInput } from '@siemens/ix-react'; + +function DatetimeInputDisabled() { + return ; +} + +export default DatetimeInputDisabled; diff --git a/packages/react-test-app/src/preview-examples/datetime-input-label.tsx b/packages/react-test-app/src/preview-examples/datetime-input-label.tsx new file mode 100644 index 00000000000..b16651be150 --- /dev/null +++ b/packages/react-test-app/src/preview-examples/datetime-input-label.tsx @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { IxDatetimeInput } from '@siemens/ix-react'; + +function DatetimeInputLabel() { + return ( + + ); +} + +export default DatetimeInputLabel; diff --git a/packages/react-test-app/src/preview-examples/datetime-input-min-max-date.tsx b/packages/react-test-app/src/preview-examples/datetime-input-min-max-date.tsx new file mode 100644 index 00000000000..b378811a49d --- /dev/null +++ b/packages/react-test-app/src/preview-examples/datetime-input-min-max-date.tsx @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { IxDatetimeInput } from '@siemens/ix-react'; + +function DatetimeInputMinMaxDate() { + return ( + + ); +} + +export default DatetimeInputMinMaxDate; diff --git a/packages/react-test-app/src/preview-examples/datetime-input-readonly.tsx b/packages/react-test-app/src/preview-examples/datetime-input-readonly.tsx new file mode 100644 index 00000000000..12268341354 --- /dev/null +++ b/packages/react-test-app/src/preview-examples/datetime-input-readonly.tsx @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { IxDatetimeInput } from '@siemens/ix-react'; + +function DatetimeInputReadonly() { + return ; +} + +export default DatetimeInputReadonly; diff --git a/packages/react-test-app/src/preview-examples/datetime-input-validation.tsx b/packages/react-test-app/src/preview-examples/datetime-input-validation.tsx new file mode 100644 index 00000000000..55173107f57 --- /dev/null +++ b/packages/react-test-app/src/preview-examples/datetime-input-validation.tsx @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { IxDatetimeInput } from '@siemens/ix-react'; + +function DatetimeInputValidation() { + return ( + <> +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + ); +} + +export default DatetimeInputValidation; diff --git a/packages/react-test-app/src/preview-examples/datetime-input-with-slots.tsx b/packages/react-test-app/src/preview-examples/datetime-input-with-slots.tsx new file mode 100644 index 00000000000..4a726c7873d --- /dev/null +++ b/packages/react-test-app/src/preview-examples/datetime-input-with-slots.tsx @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { IxDatetimeInput, IxIcon, IxTypography } from '@siemens/ix-react'; + +function DatetimeInputWithSlots() { + return ( + + + Slot + + ); +} + +export default DatetimeInputWithSlots; diff --git a/packages/react-test-app/src/preview-examples/datetime-input.tsx b/packages/react-test-app/src/preview-examples/datetime-input.tsx new file mode 100644 index 00000000000..5a384797a01 --- /dev/null +++ b/packages/react-test-app/src/preview-examples/datetime-input.tsx @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2024 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { IxDatetimeInput } from '@siemens/ix-react'; + +function DatetimeInput() { + return ; +} + +export default DatetimeInput; diff --git a/packages/react/src/components.server.ts b/packages/react/src/components.server.ts index 5e023d86cb4..55398a77e0f 100644 --- a/packages/react/src/components.server.ts +++ b/packages/react/src/components.server.ts @@ -8,7 +8,7 @@ // @ts-ignore - ignore potential type issues as the project is importing itself import * as clientComponents from '@siemens/ix'; -import { type BorderlessChangedEvent, type CustomCloseEvent, type CustomLabelChangeEvent, type DateChangeEvent, type DateInputValidityState, type DateRangeChangeEvent, type DateTimeDateChangeEvent, type DateTimeSelectEvent, type ExpandedChangedEvent, type FilterState, type InputState, type IxBreadcrumbCustomEvent, type IxCardListCustomEvent, type IxCategoryFilterCustomEvent, type IxDateDropdownCustomEvent, type IxDateInputCustomEvent, type IxDatePickerCustomEvent, type IxDatetimePickerCustomEvent, type IxGroupItemCustomEvent, type IxInputCustomEvent, type IxMenuAboutCustomEvent, type IxMenuAboutItemCustomEvent, type IxMenuAboutNewsCustomEvent, type IxMenuAvatarItemCustomEvent, type IxMenuSettingsCustomEvent, type IxMenuSettingsItemCustomEvent, type IxModalHeaderCustomEvent, type IxNumberInputCustomEvent, type IxPaneCustomEvent, type IxSplitButtonCustomEvent, type IxTabItemCustomEvent, type IxTextareaCustomEvent, type IxTimeInputCustomEvent, type IxUploadCustomEvent, type TabClickDetail, type TimeInputValidityState, type VariantChangedEvent } from "@siemens/ix"; +import { type BorderlessChangedEvent, type CustomCloseEvent, type CustomLabelChangeEvent, type DateChangeEvent, type DateInputValidityState, type DateRangeChangeEvent, type DateTimeDateChangeEvent, type DateTimeInputValidityState, type DateTimeSelectEvent, type ExpandedChangedEvent, type FilterState, type InputState, type IxBreadcrumbCustomEvent, type IxCardListCustomEvent, type IxCategoryFilterCustomEvent, type IxDateDropdownCustomEvent, type IxDateInputCustomEvent, type IxDatePickerCustomEvent, type IxDatetimeInputCustomEvent, type IxDatetimePickerCustomEvent, type IxGroupItemCustomEvent, type IxInputCustomEvent, type IxMenuAboutCustomEvent, type IxMenuAboutItemCustomEvent, type IxMenuAboutNewsCustomEvent, type IxMenuAvatarItemCustomEvent, type IxMenuSettingsCustomEvent, type IxMenuSettingsItemCustomEvent, type IxModalHeaderCustomEvent, type IxNumberInputCustomEvent, type IxPaneCustomEvent, type IxSplitButtonCustomEvent, type IxTabItemCustomEvent, type IxTextareaCustomEvent, type IxTimeInputCustomEvent, type IxUploadCustomEvent, type TabClickDetail, type TimeInputValidityState, type VariantChangedEvent } from "@siemens/ix"; import { IxActionCard as IxActionCardElement } from "@siemens/ix/components/ix-action-card.js"; import { IxApplicationHeader as IxApplicationHeaderElement } from "@siemens/ix/components/ix-application-header.js"; import { IxApplication as IxApplicationElement } from "@siemens/ix/components/ix-application.js"; @@ -33,6 +33,7 @@ import { IxCustomField as IxCustomFieldElement } from "@siemens/ix/components/ix import { IxDateDropdown as IxDateDropdownElement } from "@siemens/ix/components/ix-date-dropdown.js"; import { IxDateInput as IxDateInputElement } from "@siemens/ix/components/ix-date-input.js"; import { IxDatePicker as IxDatePickerElement } from "@siemens/ix/components/ix-date-picker.js"; +import { IxDatetimeInput as IxDatetimeInputElement } from "@siemens/ix/components/ix-datetime-input.js"; import { IxDatetimePicker as IxDatetimePickerElement } from "@siemens/ix/components/ix-datetime-picker.js"; import { IxDivider as IxDividerElement } from "@siemens/ix/components/ix-divider.js"; import { IxDrawer as IxDrawerElement } from "@siemens/ix/components/ix-drawer.js"; @@ -609,6 +610,50 @@ export const IxDatePicker: StencilReactComponent>, + onValidityStateChange: EventName>, + onIxFocus: EventName>, + onIxBlur: EventName> +}; + +export const IxDatetimeInput: StencilReactComponent = /*@__PURE__*/ createComponent({ + tagName: 'ix-datetime-input', + properties: { + name: 'name', + placeholder: 'placeholder', + value: 'value', + dateFormat: 'date-format', + timeFormat: 'time-format', + locale: 'locale', + required: 'required', + disabled: 'disabled', + readonly: 'readonly', + minDate: 'min-date', + maxDate: 'max-date', + label: 'label', + helperText: 'helper-text', + invalidText: 'invalid-text', + infoText: 'info-text', + warningText: 'warning-text', + validText: 'valid-text', + showTextAsTooltip: 'show-text-as-tooltip', + i18nErrorDateTimeUnparsable: 'i-1-8n-error-date-time-unparsable', + i18nDone: 'i-1-8n-done', + i18nTime: 'i-1-8n-time', + ariaLabelPreviousMonthButton: 'aria-label-previous-month-button', + ariaLabelNextMonthButton: 'aria-label-next-month-button', + ariaLabelCalendarButton: 'aria-label-calendar-button', + showWeekNumbers: 'show-week-numbers', + weekStartIndex: 'week-start-index', + suppressSubmitOnEnter: 'suppress-submit-on-enter', + textAlignment: 'text-alignment' + }, + hydrateModule: import('@siemens/ix/hydrate') as Promise, + clientModule: clientComponents.IxDatetimeInput as ReactWebComponent, + serializeShadowRoot, +}); + export type IxDatetimePickerEvents = { onTimeChange: EventName>, onDateChange: EventName>, @@ -634,7 +679,8 @@ export const IxDatetimePicker: StencilReactComponent, clientModule: clientComponents.IxDatetimePicker as ReactWebComponent, diff --git a/packages/react/src/components.ts b/packages/react/src/components.ts index 9d819492a12..32ed8cebbdf 100644 --- a/packages/react/src/components.ts +++ b/packages/react/src/components.ts @@ -7,7 +7,7 @@ /* eslint-disable */ -import { type BorderlessChangedEvent, type CustomCloseEvent, type CustomLabelChangeEvent, type DateChangeEvent, type DateInputValidityState, type DateRangeChangeEvent, type DateTimeDateChangeEvent, type DateTimeSelectEvent, type ExpandedChangedEvent, type FilterState, type InputState, type IxBreadcrumbCustomEvent, type IxCardListCustomEvent, type IxCategoryFilterCustomEvent, type IxDateDropdownCustomEvent, type IxDateInputCustomEvent, type IxDatePickerCustomEvent, type IxDatetimePickerCustomEvent, type IxGroupItemCustomEvent, type IxInputCustomEvent, type IxMenuAboutCustomEvent, type IxMenuAboutItemCustomEvent, type IxMenuAboutNewsCustomEvent, type IxMenuAvatarItemCustomEvent, type IxMenuSettingsCustomEvent, type IxMenuSettingsItemCustomEvent, type IxModalHeaderCustomEvent, type IxNumberInputCustomEvent, type IxPaneCustomEvent, type IxSplitButtonCustomEvent, type IxTabItemCustomEvent, type IxTextareaCustomEvent, type IxTimeInputCustomEvent, type IxUploadCustomEvent, type TabClickDetail, type TimeInputValidityState, type VariantChangedEvent } from "@siemens/ix"; +import { type BorderlessChangedEvent, type CustomCloseEvent, type CustomLabelChangeEvent, type DateChangeEvent, type DateInputValidityState, type DateRangeChangeEvent, type DateTimeDateChangeEvent, type DateTimeInputValidityState, type DateTimeSelectEvent, type ExpandedChangedEvent, type FilterState, type InputState, type IxBreadcrumbCustomEvent, type IxCardListCustomEvent, type IxCategoryFilterCustomEvent, type IxDateDropdownCustomEvent, type IxDateInputCustomEvent, type IxDatePickerCustomEvent, type IxDatetimeInputCustomEvent, type IxDatetimePickerCustomEvent, type IxGroupItemCustomEvent, type IxInputCustomEvent, type IxMenuAboutCustomEvent, type IxMenuAboutItemCustomEvent, type IxMenuAboutNewsCustomEvent, type IxMenuAvatarItemCustomEvent, type IxMenuSettingsCustomEvent, type IxMenuSettingsItemCustomEvent, type IxModalHeaderCustomEvent, type IxNumberInputCustomEvent, type IxPaneCustomEvent, type IxSplitButtonCustomEvent, type IxTabItemCustomEvent, type IxTextareaCustomEvent, type IxTimeInputCustomEvent, type IxUploadCustomEvent, type TabClickDetail, type TimeInputValidityState, type VariantChangedEvent } from "@siemens/ix"; import { IxActionCard as IxActionCardElement, defineCustomElement as defineIxActionCard } from "@siemens/ix/components/ix-action-card.js"; import { IxApplicationHeader as IxApplicationHeaderElement, defineCustomElement as defineIxApplicationHeader } from "@siemens/ix/components/ix-application-header.js"; import { IxApplication as IxApplicationElement, defineCustomElement as defineIxApplication } from "@siemens/ix/components/ix-application.js"; @@ -32,6 +32,7 @@ import { IxCustomField as IxCustomFieldElement, defineCustomElement as defineIxC import { IxDateDropdown as IxDateDropdownElement, defineCustomElement as defineIxDateDropdown } from "@siemens/ix/components/ix-date-dropdown.js"; import { IxDateInput as IxDateInputElement, defineCustomElement as defineIxDateInput } from "@siemens/ix/components/ix-date-input.js"; import { IxDatePicker as IxDatePickerElement, defineCustomElement as defineIxDatePicker } from "@siemens/ix/components/ix-date-picker.js"; +import { IxDatetimeInput as IxDatetimeInputElement, defineCustomElement as defineIxDatetimeInput } from "@siemens/ix/components/ix-datetime-input.js"; import { IxDatetimePicker as IxDatetimePickerElement, defineCustomElement as defineIxDatetimePicker } from "@siemens/ix/components/ix-datetime-picker.js"; import { IxDivider as IxDividerElement, defineCustomElement as defineIxDivider } from "@siemens/ix/components/ix-divider.js"; import { IxDrawer as IxDrawerElement, defineCustomElement as defineIxDrawer } from "@siemens/ix/components/ix-drawer.js"; @@ -432,6 +433,27 @@ export const IxDatePicker: StencilReactComponent>, + onValidityStateChange: EventName>, + onIxFocus: EventName>, + onIxBlur: EventName> +}; + +export const IxDatetimeInput: StencilReactComponent = /*@__PURE__*/ createComponent({ + tagName: 'ix-datetime-input', + elementClass: IxDatetimeInputElement, + // @ts-ignore - ignore potential React type mismatches between the Stencil Output Target and your project. + react: React, + events: { + onValueChange: 'valueChange', + onValidityStateChange: 'validityStateChange', + onIxFocus: 'ixFocus', + onIxBlur: 'ixBlur' + } as IxDatetimeInputEvents, + defineCustomElement: defineIxDatetimeInput +}); + export type IxDatetimePickerEvents = { onTimeChange: EventName>, onDateChange: EventName>, diff --git a/packages/storybook-docs/.storybook/define-custom-elements.ts b/packages/storybook-docs/.storybook/define-custom-elements.ts index 288e8b14523..c010b082161 100644 --- a/packages/storybook-docs/.storybook/define-custom-elements.ts +++ b/packages/storybook-docs/.storybook/define-custom-elements.ts @@ -24,6 +24,7 @@ import { defineCustomElement as ixCustomField } from '@siemens/ix/components/ix- import { defineCustomElement as ixDateDropdown } from '@siemens/ix/components/ix-date-dropdown.js'; import { defineCustomElement as ixDateInput } from '@siemens/ix/components/ix-date-input.js'; import { defineCustomElement as ixDatePicker } from '@siemens/ix/components/ix-date-picker.js'; +import { defineCustomElement as ixDatetimeInput } from '@siemens/ix/components/ix-datetime-input.js'; import { defineCustomElement as ixDatetimePicker } from '@siemens/ix/components/ix-datetime-picker.js'; import { defineCustomElement as ixDivider } from '@siemens/ix/components/ix-divider.js'; import { defineCustomElement as ixDrawer } from '@siemens/ix/components/ix-drawer.js'; @@ -126,6 +127,7 @@ ixCustomField(); ixDateDropdown(); ixDateInput(); ixDatePicker(); +ixDatetimeInput(); ixDatetimePicker(); ixDivider(); ixDrawer(); diff --git a/packages/vue-test-app/src/Root.vue b/packages/vue-test-app/src/Root.vue index 988246d4e6d..46b8a1788d1 100644 --- a/packages/vue-test-app/src/Root.vue +++ b/packages/vue-test-app/src/Root.vue @@ -17,11 +17,11 @@ import AboutAndLegal from './preview-examples/about-and-legal.vue'; import ActionCard from './preview-examples/action-card.vue'; import AddIcons from './preview-examples/add-icons.vue'; import AgGrid from './preview-examples/aggrid.vue'; +import ApplicationAdvanced from './preview-examples/application-advanced.vue'; import ApplicationAppSwitch from './preview-examples/application-app-switch.vue'; import ApplicationBreakpoints from './preview-examples/application-breakpoints.vue'; import ApplicationHeader from './preview-examples/application-header.vue'; import Application from './preview-examples/application.vue'; -import ApplicationAdvanced from './preview-examples/application-advanced.vue'; import AvatarImage from './preview-examples/avatar-image.vue'; import AvatarInitials from './preview-examples/avatar-initials.vue'; import Avatar from './preview-examples/avatar.vue'; @@ -53,8 +53,8 @@ import CheckboxIndeterminate from './preview-examples/checkbox-indeterminate.vue import Checkbox from './preview-examples/checkbox.vue'; import Chip from './preview-examples/chip.vue'; import ContentHeaderNoBack from './preview-examples/content-header-no-back.vue'; -import ContentHeader from './preview-examples/content-header.vue'; import ContentHeaderWithSlot from './preview-examples/content-header-with-slot.vue'; +import ContentHeader from './preview-examples/content-header.vue'; import Content from './preview-examples/content.vue'; import CustomFieldValidation from './preview-examples/custom-field-validation.vue'; import CustomField from './preview-examples/custom-field.vue'; @@ -65,6 +65,13 @@ import DateInput from './preview-examples/date-input.vue'; import DatepickerLocale from './preview-examples/datepicker-locale.vue'; import DatepickerRange from './preview-examples/datepicker-range.vue'; import Datepicker from './preview-examples/datepicker.vue'; +import DatetimeInputDisabled from './preview-examples/datetime-input-disabled.vue'; +import DatetimeInputLabel from './preview-examples/datetime-input-label.vue'; +import DatetimeInputMinMaxDate from './preview-examples/datetime-input-min-max-date.vue'; +import DatetimeInputReadonly from './preview-examples/datetime-input-readonly.vue'; +import DatetimeInputValidation from './preview-examples/datetime-input-validation.vue'; +import DatetimeInputWithSlots from './preview-examples/datetime-input-with-slots.vue'; +import DatetimeInput from './preview-examples/datetime-input.vue'; import Datetimepicker from './preview-examples/datetimepicker.vue'; import Divider from './preview-examples/divider.vue'; import DrawerFullHeight from './preview-examples/drawer-full-height.vue'; @@ -115,13 +122,13 @@ import GroupContextMenu from './preview-examples/group-context-menu.vue'; import GroupCustomEntry from './preview-examples/group-custom-entry.vue'; import GroupHeaderSuppressed from './preview-examples/group-header-suppressed.vue'; import Group from './preview-examples/group.vue'; -import HtmlTable from './preview-examples/html-table.vue'; import HtmlTableStriped from './preview-examples/html-table-striped.vue'; -import IconToggleButtonTertiary from './preview-examples/icon-toggle-button-tertiary.vue'; -import IconToggleButtonSubtleTertiary from './preview-examples/icon-toggle-button-subtle-tertiary.vue'; -import IconToggleButtonSubtleSecondary from './preview-examples/icon-toggle-button-subtle-secondary.vue'; -import IconToggleButtonSubtlePrimary from './preview-examples/icon-toggle-button-subtle-primary.vue'; +import HtmlTable from './preview-examples/html-table.vue'; import IconToggleButtonSecondary from './preview-examples/icon-toggle-button-secondary.vue'; +import IconToggleButtonSubtlePrimary from './preview-examples/icon-toggle-button-subtle-primary.vue'; +import IconToggleButtonSubtleSecondary from './preview-examples/icon-toggle-button-subtle-secondary.vue'; +import IconToggleButtonSubtleTertiary from './preview-examples/icon-toggle-button-subtle-tertiary.vue'; +import IconToggleButtonTertiary from './preview-examples/icon-toggle-button-tertiary.vue'; import InputDisabled from './preview-examples/input-disabled.vue'; import InputLabel from './preview-examples/input-label.vue'; import InputLegacyDisabled from './preview-examples/input-legacy-disabled.vue'; @@ -149,12 +156,13 @@ import LinkButton from './preview-examples/link-button.vue'; import Loading from './preview-examples/loading.vue'; import MenuCategory from './preview-examples/menu-category.vue'; import MenuWithBottomTabs from './preview-examples/menu-with-bottom-tabs.vue'; -import MessageBar from './preview-examples/message-bar.vue'; import MessageBarRemoval from './preview-examples/message-bar-removal.vue'; +import MessageBar from './preview-examples/message-bar.vue'; import Message from './preview-examples/message.vue'; +import ModalClose from './preview-examples/modal-close.vue'; +import ModalFormIxButtonSubmit from './preview-examples/modal-form-ix-button-submit.vue'; import ModalSizes from './preview-examples/modal-sizes.vue'; import ModalExample from './preview-examples/modal.vue'; -import ModalClose from './preview-examples/modal-close.vue'; import NumberInputDisabled from './preview-examples/number-input-disabled.vue'; import NumberInputLabel from './preview-examples/number-input-label.vue'; import NumberInputReadOnly from './preview-examples/number-input-readonly.vue'; @@ -168,6 +176,12 @@ import Pane from './preview-examples/pane.vue'; import PillVariants from './preview-examples/pill-variants.vue'; import Pill from './preview-examples/pill.vue'; import PopoverNews from './preview-examples/popover-news.vue'; +import ProgressIndicatorCircularSizes from './preview-examples/progress-indicator-circular-sizes.vue'; +import ProgressIndicatorCircularStatus from './preview-examples/progress-indicator-circular-status.vue'; +import ProgressIndicatorCircular from './preview-examples/progress-indicator-circular.vue'; +import ProgressIndicatorLinearSizes from './preview-examples/progress-indicator-linear-sizes.vue'; +import ProgressIndicatorLinearStatus from './preview-examples/progress-indicator-linear-status.vue'; +import ProgressIndicator from './preview-examples/progress-indicator.vue'; import PushCard from './preview-examples/push-card.vue'; import RadioButton from './preview-examples/radio-button.vue'; import RadioDisabled from './preview-examples/radio-disabled.vue'; @@ -203,19 +217,19 @@ import Timepicker from './preview-examples/timepicker.vue'; import ToastCustom from './preview-examples/toast-custom.vue'; import ToastPosition from './preview-examples/toast-position.vue'; import Toast from './preview-examples/toast.vue'; -import ToggleButtonTertiary from './preview-examples/toggle-button-tertiary.vue'; -import ToggleButtonSubtleTertiary from './preview-examples/toggle-button-subtle-tertiary.vue'; import ToggleButtonPrimary from './preview-examples/toggle-button-primary.vue'; -import ToggleButtonSubtleSecondary from './preview-examples/toggle-button-subtle-secondary.vue'; -import ToggleButtonSubtlePrimary from './preview-examples/toggle-button-subtle-primary.vue'; import ToggleButtonSecondary from './preview-examples/toggle-button-secondary.vue'; +import ToggleButtonSubtlePrimary from './preview-examples/toggle-button-subtle-primary.vue'; +import ToggleButtonSubtleSecondary from './preview-examples/toggle-button-subtle-secondary.vue'; +import ToggleButtonSubtleTertiary from './preview-examples/toggle-button-subtle-tertiary.vue'; +import ToggleButtonTertiary from './preview-examples/toggle-button-tertiary.vue'; import ToggleChecked from './preview-examples/toggle-checked.vue'; import ToggleCustomLabel from './preview-examples/toggle-custom-label.vue'; import ToggleDisabled from './preview-examples/toggle-disabled.vue'; import ToggleIndeterminate from './preview-examples/toggle-indeterminate.vue'; import Toggle from './preview-examples/toggle.vue'; -import Tooltip from './preview-examples/tooltip.vue'; import TooltipWithIcon from './preview-examples/tooltip-with-icon.vue'; +import Tooltip from './preview-examples/tooltip.vue'; import TreeCustom from './preview-examples/tree-custom.vue'; import Tree from './preview-examples/tree.vue'; import Upload from './preview-examples/upload.vue'; @@ -225,13 +239,6 @@ import VerticalTabsWithAvatar from './preview-examples/vertical-tabs-with-avatar import VerticalTabs from './preview-examples/vertical-tabs.vue'; import WorkflowVertical from './preview-examples/workflow-vertical.vue'; import Workflow from './preview-examples/workflow.vue'; -import ModalFormIxButtonSubmit from './preview-examples/modal-form-ix-button-submit.vue'; -import ProgressIndicatorLinearStatus from './preview-examples/progress-indicator-linear-status.vue'; -import ProgressIndicatorCircularStatus from './preview-examples/progress-indicator-circular-status.vue'; -import ProgressIndicatorLinearSizes from './preview-examples/progress-indicator-linear-sizes.vue'; -import ProgressIndicatorCircularSizes from './preview-examples/progress-indicator-circular-sizes.vue'; -import ProgressIndicatorCircular from './preview-examples/progress-indicator-circular.vue'; -import ProgressIndicator from './preview-examples/progress-indicator.vue'; const routes: any = { '/': App, @@ -399,6 +406,13 @@ const routes: any = { '/preview/number-input-with-slots': NumberInputWithSlots, '/preview/date-input': DateInput, '/preview/date-input-with-slots': DateInputWithSlots, + '/preview/datetime-input': DatetimeInput, + '/preview/datetime-input-disabled': DatetimeInputDisabled, + '/preview/datetime-input-label': DatetimeInputLabel, + '/preview/datetime-input-min-max-date': DatetimeInputMinMaxDate, + '/preview/datetime-input-readonly': DatetimeInputReadonly, + '/preview/datetime-input-validation': DatetimeInputValidation, + '/preview/datetime-input-with-slots': DatetimeInputWithSlots, '/preview/workflow': Workflow, '/preview/workflow-vertical': WorkflowVertical, '/preview/tooltip': Tooltip, diff --git a/packages/vue-test-app/src/preview-examples/datetime-input-disabled.vue b/packages/vue-test-app/src/preview-examples/datetime-input-disabled.vue new file mode 100644 index 00000000000..2150aa0bdca --- /dev/null +++ b/packages/vue-test-app/src/preview-examples/datetime-input-disabled.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/vue-test-app/src/preview-examples/datetime-input-label.vue b/packages/vue-test-app/src/preview-examples/datetime-input-label.vue new file mode 100644 index 00000000000..846616421b7 --- /dev/null +++ b/packages/vue-test-app/src/preview-examples/datetime-input-label.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/vue-test-app/src/preview-examples/datetime-input-min-max-date.vue b/packages/vue-test-app/src/preview-examples/datetime-input-min-max-date.vue new file mode 100644 index 00000000000..946c2b69f40 --- /dev/null +++ b/packages/vue-test-app/src/preview-examples/datetime-input-min-max-date.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/packages/vue-test-app/src/preview-examples/datetime-input-readonly.vue b/packages/vue-test-app/src/preview-examples/datetime-input-readonly.vue new file mode 100644 index 00000000000..e25c99b773c --- /dev/null +++ b/packages/vue-test-app/src/preview-examples/datetime-input-readonly.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/vue-test-app/src/preview-examples/datetime-input-validation.vue b/packages/vue-test-app/src/preview-examples/datetime-input-validation.vue new file mode 100644 index 00000000000..e940e42cd74 --- /dev/null +++ b/packages/vue-test-app/src/preview-examples/datetime-input-validation.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/vue-test-app/src/preview-examples/datetime-input-with-slots.vue b/packages/vue-test-app/src/preview-examples/datetime-input-with-slots.vue new file mode 100644 index 00000000000..fe3024cf984 --- /dev/null +++ b/packages/vue-test-app/src/preview-examples/datetime-input-with-slots.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/vue-test-app/src/preview-examples/datetime-input.vue b/packages/vue-test-app/src/preview-examples/datetime-input.vue new file mode 100644 index 00000000000..6eccc7e8e8c --- /dev/null +++ b/packages/vue-test-app/src/preview-examples/datetime-input.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/vue/src/components.ts b/packages/vue/src/components.ts index 169ef8d5fbd..ff01514d50a 100644 --- a/packages/vue/src/components.ts +++ b/packages/vue/src/components.ts @@ -29,6 +29,7 @@ import { defineCustomElement as defineIxCustomField } from '@siemens/ix/componen import { defineCustomElement as defineIxDateDropdown } from '@siemens/ix/components/ix-date-dropdown.js'; import { defineCustomElement as defineIxDateInput } from '@siemens/ix/components/ix-date-input.js'; import { defineCustomElement as defineIxDatePicker } from '@siemens/ix/components/ix-date-picker.js'; +import { defineCustomElement as defineIxDatetimeInput } from '@siemens/ix/components/ix-datetime-input.js'; import { defineCustomElement as defineIxDatetimePicker } from '@siemens/ix/components/ix-datetime-picker.js'; import { defineCustomElement as defineIxDivider } from '@siemens/ix/components/ix-divider.js'; import { defineCustomElement as defineIxDrawer } from '@siemens/ix/components/ix-drawer.js'; @@ -475,6 +476,47 @@ export const IxDatePicker: StencilVueComponent = /*@__PURE__*/ ]); +export const IxDatetimeInput: StencilVueComponent = /*@__PURE__*/ defineContainer('ix-datetime-input', defineIxDatetimeInput, [ + 'name', + 'placeholder', + 'value', + 'dateFormat', + 'timeFormat', + 'locale', + 'required', + 'disabled', + 'readonly', + 'minDate', + 'maxDate', + 'label', + 'helperText', + 'invalidText', + 'infoText', + 'warningText', + 'validText', + 'showTextAsTooltip', + 'i18nErrorDateTimeUnparsable', + 'i18nDone', + 'i18nTime', + 'ariaLabelPreviousMonthButton', + 'ariaLabelNextMonthButton', + 'ariaLabelCalendarButton', + 'showWeekNumbers', + 'weekStartIndex', + 'suppressSubmitOnEnter', + 'textAlignment', + 'valueChange', + 'validityStateChange', + 'ixFocus', + 'ixBlur' +], [ + 'valueChange', + 'validityStateChange', + 'ixFocus', + 'ixBlur' +]); + + export const IxDatetimePicker: StencilVueComponent = /*@__PURE__*/ defineContainer('ix-datetime-picker', defineIxDatetimePicker, [ 'singleSelection', 'minDate', @@ -493,6 +535,7 @@ export const IxDatetimePicker: StencilVueComponent = /*@__ 'weekStartIndex', 'locale', 'showWeekNumbers', + 'embedded', 'timeChange', 'dateChange', 'dateSelect' diff --git a/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-basic-1-chromium---classic-dark-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-basic-1-chromium---classic-dark-linux.png new file mode 100644 index 00000000000..84c1b6f531c Binary files /dev/null and b/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-basic-1-chromium---classic-dark-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-basic-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-basic-1-chromium---classic-light-linux.png new file mode 100644 index 00000000000..08d7a54161d Binary files /dev/null and b/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-basic-1-chromium---classic-light-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-validation-1-chromium---classic-dark-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-validation-1-chromium---classic-dark-linux.png new file mode 100644 index 00000000000..ce0952b708e Binary files /dev/null and b/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-validation-1-chromium---classic-dark-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-validation-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-validation-1-chromium---classic-light-linux.png new file mode 100644 index 00000000000..7dfe9c11a10 Binary files /dev/null and b/testing/visual-testing/__screenshots__/tests/datetime-input/datetime-input.e2e.ts/datetime-input-validation-1-chromium---classic-light-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-light-linux.png index 52dfb7b452c..ff2b851ae60 100644 Binary files a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-light-linux.png and b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-basic-1-chromium---classic-light-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-light-linux.png index a010245cb4b..d2726b6b81e 100644 Binary files a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-light-linux.png and b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-mobile-1-chromium---classic-light-linux.png differ diff --git a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-light-linux.png b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-light-linux.png index 6c0c0672f35..2f0f590e30e 100644 Binary files a/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-light-linux.png and b/testing/visual-testing/__screenshots__/tests/datetime-picker/datetime-picker.e2e.ts/datetime-picker-show-week-numbers-1-chromium---classic-light-linux.png differ diff --git a/testing/visual-testing/tests/datetime-input/basic/index.html b/testing/visual-testing/tests/datetime-input/basic/index.html new file mode 100644 index 00000000000..68cf9de13b4 --- /dev/null +++ b/testing/visual-testing/tests/datetime-input/basic/index.html @@ -0,0 +1,67 @@ + + + + + + + + DateTime Input Test + + +
+

Default

+ + +

Disabled

+ + +

Readonly

+ + +

With Label and Helper Text

+ + +

Text Alignment End

+ + +

Custom Format

+ +
+ + + diff --git a/testing/visual-testing/tests/datetime-input/datetime-input.e2e.ts b/testing/visual-testing/tests/datetime-input/datetime-input.e2e.ts new file mode 100644 index 00000000000..215ddf68d41 --- /dev/null +++ b/testing/visual-testing/tests/datetime-input/datetime-input.e2e.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 Siemens AG + * + * SPDX-License-Identifier: MIT + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { expect } from '@playwright/test'; +import { regressionTest } from '@utils/test'; + +regressionTest.describe('datetime-input', () => { + regressionTest('basic', async ({ page }) => { + await page.goto('datetime-input/basic'); + expect(await page.screenshot({ fullPage: true })).toMatchSnapshot({ + maxDiffPixels: 25, + }); + }); + + regressionTest('validation', async ({ page }) => { + await page.goto('datetime-input/validation'); + expect(await page.screenshot({ fullPage: true })).toMatchSnapshot({ + maxDiffPixels: 25, + }); + }); +}); diff --git a/testing/visual-testing/tests/datetime-input/validation/index.html b/testing/visual-testing/tests/datetime-input/validation/index.html new file mode 100644 index 00000000000..ca8cbafd682 --- /dev/null +++ b/testing/visual-testing/tests/datetime-input/validation/index.html @@ -0,0 +1,66 @@ + + + + + + + + DateTime Input Validation Test + + +
+

Info

+ + +

Warning

+ + +

Valid

+ + +

Invalid

+ + +

Required (empty)

+ +
+ + +