blob: 326cef89ffb68920392b4e8e422bb818ba876ad5 [file] [log] [blame]
/**
* @module modules/calendar-input-sk
* @description <h2><code>calendar-input-sk</code></h2>
*
* Displays a text input for the date along with a button that pops up a dialog
* allowing the user to select the date from a calendar-sk.
*
* @evt input - A CustomEvent<Date> with the new date.
*
*/
import { define } from 'elements-sk/define';
import { html } from 'lit-html';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import 'elements-sk/icon/date-range-icon-sk';
import dialogPolyfill from 'dialog-polyfill';
import '../calendar-sk';
import { CalendarSk } from '../calendar-sk/calendar-sk';
export class CalendarInputSk extends ElementSk {
private static template = (ele: CalendarInputSk) => html`
<label>
<input
@change=${ele.inputChangeHandler}
type="text"
pattern="[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}"
title="Date in YYYY-MM-DD format."
placeholder="yyyy-mm-dd"
.value="${ele._displayDate.getFullYear()}-${ele._displayDate.getMonth() +
1}-${ele._displayDate.getDate()}"
/>
<span class="invalid" aria-live="polite" title="Date is invalid.">
&cross;
</span>
</label>
<button
class="calendar"
@click=${ele.openHandler}
title="Open calendar dialog to choose the date."
>
<date-range-icon-sk></date-range-icon-sk>
</button>
<dialog @cancel=${ele.dialogCancelHandler}>
<calendar-sk
@change=${ele.calendarChangeHandler}
.displayDate=${ele.displayDate}
></calendar-sk>
<button @click=${ele.dialogCancelHandler}>Cancel</button>
</dialog>
`;
private dialog: HTMLDialogElement | null = null;
private calendar: CalendarSk | null = null;
private input: HTMLInputElement | null = null;
private _displayDate: Date = new Date();
// These two functions store the callbacks from a Promise, which allows the
// openHandler() function to be a nice linear function.
private resolve: ((value?: any) => void) | null = null;
private reject: ((reason?: any) => void) | null = null;
constructor() {
super(CalendarInputSk.template);
}
connectedCallback() {
super.connectedCallback();
this._render();
this.dialog = this.querySelector('dialog')!;
this.calendar = this.querySelector<CalendarSk>('calendar-sk');
this.input = this.querySelector('input');
dialogPolyfill.registerDialog(this.dialog);
}
private inputChangeHandler(e: InputEvent) {
// We don't change event from the input element to be confused with the
// 'change' event that CalendarInputSk will produce.
e.stopPropagation();
e.preventDefault();
const inputElement = e.target! as HTMLInputElement;
if (inputElement.validity.patternMismatch) {
return;
}
// Since we have checked validity above we can parse the values with
// confidence here.
const dateString = inputElement.value;
const parts = dateString.split('-');
try {
this.displayDate = new Date(+parts[0], +parts[1] - 1, +parts[2]);
} catch (error) {
return;
}
this.sendEvent();
}
private async openHandler() {
const keyboardHandler = (e: KeyboardEvent) =>
this.calendar!.keyboardHandler(e);
try {
this.dialog!.showModal();
this.dialog!.addEventListener('keydown', keyboardHandler);
const date = await new Promise<Date>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
this.displayDate = date;
this.sendEvent();
this.input!.focus();
} catch (_) {
// Cancel button was pressed.
} finally {
this.dialog!.removeEventListener('keydown', keyboardHandler);
}
}
private sendEvent() {
this.dispatchEvent(
new CustomEvent<Date>('input', {
detail: this.displayDate,
bubbles: true,
})
);
}
private calendarChangeHandler(e: CustomEvent<Date>) {
this.dialog!.close();
this.resolve!(e.detail);
}
private dialogCancelHandler() {
this.dialog!.close();
this.reject!();
}
/** The default date, if not set defaults to today. */
get displayDate() {
return this._displayDate;
}
set displayDate(val) {
this._displayDate = val;
this._render();
}
/** @prop locale - The locale, used just for testing. */
get locale() {
return this.calendar!.locale;
}
set locale(val) {
this.calendar!.locale = val;
}
}
define('calendar-input-sk', CalendarInputSk);