Skip to content

Commit

Permalink
feat(datepicker): improve calendar and datepicker components (#995)
Browse files Browse the repository at this point in the history
  • Loading branch information
dilandoogan authored Jan 29, 2025
1 parent a10f0bf commit 92536ea
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 270 deletions.
81 changes: 53 additions & 28 deletions src/components/calendar/bl-calendar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe("bl-calendar", () => {
let element: BlCalendar;
let consoleWarnSpy: sinon.SinonSpy;


beforeEach(async () => {
element = await fixture<BlCalendar>(html`
<bl-calendar locale="en"></bl-calendar>`);
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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]);

});

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -349,43 +352,43 @@ 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", () => {
const calendarDate = new Date(2023, 0, 10);
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", () => {
const calendarDate = new Date(2023, 0, 5);

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", () => {
Expand All @@ -402,30 +405,30 @@ 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)
];

element.setHoverClass();

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)
];

element.setHoverClass();
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;
Expand Down Expand Up @@ -569,12 +572,34 @@ 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");

element.setHoverClass();

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;
});
});
68 changes: 43 additions & 25 deletions src/components/calendar/bl-calendar.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -32,6 +33,8 @@ export default class BlCalendar extends DatepickerCalendarMixin {
_calendarYears: number[] = [];
@state()
_calendarDays: CalendarDay[] = [];
@state()
_dates: Date[] = [];
/**
* Fires when date selection changes
*/
Expand All @@ -56,7 +59,7 @@ export default class BlCalendar extends DatepickerCalendarMixin {
}

public handleClearSelectedDates = () => {
this._selectedDates = [];
this._dates = [];
this._onBlCalendarChange([]);
this.clearRangePickerStyles();
};
Expand Down Expand Up @@ -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() &&
Expand All @@ -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() &&
Expand All @@ -240,25 +243,25 @@ 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");
const rangeDays = [...this.createCalendarDays().values()]
.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++) {
Expand Down Expand Up @@ -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" : "";
Expand Down
Loading

0 comments on commit 92536ea

Please sign in to comment.