Skip to content

Commit

Permalink
test(datepicker): add constraint validation tests
Browse files Browse the repository at this point in the history
  • Loading branch information
clukhei committed Jan 3, 2025
1 parent b211195 commit ab4e449
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 71 deletions.
7 changes: 4 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ <h2>Badge</h2>
<div class="container">
<h2>Form Validation</h2>
<form id="validation-form">
<!-- <input type="date" min="2018-01-01" max="2018-08-29"/>

<sgds-file-upload required multiple hinttext="test" name="fileuploadtest" hasFeedback>
File upload
<sgds-icon slot="icon" size="md" name="placeholder"></sgds-icon>
Expand Down Expand Up @@ -1023,9 +1023,10 @@ <h2>Form Validation</h2>
</sgds-radio-group>
<sgds-textarea minlength="3" required hasFeedback resize="auto">
<sgds-icon slot="invalidIcon" size="md" name="placeholder"></sgds-icon>
</sgds-textarea> -->
</sgds-textarea>
<!-- <sgds-datepicker required name="datepicker-hi"></sgds-datepicker> -->
<sgds-datepicker id="test-date" required name="datepicker-hi" mode="range" initialValue='["20/01/2025", "20/01/2027"]'></sgds-datepicker>
<sgds-datepicker id="test-date" required name="datepicker-hi" mindate="2024-01-02T12:00:00.000Z"
maxdate="2024-12-30T12:00:00.000Z"></sgds-datepicker>
<!--<sgds-datepicker required mode="range"></sgds-datepicker>
<sgds-datepicker required mode="range" mindate="2024-01-02T12:00:00.000Z"
maxdate="2024-12-30T12:00:00.000Z"></sgds-datepicker> -->
Expand Down
2 changes: 0 additions & 2 deletions src/components/Datepicker/datepicker-calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,6 @@ export class DatepickerCalendar extends SgdsElement {
connectedCallback(): void {
super.connectedCallback();
this.addEventListener("keydown", this._handleKeyPress);
/** Stop blur event from bubbling from calendar, as we want blur to happen for entire datepicker */
this.addEventListener("blur", e => e.stopPropagation());
}

updated() {
Expand Down
17 changes: 5 additions & 12 deletions src/components/Datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { isAfter, isBefore, isValid, parse } from "date-fns";
import IMask, { InputMask } from "imask";
import { html, PropertyValues } from "lit";
import { html } from "lit";
import { property, queryAsync } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { DATE_PATTERNS, setTimeToNoon } from "../../utils/time";
import { watch } from "../../utils/watch";
import { SgdsInput } from "../Input/sgds-input";
import datepickerInputStyles from "./datepicker-input.css";
export type DateFormat = "MM/DD/YYYY" | "DD/MM/YYYY" | "YYYY/MM/DD";
Expand All @@ -31,17 +30,11 @@ export class DatepickerInput extends SgdsInput {
super();
this.type = "text";
this.hasFeedback = "both";
// this._handleChange = () => null;
// this._handleInputChange = () => null
}
connectedCallback(): void {
super.connectedCallback();
/** Stop blur event from bubbling from input, as we want blur to happen for entire datepicker */
this.addEventListener("blur", e => e.stopPropagation());
}
protected override _handleBlur() {
this.emit("sgds-blur");
this._handleBlur = () => null;
}
// protected override _handleBlur() {
// this.emit("sgds-blur");
// }
protected override async _handleChange(e: Event) {
this.value = this.input.value;
this.emit("sgds-change");
Expand Down
88 changes: 45 additions & 43 deletions src/components/Datepicker/sgds-datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ export type DateFormat = "MM/DD/YYYY" | "DD/MM/YYYY" | "YYYY/MM/DD";
*/
export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) implements SgdsFormControl {
static styles = [...DropdownElement.styles, dropdownStyle, dropdownMenuStyle, datepickerStyle];
// /**@internal */
// static formAssociated = true;
// private _internals: ElementInternals;

/**@internal */
static dependencies = {
"sgds-datepicker-input": DatepickerInput,
Expand Down Expand Up @@ -100,22 +96,20 @@ export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) impl

@state() private focusedTabIndex = 3;

@state() private _isTouched = false;
private isValueEmpty() {
return this.value === "" || this.value === "DD/MM/YYYY" || this.value === "DD/MM/YYYY - DD/MM/YYYY";
}

private initialDisplayDate: Date = new Date()
private initialDisplayDate: Date = new Date();

@queryAsync("sgds-datepicker-calendar")
private calendar: Promise<DatepickerCalendar>;

@queryAsync("sgds-datepicker-input")
private datepickerInputAsync: Promise<DatepickerInput>;

@queryAsync("sgds-datepicker-header")
private datepickerHeaderAsync: Promise<DatepickerHeader>;

@query("sgds-datepicker-input")
private datepickerInput: DatepickerInput;

/**
* Checks for validity. Under the hood, HTMLFormElement's reportValidity method calls this method to check for component's validity state
* Note that the native error popup is prevented for SGDS form components by default. Instead the validation message shows up in the feedback container of SgdsInput
Expand Down Expand Up @@ -160,10 +154,11 @@ export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) impl
this.addEventListener("sgds-selectyear", this._handleSelectYear);
this.addEventListener("sgds-selectdates", this._handleSelectDatesAndClose);
this.addEventListener("sgds-selectdates-input", this._handleSelectDatesInput);
this.addEventListener("sgds-empty-input", this._handleEmptyInput);
this.addEventListener("keydown", this._handleTab);
this.addEventListener("sgds-hide", this._handleCloseMenu);
this.addEventListener("sgds-show", this._handleOpenMenu);
this.addEventListener("blur", () => (this._isTouched = true));
this.addEventListener("blur", this._mixinCheckValidity);
this.initialDisplayDate = this.displayDate || new Date();
if (this.initialValue && this.initialValue.length > 0) {
// Validate initialValue against the dateFormat regex
Expand Down Expand Up @@ -229,14 +224,6 @@ export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) impl
this.emit("sgds-change-date");
}

/** @internal */
@watch("_isTouched", { waitUntilFirstUpdate: true })
_handleIsTouched() {
if (this._isTouched && this.required && this.value === "") {
this.invalid = true;
}
}

private async _handleCloseMenu() {
//return focus to input when menu closes
const input = await this.datepickerInputAsync;
Expand Down Expand Up @@ -299,12 +286,8 @@ export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) impl
const input = await this.datepickerInputAsync;
input.updateMaskValue();
this._manageInternalsValid();

}

private get _shadowInput() {
return this.datepickerInput.shadowRoot.querySelector("input");
}
private async _handleSelectDatesAndClose(event: CustomEvent<Date[]>) {
await this._handleSelectDates(event.detail);

Expand Down Expand Up @@ -336,11 +319,13 @@ export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) impl
}
private async _handleInvalidInput() {
this.selectedDateRange = [];
console.log(this.initialDisplayDate)
this.displayDate = this.initialDisplayDate;

this._manageInternalsBadInput();
}
private async _handleEmptyInput() {
this._manageEmptyInput();
}
private async _resetDatepicker() {
this.displayDate = this.initialDisplayDate;
this.selectedDateRange = [];
Expand All @@ -352,6 +337,9 @@ export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) impl
await input.applyInputMask();

this._mixinResetValidity(input);
if (this.isValueEmpty()) {
this._handleEmptyInput();
}
}

