Skip to content
Open
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
@@ -1,12 +1,12 @@
import { customElement, css } from '@umbraco-cms/backoffice/external/lit';
import { UUIInputElement } from '@umbraco-cms/backoffice/external/uui';
import { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';

// eslint-disable-next-line @typescript-eslint/naming-convention
export type InputDateType = 'date' | 'time' | 'datetime-local';

/**
* This element passes a datetime string to a regular HTML input element.
* Be aware that you cannot include a time demonination, i.e. "10:44:00" if you
* Be aware that you cannot include a time denomination, i.e. "10:44:00" if you
* set the input type of this element to "date". If you do, the browser will not show
* the value at all.
* @element umb-input-date
Expand All @@ -28,6 +28,38 @@ export class UmbInputDateElement extends UUIInputElement {
constructor() {
super();
this.type = 'date'; // Default to 'date'

// On focusout, sync our value with whatever the native input shows now. This
// captures an intentional clear that #onInput skipped while focus was inside.
// Dispatch input and change so downstream listeners learn about the committed
// value — the native change event that fires at blur was emitted before this
// sync ran, and so carried the stale (pre-clear) value.
this.addEventListener('focusout', () => {
const native = this.shadowRoot?.querySelector('input');
if (native && this.value !== native.value) {
this.value = native.value;
this.dispatchEvent(new UUIInputEvent(UUIInputEvent.INPUT));
this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE));
}
});
}

override onInput(e: Event) {
e.stopPropagation();
const target = e.target as HTMLInputElement;

// The browser reports `.value === ''` while a date/datetime-local/time input is
// in a transiently invalid state (e.g. a single "0" typed into a segment before
// the second digit). Reflecting that empty value back via `this.value = ''` would
// trigger a re-render that writes `''` to the native input, which clears all
// segments visually and wipes the digit just typed. Skip both the reflection and
// the input event in this case; focusout commits the final value.
if (target.value === '') {
return;
}

this.value = target.value;
this.dispatchEvent(new UUIInputEvent(UUIInputEvent.INPUT));
Comment thread
AndyButland marked this conversation as resolved.
}

// Adding styles override to add a darkmode version.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { UmbInputDateElement } from './input-date.element.js';
import { expect, fixture, html } from '@open-wc/testing';
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
describe('UmbInputDateElement', () => {
let element: UmbInputDateElement;
Expand All @@ -17,4 +17,46 @@ describe('UmbInputDateElement', () => {
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
});
}

describe('transient invalid input', () => {
it('does not reflect an empty native value back while the input is being edited', async () => {
element.value = '2026-05-27';
await element.updateComplete;

let inputEvents = 0;
element.addEventListener('input', () => inputEvents++);

const native = element.shadowRoot!.querySelector('input')!;
native.value = '';
native.dispatchEvent(new Event('input', { bubbles: true, composed: true }));

expect(element.value).to.equal('2026-05-27');
expect(inputEvents).to.equal(0);
});

it('reflects a non-empty native value back via the input event', async () => {
element.value = '2026-05-27';
await element.updateComplete;

const native = element.shadowRoot!.querySelector('input')!;
native.value = '2026-04-27';
native.dispatchEvent(new Event('input', { bubbles: true, composed: true }));

expect(element.value).to.equal('2026-04-27');
});

it('commits an emptied native value on focusout and dispatches a change event', async () => {
element.value = '2026-05-27';
await element.updateComplete;

const native = element.shadowRoot!.querySelector('input')!;
native.value = '';

const changeEvent = oneEvent(element, 'change');
native.dispatchEvent(new FocusEvent('focusout', { bubbles: true, composed: true }));
await changeEvent;

expect(element.value).to.equal('');
});
});
});
Loading