From 92536ea4ee687bc5102d69fbde6ea77032f7cff8 Mon Sep 17 00:00:00 2001 From: dilandoogan <147757889+dilandoogan@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:45:45 +0300 Subject: [PATCH] feat(datepicker): improve calendar and datepicker components (#995) --- src/components/calendar/bl-calendar.test.ts | 81 +++++++++++------ src/components/calendar/bl-calendar.ts | 68 +++++++++----- .../datepicker/bl-datepicker.test.ts | 91 +++---------------- src/components/datepicker/bl-datepicker.ts | 63 ++++++------- .../datepicker-calendar-mixin.test.ts | 70 +++----------- .../datepicker-calendar-mixin.ts | 61 +++---------- src/utilities/format-to-date-array.test.ts | 27 ++++++ src/utilities/format-to-date-array.ts | 11 +++ 8 files changed, 202 insertions(+), 270 deletions(-) create mode 100644 src/utilities/format-to-date-array.test.ts create mode 100644 src/utilities/format-to-date-array.ts diff --git a/src/components/calendar/bl-calendar.test.ts b/src/components/calendar/bl-calendar.test.ts index 21d2af71..e19f6767 100644 --- a/src/components/calendar/bl-calendar.test.ts +++ b/src/components/calendar/bl-calendar.test.ts @@ -8,6 +8,7 @@ describe("bl-calendar", () => { let element: BlCalendar; let consoleWarnSpy: sinon.SinonSpy; + beforeEach(async () => { element = await fixture(html` `); @@ -63,8 +64,8 @@ describe("bl-calendar", () => { const dayButton = element.shadowRoot?.querySelector(".day-wrapper bl-button") as BlButton; dayButton?.click(); - expect(element._selectedDates.length).to.equal(1); - expect(element.checkIfSelectedDate(element._selectedDates[0])).to.be.true; + expect(element._dates.length).to.equal(1); + expect(element.checkIfSelectedDate(element._dates[0])).to.be.true; }); it("should correctly handle multiple date selection", async () => { @@ -76,7 +77,7 @@ describe("bl-calendar", () => { dayButtons[0].click(); dayButtons[1].click(); - expect(element._selectedDates.length).to.equal(2); + expect(element._dates.length).to.equal(2); }); it("should fire bl-calendar-change event when dates are selected", async () => { @@ -97,7 +98,7 @@ describe("bl-calendar", () => { daysButtons[0].click(); expect(selectedDates.length).to.equal(1); - expect(selectedDates[0]).to.equal(singleTypeCalendar._selectedDates[0]); + expect(selectedDates[0]).to.equal(singleTypeCalendar._dates[0]); }); @@ -144,8 +145,8 @@ describe("bl-calendar", () => { element.handleRangeSelectCalendar(startDate); element.handleRangeSelectCalendar(endDate); - expect(element._selectedDates[0]).to.deep.equal(startDate); - expect(element._selectedDates[1]).to.deep.equal(endDate); + expect(element._dates[0]).to.deep.equal(startDate); + expect(element._dates[1]).to.deep.equal(endDate); }); it("should render month names in the correct locale", async () => { @@ -232,23 +233,25 @@ describe("bl-calendar", () => { }); it("should wrap value in an array if it is a single date", async () => { - const calendar = new BlCalendar(); + const date=new Date("2023-09-18"); - calendar.value = new Date("2023-09-18"); - calendar.type = CALENDAR_TYPES.SINGLE; + element.type = CALENDAR_TYPES.SINGLE; + element.value = date; + await element.updateComplete; - expect(calendar._selectedDates).to.deep.equal([new Date("2023-09-18")], {}); + expect(element._dates[0]).to.be.equal(date); }); it("should set startDate and endDate in selectedDays when type is range", async () => { const defaultDate1 = new Date(2023, 9, 18); const defaultDate2 = new Date(2023, 9, 19); - element.value = [defaultDate1, defaultDate2]; element.type = CALENDAR_TYPES.RANGE; + element.value = [defaultDate1, defaultDate2]; + await element.updateComplete; - expect(element._selectedDates[0]).to.be.equal(defaultDate1); - expect(element._selectedDates[1]).to.be.equal(defaultDate2); + expect(element._dates[0]).to.be.equal(defaultDate1); + expect(element._dates[1]).to.be.equal(defaultDate2); }); it("should navigate to the previous month in DAYS view", async () => { @@ -349,11 +352,11 @@ describe("bl-calendar", () => { const startDate = new Date(2023, 0, 5); const calendarDate = new Date(2023, 0, 1); - element._selectedDates[0] = startDate; + element._dates[0] = startDate; element.handleRangeSelectCalendar(calendarDate); - expect(element._selectedDates).to.deep.equal([calendarDate, startDate]); + expect(element._dates).to.deep.equal([calendarDate, startDate]); }); it("should reset to only startDate when both startDate and endDate are set", () => { @@ -361,22 +364,22 @@ describe("bl-calendar", () => { const startDate = new Date(2023, 0, 5); const endDate = new Date(2023, 0, 15); - element._selectedDates = [startDate, endDate]; + element._dates = [startDate, endDate]; element.handleRangeSelectCalendar(calendarDate); - expect(element._selectedDates).to.deep.equal([calendarDate]); + expect(element._dates).to.deep.equal([calendarDate]); }); - it("should remove the date if it already exists in _selectedDates", () => { + it("should remove the date if it already exists in _dates", () => { const calendarDate = new Date(2023, 0, 5); - element._selectedDates.push(calendarDate); + element._dates.push(calendarDate); element.handleMultipleSelectCalendar(calendarDate); - expect(element._selectedDates).to.not.include(calendarDate); - expect(element._selectedDates).to.have.lengthOf(0); + expect(element._dates).to.not.include(calendarDate); + expect(element._dates).to.have.lengthOf(0); }); it("should add the date if it does not exist in selectedDates", () => { @@ -384,8 +387,8 @@ describe("bl-calendar", () => { element.handleMultipleSelectCalendar(calendarDate); - expect(element._selectedDates).to.include(calendarDate); - expect(element._selectedDates).to.have.lengthOf(1); + expect(element._dates).to.include(calendarDate); + expect(element._dates).to.have.lengthOf(1); }); it("should call handleRangeSelectCalendar when type is RANGE", () => { @@ -402,7 +405,7 @@ describe("bl-calendar", () => { it("should add range-start-day class to the start date element", async () => { - element._selectedDates = [new Date(element.today.getFullYear(), element.today.getMonth(), 1), + element._dates = [new Date(element.today.getFullYear(), element.today.getMonth(), 1), new Date(element.today.getFullYear(), element.today.getMonth(), 5) ]; @@ -410,14 +413,14 @@ describe("bl-calendar", () => { await new Promise((resolve) => setTimeout(resolve, 200)); const startDateElement = element.shadowRoot?.getElementById( - `${element._selectedDates[0]?.getTime()}` + `${element._dates[0]?.getTime()}` )?.parentElement; expect(startDateElement?.classList.contains("range-start-day")).to.be.true; }); it("should add range-end-day class to the end date element", async () => { - element._selectedDates = [new Date(element.today.getFullYear(), element.today.getMonth(), 1), + element._dates = [new Date(element.today.getFullYear(), element.today.getMonth(), 1), new Date(element.today.getFullYear(), element.today.getMonth(), 5) ]; @@ -425,7 +428,7 @@ describe("bl-calendar", () => { await new Promise((resolve) => setTimeout(resolve, 200)); const endDateElement = element.shadowRoot?.getElementById( - `${element._selectedDates[1]?.getTime()}` + `${element._dates[1]?.getTime()}` )?.parentElement; expect(endDateElement?.classList.contains("range-end-day")).to.be.true; @@ -569,7 +572,7 @@ describe("bl-calendar", () => { it("should add classes when both startDate and endDate are defined", () => { - element._selectedDates = [new Date(2024, 0, 10), new Date(2024, 0, 15)]; + element._dates = [new Date(2024, 0, 10), new Date(2024, 0, 15)]; const setTimeoutSpy = sinon.spy(window, "setTimeout"); @@ -577,4 +580,26 @@ describe("bl-calendar", () => { expect(setTimeoutSpy).to.have.been.calledOnce; }); + it("should clear selected dates, dispatch event and clear range picker styles", async () => { + const clearRangePickerStylesSpy = sinon.spy(element, "clearRangePickerStyles"); + + element._dates = [new Date(2023, 0, 1), new Date(2023, 0, 5)]; + element.handleClearSelectedDates(); + + expect(element._dates).to.be.empty; + expect(clearRangePickerStylesSpy).to.have.been.calledOnce; + }); + + + it("should clear _dates and dispatch event with empty array when value is empty", async () => { + const changedProperties = new Map(); + + changedProperties.set("value", true); + element._value = []; + element.requestUpdate(); + await element.updateComplete; + element.updated(changedProperties); + + expect(element._dates).to.be.empty; + }); }); diff --git a/src/components/calendar/bl-calendar.ts b/src/components/calendar/bl-calendar.ts index 55b8ae3b..5999e2a6 100644 --- a/src/components/calendar/bl-calendar.ts +++ b/src/components/calendar/bl-calendar.ts @@ -1,8 +1,9 @@ -import { CSSResultGroup, html } from "lit"; +import { CSSResultGroup, html, PropertyValues } from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; import DatepickerCalendarMixin from "../../mixins/datepicker-calendar-mixin/datepicker-calendar-mixin"; import { event, EventDispatcher } from "../../utilities/event"; +import { formatToDateArray } from "../../utilities/format-to-date-array"; import "../button/bl-button"; import "../icon/bl-icon"; import { @@ -32,6 +33,8 @@ export default class BlCalendar extends DatepickerCalendarMixin { _calendarYears: number[] = []; @state() _calendarDays: CalendarDay[] = []; + @state() + _dates: Date[] = []; /** * Fires when date selection changes */ @@ -56,7 +59,7 @@ export default class BlCalendar extends DatepickerCalendarMixin { } public handleClearSelectedDates = () => { - this._selectedDates = []; + this._dates = []; this._onBlCalendarChange([]); this.clearRangePickerStyles(); }; @@ -161,46 +164,46 @@ export default class BlCalendar extends DatepickerCalendarMixin { break; } - this._onBlCalendarChange(this._selectedDates); + this._onBlCalendarChange(this._dates); this.requestUpdate(); } handleSingleSelectCalendar(calendarDate: Date) { - this._selectedDates = [calendarDate]; + this._dates = [calendarDate]; } handleMultipleSelectCalendar(calendarDate: Date) { - const dateExist = this._selectedDates?.some(d => d.getTime() === calendarDate.getTime()); + const dateExist = this._dates?.some(d => d.getTime() === calendarDate.getTime()); dateExist - ? this._selectedDates?.splice( - this._selectedDates?.findIndex(d => d.getTime() === calendarDate.getTime()), + ? this._dates?.splice( + this._dates?.findIndex(d => d.getTime() === calendarDate.getTime()), 1 ) - : this._selectedDates.push(calendarDate); + : this._dates.push(calendarDate); } handleRangeSelectCalendar(calendarDate: Date) { - if (!this._selectedDates[0]) { - this._selectedDates[0] = calendarDate; - } else if (!this._selectedDates[1]) { - if (calendarDate.getTime() > this._selectedDates[0].getTime()) { - this._selectedDates[1] = calendarDate; + if (!this._dates[0]) { + this._dates[0] = calendarDate; + } else if (!this._dates[1]) { + if (calendarDate.getTime() > this._dates[0].getTime()) { + this._dates[1] = calendarDate; } else { - const tempEndDate = this._selectedDates[0]; + const tempEndDate = this._dates[0]; - this._selectedDates[0] = calendarDate; - this._selectedDates[1] = tempEndDate; + this._dates[0] = calendarDate; + this._dates[1] = tempEndDate; } } else { - this._selectedDates = []; - this._selectedDates[0] = calendarDate; + this._dates = []; + this._dates[0] = calendarDate; } this.setHoverClass(); } checkIfSelectedDate(calendarDate: Date) { - return this._selectedDates?.some( + return this._dates?.some( date => date.getFullYear() === calendarDate.getFullYear() && date.getMonth() === calendarDate.getMonth() && @@ -226,7 +229,7 @@ export default class BlCalendar extends DatepickerCalendarMixin { return true; } if (this.disabledDates.length > 0) { - return this.disabledDates.some(disabledDate => { + return this.disabledDates?.some(disabledDate => { return ( calendarDate.getDate() === disabledDate.getDate() && calendarDate.getMonth() === disabledDate.getMonth() && @@ -240,16 +243,16 @@ export default class BlCalendar extends DatepickerCalendarMixin { setHoverClass() { this.clearRangePickerStyles(); - if (this._selectedDates[0] && this._selectedDates[1]) { + if (this._dates[0] && this._dates[1]) { setTimeout(() => { const startDateParentElement = this.shadowRoot?.getElementById( - `${this._selectedDates[0]?.getTime()}` + `${this._dates[0]?.getTime()}` )?.parentElement; startDateParentElement?.classList.add("range-start-day"); const endDateParentElement = this.shadowRoot?.getElementById( - `${this._selectedDates[1]?.getTime()}` + `${this._dates[1]?.getTime()}` )?.parentElement; endDateParentElement?.classList.add("range-end-day"); @@ -257,8 +260,8 @@ export default class BlCalendar extends DatepickerCalendarMixin { .flat() .filter( date => - date.getTime() > this._selectedDates[0]!.getTime() && - date.getTime() < this._selectedDates[1]!.getTime() + date.getTime() > this._dates[0]!.getTime() && + date.getTime() < this._dates[1]!.getTime() ); for (let i = 0; i < rangeDays.length; i++) { @@ -346,6 +349,21 @@ export default class BlCalendar extends DatepickerCalendarMixin { return calendar; } + updated(changedProperties: PropertyValues) { + if (changedProperties.has("value")) { + const dates = formatToDateArray(this._value); + + if (!dates.length) { + this._dates = []; + this._onBlCalendarChange([]); + } else { + dates?.forEach(date => { + this.handleDate(date); + }); + } + } + } + renderCalendarHeader() { const showMonthSelected = this._calendarView === CALENDAR_VIEWS.MONTHS ? "header-text-hover" : ""; diff --git a/src/components/datepicker/bl-datepicker.test.ts b/src/components/datepicker/bl-datepicker.test.ts index c194235c..aa6ceff0 100644 --- a/src/components/datepicker/bl-datepicker.test.ts +++ b/src/components/datepicker/bl-datepicker.test.ts @@ -1,4 +1,4 @@ -import { aTimeout, elementUpdated, expect, fixture, html } from "@open-wc/testing"; +import { aTimeout, expect, fixture, html } from "@open-wc/testing"; import { BlButton, BlDatePicker } from "../../baklava"; import { CALENDAR_TYPES } from "../calendar/bl-calendar.constant"; import sinon from "sinon"; @@ -9,6 +9,7 @@ describe("BlDatepicker", () => { let getElementByIdStub: sinon.SinonStub; let consoleWarnSpy: sinon.SinonSpy; + beforeEach(async () => { element = await fixture(html` `); @@ -24,6 +25,7 @@ describe("BlDatepicker", () => { } return null; }); + consoleWarnSpy = sinon.spy(console, "warn"); await element.updateComplete; @@ -63,38 +65,7 @@ describe("BlDatepicker", () => { expect(element._popoverEl?.visible).to.be.true; }); - it("should close the popover after selecting a date", async () => { - - element._inputEl?.click(); - await element.updateComplete; - - element._calendarEl?.dispatchEvent(new CustomEvent("bl-calendar-change", { detail: [new Date()] })); - await element.updateComplete; - await aTimeout(400); - expect(element._selectedDates.length).to.equal(1); - expect(element._popoverEl.visible).to.be.false; - }); - - it("should trigger datepicker change event on date selection", async () => { - const testDate = new Date(2023, 1, 1); - - element.addEventListener("bl-datepicker-change", (event) => { - const customEvent = event as CustomEvent; - - expect(customEvent).to.exist; - expect(customEvent.detail).to.deep.equal([testDate]); - - }); - - element._calendarEl.dispatchEvent(new CustomEvent("bl-calendar-change", { detail: [testDate] })); - - await element.updateComplete; - }); - it("should clear selected dates when clear button is clicked", async () => { - element._selectedDates = [new Date(2023, 1, 1)]; - await element.updateComplete; - element.addEventListener("bl-datepicker-change", (event) => { const customEvent = event as CustomEvent; @@ -109,7 +80,7 @@ describe("BlDatepicker", () => { await element.updateComplete; - expect(element._selectedDates).to.deep.equal([]); + expect(element._calendarEl?._dates).to.deep.equal([]); expect(element._inputValue).to.equal(""); }); @@ -122,51 +93,26 @@ describe("BlDatepicker", () => { expect(input?.hasAttribute("disabled")).to.be.true; }); - it("should use custom value formatter when provided", async () => { + it("should use custom value formatter when provided", async () => { const testDate = new Date(2023, 1, 1); element.valueFormatter = (dates: Date[]) => `Custom format: ${dates[0].toDateString()}`; - element.setDatePickerInput([testDate]); - await element.updateComplete; + element._calendarEl._dates=[testDate]; + element.setDatePickerInput(); + await element._calendarEl.updateComplete; + element._calendarEl.requestUpdate(); expect(element._inputValue).to.equal(`Custom format: ${testDate.toDateString()}`); }); - it("should handle multiple date selections", async () => { - const dates = [new Date(2023, 1, 1), new Date(2023, 1, 2)]; - - element.type = CALENDAR_TYPES.MULTIPLE; - await element.updateComplete; - - element._calendarEl?.dispatchEvent(new CustomEvent("bl-calendar-change", { detail: dates })); - await element.updateComplete; - - expect(element._selectedDates.length).to.equal(2); - expect(element._selectedDates).to.deep.equal(dates); - }); - it("should clear the datepicker even if no dates are selected", async () => { element.clearDatepicker(); await element.updateComplete; - expect(element._selectedDates).to.deep.equal([]); + expect(element._calendarEl?._dates).to.deep.equal([]); expect(element._inputValue).to.equal(""); }); - it("should handle selecting a range of dates", async () => { - const startDate = new Date(2023, 1, 1); - const endDate = new Date(2023, 1, 7); - - element.type = CALENDAR_TYPES.RANGE; - await element.updateComplete; - - element._calendarEl?.dispatchEvent(new CustomEvent("bl-calendar-change", { detail: [startDate, endDate] })); - await element.updateComplete; - - expect(element._selectedDates.length).to.equal(2); - expect(element._selectedDates).to.deep.equal([startDate, endDate]); - }); - it("should display help text when provided", async () => { element.helpText = "Please select a valid date."; await element.updateComplete; @@ -212,7 +158,7 @@ describe("BlDatepicker", () => { it("should include ',...' when floatingDateCount is greater than 0 for MULTIPLE type", () => { element.type = CALENDAR_TYPES.MULTIPLE; - element._selectedDates = [new Date("2024-01-01"), new Date("2024-01-02"), new Date("2024-01-03")]; + element._calendarEl._dates = [new Date("2024-01-01"), new Date("2024-01-02"), new Date("2024-01-03")]; element.setFloatingDates(); element.defaultInputValueFormatter(); expect(element._inputValue).to.include(" ,..."); @@ -221,7 +167,7 @@ describe("BlDatepicker", () => { it("should not include \" ,...\" when floatingDateCount is 0 for MULTIPLE type", () => { element.type = CALENDAR_TYPES.MULTIPLE; - element._selectedDates = [new Date("2024-01-01")]; + element._calendarEl._dates = [new Date("2024-01-01")]; element.setFloatingDates(); @@ -280,10 +226,6 @@ describe("BlDatepicker", () => { expect(element.value).to.equal(dates); }); - it("should return undefined if value is not set", () => { - expect(element.value).to.be.undefined; - }); - it("should not warn when value is an array for multiple/range selection", () => { element.type = CALENDAR_TYPES.MULTIPLE; element.value = [new Date(), new Date()]; @@ -375,13 +317,4 @@ describe("BlDatepicker", () => { expect(focusSpy.called).to.be.true; }); - it("should call setDatePickerInput when _selectedDates changes", async () => { - const setDatePickerInputSpy = sinon.spy(element, "setDatePickerInput"); - - element.value = [new Date(2025,0,10)]; - await elementUpdated(element); - - expect(setDatePickerInputSpy).to.have.been.calledOnceWith(element._selectedDates); - }); - }); diff --git a/src/components/datepicker/bl-datepicker.ts b/src/components/datepicker/bl-datepicker.ts index f68c62c2..23fd7582 100644 --- a/src/components/datepicker/bl-datepicker.ts +++ b/src/components/datepicker/bl-datepicker.ts @@ -1,4 +1,4 @@ -import { CSSResultGroup, html, PropertyValues, TemplateResult } from "lit"; +import { CSSResultGroup, html, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { BlCalendar, BlPopover } from "../../baklava"; import DatepickerCalendarMixin from "../../mixins/datepicker-calendar-mixin/datepicker-calendar-mixin"; @@ -58,9 +58,6 @@ export default class BlDatepicker extends DatepickerCalendarMixin { @state() _inputValue = ""; - @state() - _selectedDates: Date[] = []; - @state() _floatingDateCount: number = 0; @@ -90,20 +87,22 @@ export default class BlDatepicker extends DatepickerCalendarMixin { defaultInputValueFormatter() { if (this.type === CALENDAR_TYPES.SINGLE) { - this._inputValue = this.formatDate(this._selectedDates[0]); + this._inputValue = this.formatDate(this._calendarEl?._dates[0]); this.closePopoverWithTimeout(); } else if (this.type === CALENDAR_TYPES.MULTIPLE) { this.setFloatingDates(); - const values = this._selectedDates + const values = this._calendarEl?._dates .slice(0, this._fittingDateCount) .map(date => this.formatDate(date)); this._inputValue = values.join(",") + (this._floatingDateCount > 0 ? " ,..." : ""); } else if (this.type === CALENDAR_TYPES.RANGE) { - if (this._selectedDates[0]) this._inputValue = this.formatDate(this._selectedDates[0]); - if (this._selectedDates[1]) - this._inputValue = `${this._inputValue}-${this.formatDate(this._selectedDates[1])}`; - if (this._selectedDates[0] && this._selectedDates[1]) this.closePopoverWithTimeout(); + if (this._calendarEl?._dates[0]) + this._inputValue = this.formatDate(this._calendarEl?._dates[0]); + if (this._calendarEl?._dates[1]) + this._inputValue = `${this._inputValue}-${this.formatDate(this._calendarEl?._dates[1])}`; + if (this._calendarEl?._dates[0] && this._calendarEl?._dates[1]) + this.closePopoverWithTimeout(); } } @@ -121,22 +120,19 @@ export default class BlDatepicker extends DatepickerCalendarMixin { this._fittingDateCount = Math.floor(datesTextTotalWidth / 90); - this._floatingDateCount = this._selectedDates.length - this._fittingDateCount; + this._floatingDateCount = this._calendarEl?._dates.length - this._fittingDateCount; } - setDatePickerInput(dates: Date[] | []) { - if (!dates.length) { + setDatePickerInput() { + if (!this._calendarEl?._dates.length) { this._inputValue = ""; } else { - this._selectedDates = dates; if (this.valueFormatter) { - this._inputValue = this.valueFormatter(this._selectedDates); + this._inputValue = this.valueFormatter(this._calendarEl?._dates); } else { this.defaultInputValueFormatter(); } } - - this._onBlDatepickerChange(this._selectedDates); } formatDate(date: Date): string { @@ -147,10 +143,9 @@ export default class BlDatepicker extends DatepickerCalendarMixin { } clearDatepicker() { - this._selectedDates = []; + this._calendarEl.handleClearSelectedDates(); this._inputValue = ""; this._floatingDateCount = 0; - this._calendarEl.handleClearSelectedDates(); } openPopover() { @@ -169,7 +164,7 @@ export default class BlDatepicker extends DatepickerCalendarMixin { formatAdditionalDates(str: string): TemplateResult[] { const parts = str.split(","); - return parts.reduce((acc, part, index) => { + return parts?.reduce((acc, part, index) => { if (index > 0 && index % 3 === 0) { acc.push(html`
`); } @@ -192,15 +187,14 @@ export default class BlDatepicker extends DatepickerCalendarMixin { this._calendarEl?.addEventListener("mousedown", this._onCalendarMouseDown); this._inputEl?.addEventListener("mousedown", this._onInputMouseDown); - if (this._selectedDates) { - this.setDatePickerInput(this._selectedDates); + if (this._calendarEl?._dates) { + this.setDatePickerInput(); } } - updated(changedProperties: PropertyValues) { - if (changedProperties.has("_selectedDates")) { - this.setDatePickerInput(this._selectedDates); - } + onCalendarChange() { + this.setDatePickerInput(); + this._onBlDatepickerChange(this._calendarEl?._dates); } disconnectedCallback() { @@ -220,16 +214,17 @@ export default class BlDatepicker extends DatepickerCalendarMixin { .disabledDates=${this.disabledDates} .value=${this.value} .locale=${this.locale} - @bl-calendar-change="${(event: CustomEvent) => this.setDatePickerInput(event.detail)}" + @bl-calendar-change="${this.onCalendarChange}" > `; - const additionalDates = this._selectedDates - ?.slice(this._fittingDateCount) - .map(date => { - return this.formatDate(date); - }) - .join(","); + const additionalDates = + this._calendarEl?._dates + ?.slice(this._fittingDateCount) + .map(date => { + return this.formatDate(date); + }) + .join(",") ?? ""; const formattedAdditionalDates = this.formatAdditionalDates(additionalDates); @@ -242,7 +237,7 @@ export default class BlDatepicker extends DatepickerCalendarMixin { : ""; const clearDatepickerButton = - this._selectedDates.length > 0 + this._calendarEl?._dates.length > 0 ? html` { let element: TestDatepickerCalendar; + let consoleSpy: sinon.SinonSpy; beforeEach(async () => { element = await fixture( html` ` ); + consoleSpy = sinon.spy(console, "warn"); + }); + + afterEach(() => { + consoleSpy.restore(); }); it("should correctly set and get disabledDates from a string", () => { @@ -40,46 +45,29 @@ describe("DatepickerCalendarMixin", () => { }); it("should set and get minDate correctly", () => { - const minDate = new Date(2024,1,1); + const minDate = new Date(2024, 1, 1); element.minDate = minDate; expect(element.minDate.getTime()).to.equal(minDate.getTime()); }); it("should log a warning if minDate is greater than maxDate", () => { - const consoleSpy = sinon.spy(console, "warn"); - element.maxDate = new Date("2024-01-01"); element.minDate = new Date("2024-02-01"); expect(consoleSpy.calledWith("minDate cannot be greater than maxDate.")).to.be.true; - consoleSpy.restore(); }); it("should set and get maxDate correctly", () => { - const maxDate = new Date(2024,12,31); + const maxDate = new Date(2024, 12, 31); element.maxDate = maxDate; expect(element.maxDate.getTime()).to.equal(maxDate.getTime()); }); it("should log a warning if maxDate is smaller than minDate", () => { - const consoleSpy = sinon.spy(console, "warn"); - element.minDate = new Date("2024-12-31"); element.maxDate = new Date("2024-01-01"); expect(consoleSpy.calledWith("maxDate cannot be smaller than minDate.")).to.be.true; - consoleSpy.restore(); - }); - - it("should correctly parse value from a string", () => { - const valueString = "2024-01-01,2024-01-15"; - - element.type = CALENDAR_TYPES.MULTIPLE; - element.value = valueString; - expect(element._selectedDates).to.be.an("array"); - expect(element._selectedDates).to.have.length(2); - expect((element._selectedDates)[0].getTime()).to.equal(new Date("2024-01-01").getTime()); - expect((element._selectedDates)[1].getTime()).to.equal(new Date("2024-01-15").getTime()); }); it("should correctly parse value from a Date object", () => { @@ -90,41 +78,13 @@ describe("DatepickerCalendarMixin", () => { expect(element.value).to.equal(dateValue); }); - it("should log a warning if value type is invalid for CALENDAR_TYPES.SINGLE", () => { - const consoleSpy = sinon.spy(console, "warn"); - - element.type = CALENDAR_TYPES.SINGLE; - element.value = [new Date("2024-01-01"), new Date("2024-01-15")]; - expect(consoleSpy.calledWith("'value' must be a single Date for single type selection.")).to.be - .true; - consoleSpy.restore(); - }); - - it("should log a warning if value type is invalid for CALENDAR_TYPES.RANGE", () => { - const consoleSpy = sinon.spy(console, "warn"); - - element.type = CALENDAR_TYPES.RANGE; - element.value = [new Date("2024-01-01")]; - expect( - consoleSpy.calledWith( - "'value' must be an array of two Date objects when the type selection mode is set to range." - ) - ).to.be.true; - consoleSpy.restore(); - }); - - it("should update selectedDates when value changes", () => { - const dateValue = new Date("2024-01-01"); - - element.type = CALENDAR_TYPES.SINGLE; - element.value = dateValue; - expect(element._selectedDates).to.deep.equal([dateValue]); + it("should warn for invalid minDate value", () => { + element.minDate = new Date("invalid date"); + expect(consoleSpy.calledWith("Invalid minDate value.")).to.be.true; }); - it("should not update selectedDates if value is invalid", () => { - const originalSelectedDates = [...element._selectedDates]; - - element.value = "invalid-date"; - expect(element._selectedDates).to.deep.equal(originalSelectedDates); + it("should warn for invalid maxDate value", () => { + element.maxDate = new Date("invalid date"); + expect(consoleSpy.calledWith("Invalid maxDate value.")).to.be.true; }); }); diff --git a/src/mixins/datepicker-calendar-mixin/datepicker-calendar-mixin.ts b/src/mixins/datepicker-calendar-mixin/datepicker-calendar-mixin.ts index 6defd4ac..0f7ad1e8 100644 --- a/src/mixins/datepicker-calendar-mixin/datepicker-calendar-mixin.ts +++ b/src/mixins/datepicker-calendar-mixin/datepicker-calendar-mixin.ts @@ -1,6 +1,5 @@ import { LitElement } from "lit"; -import { property, state } from "lit/decorators.js"; -import { CALENDAR_TYPES } from "../../components/calendar/bl-calendar.constant"; +import { property } from "lit/decorators.js"; import { CalendarType, DayValues } from "../../components/calendar/bl-calendar.types"; import { stringToDateArray } from "../../utilities/string-to-date-converter"; @@ -20,8 +19,6 @@ export default class DatepickerCalendarMixin extends LitElement { */ @property() locale: string = document.documentElement.lang || "en-EN"; - @state() - _selectedDates: Date[] = []; /** * Defines the unselectable dates for calendar @@ -64,7 +61,7 @@ export default class DatepickerCalendarMixin extends LitElement { @property({ type: Date, attribute: "max-date", reflect: true }) set maxDate(maxDate: Date) { - if (isNaN(new Date(maxDate).getTime())) { + if (maxDate && isNaN(new Date(maxDate).getTime())) { console.warn("Invalid maxDate value."); return; } @@ -87,7 +84,7 @@ export default class DatepickerCalendarMixin extends LitElement { @property({ type: Date, attribute: "min-date", reflect: true }) set minDate(minDate: Date) { - if (isNaN(new Date(minDate).getTime())) { + if (minDate && isNaN(new Date(minDate).getTime())) { console.warn("Invalid minDate value."); return; } @@ -99,51 +96,17 @@ export default class DatepickerCalendarMixin extends LitElement { } } - /** - * Target elements state - */ - protected _value: Date | Date[] | string; + @property({ attribute: "value", reflect: true }) + set value(value: string | Date | Date[]) { + const oldValue = this._value; - /** - * Sets the target element of the popover to align and trigger. - * It can be a string id of the target element or can be a direct Element reference of it. - */ - get value() { - return this._value; + this._value = value; + this.requestUpdate("value", oldValue); } - @property({ attribute: "value", reflect: true }) - set value(value: string | Date | Date[]) { - if (value) { - const oldValue = this._value; - let tempVal: Date[] = []; - - if (typeof value === "string") { - tempVal = stringToDateArray(value); - } else if (value instanceof Date) { - tempVal.push(value); - } else if (Array.isArray(value)) { - tempVal = value; - } - - if (tempVal.length > 0) { - if (this.type === CALENDAR_TYPES.SINGLE && tempVal.length > 1) { - console.warn("'value' must be a single Date for single type selection."); - } else if ( - this.type === CALENDAR_TYPES.RANGE && - Array.isArray(tempVal) && - tempVal.length != 2 - ) { - console.warn( - "'value' must be an array of two Date objects when the type selection mode is set to range." - ); - } else { - this._value = value; - this._selectedDates = tempVal; - } - } - - this.requestUpdate("value", oldValue); - } + get value(): string | Date | Date[] { + return this._value; } + + _value: string | Date | Date[] = []; } diff --git a/src/utilities/format-to-date-array.test.ts b/src/utilities/format-to-date-array.test.ts new file mode 100644 index 00000000..69ca5f9f --- /dev/null +++ b/src/utilities/format-to-date-array.test.ts @@ -0,0 +1,27 @@ +import { expect } from "@open-wc/testing"; +import { formatToDateArray } from "./format-to-date-array"; + +describe("normalizeValue", () => { + it("should return an array of dates from a string", () => { + const input = "2023-10-10,2023-10-11"; + const expected = [new Date("2023-10-10"), new Date("2023-10-11")]; + const result = formatToDateArray(input); + + expect(result).to.deep.equal(expected); + }); + + it("should return an array with a single date from a Date object", () => { + const input = new Date("2023-10-12"); + const expected = [input]; + const result = formatToDateArray(input); + + expect(result).to.deep.equal(expected); + }); + + it("should return the input array if it contains only Date objects", () => { + const input = [new Date("2023-10-13"), new Date("2023-10-14")]; + const result = formatToDateArray(input); + + expect(result).to.equal(input); + }); +}); diff --git a/src/utilities/format-to-date-array.ts b/src/utilities/format-to-date-array.ts new file mode 100644 index 00000000..993548a3 --- /dev/null +++ b/src/utilities/format-to-date-array.ts @@ -0,0 +1,11 @@ +import { stringToDateArray } from "./string-to-date-converter"; + +export function formatToDateArray(value: string | Date | Date[]): Date[] { + if (typeof value === "string") { + return stringToDateArray(value); + } else if (value instanceof Date) { + return [value]; + } else { + return value; + } +}