Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
@state()
private _dateInputStep: number = 1;

@state()
private _min?: string;

@state()
private _max?: string;

@state()
private _selectedDate?: DateTime;

Expand Down Expand Up @@ -117,6 +123,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
if (!config) return;

const timeFormat = config.getValueByAlias<string>('timeFormat');
this._min = this.#formatMinMaxValue(config.getValueByAlias<string>('min'), 'min');
this._max = this.#formatMinMaxValue(config.getValueByAlias<string>('max'), 'max');
let timeZonePickerConfig: UmbTimeZonePickerValue | undefined = undefined;

if (this._displayTimeZone) {
Expand Down Expand Up @@ -256,6 +264,34 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
}
}

// A native date/datetime-local/time input ignores min/max unless they match the input's own
// value format, so normalise the configured bound to the active input type (e.g. a date-only
// bound like "2026-07-01" must gain a time component to constrain a datetime-local input).
#formatMinMaxValue(value: string | undefined, bound: 'min' | 'max'): string | undefined {
if (!value) return undefined;

let dateTime = DateTime.fromISO(value);
if (!dateTime.isValid) {
dateTime = DateTime.fromSQL(value);
}
if (!dateTime.isValid) return value;

switch (this._dateInputType) {
case 'date':
return dateTime.toFormat('yyyy-MM-dd');
case 'time':
return dateTime.toFormat('HH:mm:ss');
default: {
// A date-only bound should cover the whole day on a datetime-local field, mirroring
// how a date field treats it: the lower bound starts the day, the upper bound ends it.
if (/^\d{4}-\d{2}-\d{2}$/.test(value.trim())) {
dateTime = bound === 'max' ? dateTime.endOf('day') : dateTime.startOf('day');
}
return dateTime.toFormat("yyyy-MM-dd'T'HH:mm:ss");
}
}
}

#setTimeInputStep(timeFormat: string | undefined) {
switch (timeFormat) {
case 'HH:mm':
Expand Down Expand Up @@ -396,6 +432,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
<umb-input-date
label=${this.localize.term('placeholders_enterdate')}
.value=${this._datePickerValue}
.min=${this._min}
.max=${this._max}
.step=${this._dateInputStep}
type=${this._dateInputType}
@change=${this.#onValueChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { UmbInputDateElement } from '@umbraco-cms/backoffice/components';
import { expect, fixture, html } from '@open-wc/testing';
import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import UmbPropertyEditorUIDateTimePickerElement from './date-time-picker/property-editor-ui-date-time-picker.element.js';
import UmbPropertyEditorUIDateOnlyPickerElement from './date-only-picker/property-editor-ui-date-only-picker.element.js';
import UmbPropertyEditorUITimeOnlyPickerElement from './time-only-picker/property-editor-ui-time-only-picker.element.js';
Expand All @@ -16,6 +18,54 @@
expect(element).to.be.instanceOf(UmbPropertyEditorUIDateTimePickerElement);
});

it('expands a date-only bound to span the whole day on a datetime-local field', async () => {
element.config = new UmbPropertyEditorConfigCollection([
{ alias: 'min', value: '2026-07-01' },
{ alias: 'max', value: '2026-07-30' },
]);
await element.updateComplete;
const inputElement = element.shadowRoot!.querySelector('umb-input-date') as UmbInputDateElement;
expect(inputElement.min).to.equal('2026-07-01T00:00:00');
expect(inputElement.max).to.equal('2026-07-30T23:59:59');
});

Check warning on line 30 in src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker.test.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v17/dev)

❌ New issue: Code Duplication

The module contains 6 functions with similar structure: 'applies min/max config to the date input as date-only values','applies min/max config to the date input as time values','applies min/max config to the date input, normalised for a datetime-local field','expands a date-only bound to span the whole day on a datetime-local field' and 2 more functions Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

it('preserves an explicit time on a datetime-local bound', async () => {
element.config = new UmbPropertyEditorConfigCollection([
{ alias: 'min', value: '2026-07-01T09:30' },
{ alias: 'max', value: '2026-07-30T17:45' },
]);
await element.updateComplete;
const inputElement = element.shadowRoot!.querySelector('umb-input-date') as UmbInputDateElement;
expect(inputElement.min).to.equal('2026-07-01T09:30:00');
expect(inputElement.max).to.equal('2026-07-30T17:45:00');
});

it('parses a stored SQL-style datetime bound', async () => {
element.config = new UmbPropertyEditorConfigCollection([
{ alias: 'min', value: '2026-07-01 08:15:00' },
{ alias: 'max', value: '2026-07-30 17:45:00' },
]);
await element.updateComplete;
const inputElement = element.shadowRoot!.querySelector('umb-input-date') as UmbInputDateElement;
expect(inputElement.min).to.equal('2026-07-01T08:15:00');
expect(inputElement.max).to.equal('2026-07-30T17:45:00');
});

it('passes an unparseable bound through unchanged', async () => {
element.config = new UmbPropertyEditorConfigCollection([{ alias: 'min', value: 'not-a-date' }]);
await element.updateComplete;
const inputElement = element.shadowRoot!.querySelector('umb-input-date') as UmbInputDateElement;
expect(inputElement.min).to.equal('not-a-date');
});

it('leaves a bound unset when its config value is absent', async () => {
element.config = new UmbPropertyEditorConfigCollection([{ alias: 'min', value: '2026-07-01' }]);
await element.updateComplete;
const inputElement = element.shadowRoot!.querySelector('umb-input-date') as UmbInputDateElement;
expect(inputElement.min).to.equal('2026-07-01T00:00:00');
expect(inputElement.max).to.be.undefined;
});

if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
Expand All @@ -34,6 +84,17 @@
expect(element).to.be.instanceOf(UmbPropertyEditorUIDateTimeWithTimeZonePickerElement);
});

it('applies min/max config to the date input, normalised for a datetime-local field', async () => {
element.config = new UmbPropertyEditorConfigCollection([
{ alias: 'min', value: '2026-07-01' },
{ alias: 'max', value: '2026-07-30' },
]);
Comment thread
AndyButland marked this conversation as resolved.
await element.updateComplete;
const inputElement = element.shadowRoot!.querySelector('umb-input-date') as UmbInputDateElement;
expect(inputElement.min).to.equal('2026-07-01T00:00:00');
expect(inputElement.max).to.equal('2026-07-30T23:59:59');
});

if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
Expand All @@ -53,6 +114,17 @@
expect(element).to.be.instanceOf(UmbPropertyEditorUIDateOnlyPickerElement);
});

it('applies min/max config to the date input as date-only values', async () => {
element.config = new UmbPropertyEditorConfigCollection([
{ alias: 'min', value: '2026-07-01' },
{ alias: 'max', value: '2026-07-30' },
]);
await element.updateComplete;
const inputElement = element.shadowRoot!.querySelector('umb-input-date') as UmbInputDateElement;
expect(inputElement.min).to.equal('2026-07-01');
expect(inputElement.max).to.equal('2026-07-30');
});

if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
Expand All @@ -71,6 +143,17 @@
expect(element).to.be.instanceOf(UmbPropertyEditorUITimeOnlyPickerElement);
});

it('applies min/max config to the date input as time values', async () => {
element.config = new UmbPropertyEditorConfigCollection([
{ alias: 'min', value: '09:00:00' },
{ alias: 'max', value: '17:30:00' },
]);
await element.updateComplete;
const inputElement = element.shadowRoot!.querySelector('umb-input-date') as UmbInputDateElement;
expect(inputElement.min).to.equal('09:00:00');
expect(inputElement.max).to.equal('17:30:00');
});

if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
Expand Down
Loading