private _manageInternalsBadInput() {
Expand All @@ -360,7 +348,21 @@ export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) impl
badInput: true
},
"Invalid date input",
this._shadowInput
this.datepickerInput
);
}
/**
* Even though element internals handles the required constraint validation. This custom one is still needed as
* datepicker input has a special case where the default input mask "DD/MM/YYYY" means an empty input.
* However, the required constraint validation sees "DD/MM/YYYY" as a non-empty input.
*/
private _manageEmptyInput() {
this._mixinSetValidity(
{
valueMissing: true
},
"Please fill in this field",
this.datepickerInput
);
}

Expand All @@ -385,7 +387,7 @@ export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) impl
private async _handleInputMaskChange(e: CustomEvent) {
this.value = e.detail;

if (this.value === "DD/MM/YYYY" || this.value === "DD/MM/YYYY - DD/MM/YYYY") {
if (this.isValueEmpty()) {
this._resetDatepicker();
}
}
Expand Down Expand Up @@ -416,24 +418,24 @@ export class SgdsDatepicker extends SgdsFormValidatorMixin(DropdownElement) impl
?invalid=${this.invalid}
>
</sgds-datepicker-input>
<sgds-icon-button
<sgds-icon-button
${ref(this.myDropdown)}
role="button"
class=${classMap({
"calendar-btn": true,
"with-hint-text": this.hintText || this.invalid,
"with-label": this.label,
})}
aria-expanded="${this.menuIsOpen}"
aria-haspopup="dialog"
aria-controls=${this.dropdownMenuId}
@click=${() => this.toggleMenu()}
ariaLabel=${this.menuIsOpen ? "Close Calendar" : "Open Calendar"}
?disabled=${this.disabled}
variant="outline"
name="calendar"
>
</sgds-icon-button>
role="button"
class=${classMap({
"calendar-btn": true,
"with-hint-text": this.hintText || this.invalid,
"with-label": this.label
})}
aria-expanded="${this.menuIsOpen}"
aria-haspopup="dialog"
aria-controls=${this.dropdownMenuId}
@click=${() => this.toggleMenu()}
ariaLabel=${this.menuIsOpen ? "Close Calendar" : "Open Calendar"}
?disabled=${this.disabled}
variant="outline"
name="calendar"
>
</sgds-icon-button>
<ul
id=${this.dropdownMenuId}
class="sgds datepicker dropdown-menu"
Expand Down
56 changes: 45 additions & 11 deletions test/datepicker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe("sgds-datepicker", () => {
calendarBtnEl?.click();

tdButtonOne?.click();
await waitUntil(() => !menuEl?.classList.contains("show"))
await waitUntil(() => !menuEl?.classList.contains("show"));
expect(inputEl?.value).to.contain("01");

expect(menuEl?.classList.contains("show")).to.be.false;
Expand All @@ -126,7 +126,7 @@ describe("sgds-datepicker", () => {
calendarBtnEl?.click();

tdButtonTwo?.click();
await waitUntil(() => !menuEl?.classList.contains("show"))
await waitUntil(() => !menuEl?.classList.contains("show"));
expect(inputEl?.value).to.contain("02");

expect(menuEl?.classList.contains("show")).to.be.false;
Expand Down Expand Up @@ -813,7 +813,7 @@ describe("calendar year keyboard navigation", async () => {
};

it("this year will be focused and active", async () => {
const thisYear = new Date().getFullYear()
const thisYear = new Date().getFullYear();
const { header, headerBtn, calendar, years } = await starterKit([`29/01/${thisYear}`]);
const targetYear = years?.[0];
expect(calendar.shadowRoot?.activeElement === targetYear).to.be.true;
Expand All @@ -825,17 +825,20 @@ describe("calendar year keyboard navigation", async () => {
await calendar.updateComplete;
await header.updateComplete;

const prevTwelveYearsStart = thisYear - 12
const prevTwelveYearsEnd = thisYear - 1
const prevTwelveYearsStart = thisYear - 12;
const prevTwelveYearsEnd = thisYear - 1;
expect(headerBtn.innerText).to.equal(`${prevTwelveYearsStart} - ${prevTwelveYearsEnd}`);
expect(calendar.shadowRoot?.activeElement === years?.[9]).to.be.true;
expect(years?.[9].classList.contains("active")).to.be.false;
expect(years?.[9].textContent).to.include(`${thisYear - 3}`);
});
it("the years in range mode will be active", async () => {
const thisYear = new Date().getFullYear()
const tenYearsLater = thisYear + 10
const { header, headerBtn, calendar, years } = await starterKit([`29/03/${thisYear}`, `29/03/${tenYearsLater}`], "range");
const thisYear = new Date().getFullYear();
const tenYearsLater = thisYear + 10;
const { header, headerBtn, calendar, years } = await starterKit(
[`29/03/${thisYear}`, `29/03/${tenYearsLater}`],
"range"
);
const targetYear = years?.[0];
expect(calendar.shadowRoot?.activeElement === targetYear).to.be.true;
years?.forEach((y, i) => {
Expand All @@ -853,8 +856,8 @@ describe("calendar year keyboard navigation", async () => {

await calendar.updateComplete;
await header.updateComplete;
const nextTwelveYearsStart = thisYear + 12
const nextTwelveYearsEnd = nextTwelveYearsStart + 11
const nextTwelveYearsStart = thisYear + 12;
const nextTwelveYearsEnd = nextTwelveYearsStart + 11;
expect(headerBtn.innerText).to.equal(`${nextTwelveYearsStart} - ${nextTwelveYearsEnd}`);
years?.forEach(y => expect(y.classList.contains("active")).to.be.false);
});
Expand Down Expand Up @@ -1572,6 +1575,37 @@ describe("datepicker in form context", () => {
const formData = new FormData(el);
expect(formData.get("myDatepicker")).to.equal("23/03/2020");
});
it("For required, should be invalid when touched and blurred but empty", async () => {
const el = await fixture<HTMLFormElement>(
html`<form><sgds-datepicker name="myDatepicker" required></sgds-datepicker></form>`
);
const datepicker = el.querySelector("sgds-datepicker") as SgdsDatepicker;
const input = datepicker?.shadowRoot?.querySelector("sgds-datepicker-input")?.shadowRoot?.querySelector("input");
expect(datepicker.invalid).to.be.false;
input?.focus();
input?.blur();
await elementUpdated(datepicker);
expect(datepicker.invalid).to.be.true;
});
it("For required, should be invalid when input is emptied and blurred away", async () => {
const el = await fixture<HTMLFormElement>(
html`<form>
<sgds-datepicker name="myDatepicker" .initialValue=${["23/03/2020"]} required></sgds-datepicker>
</form>`
);
const datepicker = el.querySelector("sgds-datepicker") as SgdsDatepicker;
const input = datepicker?.shadowRoot?.querySelector("sgds-datepicker-input")?.shadowRoot?.querySelector("input");
expect(datepicker.invalid).to.be.false;

input?.focus();
for (let i = 0; i < 8; i++) {
await sendKeys({ press: "Backspace" });
}
await waitUntil(() => input?.value === "DD/MM/YYYY");
input?.blur();
await elementUpdated(datepicker);
expect(datepicker.invalid).to.be.true;
});
});

describe("datepicker a11y labels", () => {
Expand Down Expand Up @@ -1728,7 +1762,7 @@ describe("datepicker a11y labels", () => {
it("aria-selected=true on selected dates when view=years, mode=range", async () => {
// 28th March 2024
const mockDate = new Date(2024, 2, 28);
const mockSelectedDate = [new Date(2024-3, 2, 14), new Date(2024, 9, 27)];
const mockSelectedDate = [new Date(2024 - 3, 2, 14), new Date(2024, 9, 27)];
const el = await fixture<DatepickerCalendar>(
html`<sgds-datepicker-calendar
.displayDate=${mockDate}
Expand Down

0 comments on commit ab4e449

Please sign in to comment.