diff --git a/.changeset/light-zoos-invite.md b/.changeset/light-zoos-invite.md new file mode 100644 index 00000000..85d7295c --- /dev/null +++ b/.changeset/light-zoos-invite.md @@ -0,0 +1,5 @@ +--- +"@shopware-ag/meteor-component-library": major +--- + +Replaced flatpickr with vue3datepicker diff --git a/packages/component-library/package.json b/packages/component-library/package.json index eca945fa..95f22d51 100644 --- a/packages/component-library/package.json +++ b/packages/component-library/package.json @@ -35,6 +35,7 @@ "@storybook/addon-a11y": "^8.1.1", "@testing-library/jest-dom": "^6.4.6", "@testing-library/vue": "^8.1.0", + "@vuepic/vue-datepicker": "^9.0.3", "@vueuse/components": "^10.7.2", "@vueuse/core": "^10.7.2", "date-fns": "^2.30.0", diff --git a/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.interactive.stories.ts b/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.interactive.stories.ts index 63a6f3a0..b96ddb11 100644 --- a/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.interactive.stories.ts +++ b/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.interactive.stories.ts @@ -17,13 +17,14 @@ export const TestDatepickerShouldOpen: MtDatepickerStory = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - // Open datepicker + // Open datepicker by clicking the input wrapper await userEvent.click(canvas.getByRole("textbox")); - await waitUntil(() => document.getElementsByClassName("flatpickr-calendar").length > 0); - const calendar = within( - document.getElementsByClassName("flatpickr-calendar")[0] as HTMLElement, - ); + // Wait until the datepicker's dropdown menu appears in the DOM + await waitUntil(() => document.getElementsByClassName("dp__menu").length > 0); + + // Once the datepicker menu is detected, look for it within the document + const calendar = within(document.getElementsByClassName("dp__menu")[0] as HTMLElement); // Expect input event is triggered expect(calendar).toBeDefined(); @@ -35,127 +36,105 @@ export const VisualTestDateInputValue: MtDatepickerStory = { args: { label: "Date value", dateType: "date", - modelValue: new Date(Date.UTC(2024, 4, 22, 22, 22)).toISOString(), }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const calendar = within( - document.getElementsByClassName("flatpickr-calendar")[0] as HTMLElement, - ); - - // Open datepicker + // Open datepicker by clicking the input wrapper await userEvent.click(canvas.getByRole("textbox")); - await waitUntil(() => document.getElementsByClassName("flatpickr-calendar").length > 0); - - // Click the 24th of month XYZ - await userEvent.click(calendar.getByText("24")); + await waitUntil(() => document.getElementsByClassName("dp__menu").length > 0); - // Click label to close datepicker - await userEvent.click(canvas.getByText(args.label!)); + // Access the calendar and click the date + const firstDate = document.getElementById("2024-10-12") as HTMLInputElement; + await userEvent.click(firstDate); - // Expect input value to be the 24th of month XYZ - expect((canvas.getByRole("textbox") as HTMLInputElement).value).toMatch(/\d{4}-\d{2}-24/); + // Check that the input value matches the date chosen + const input = document.querySelector('[data-test="dp-input"]') as HTMLInputElement; + expect(input.value).toContain("2024/10/12"); - // Expect input event is triggered - expect(args.updateModelValue).toHaveBeenCalled(); + // Expect updatemodelvalue to have been called with date + expect(args.updateModelValue).toHaveBeenCalledWith(expect.stringContaining("24-10-12")); }, }; export const VisualTestDateTimeInputValue: MtDatepickerStory = { name: "Should input datetime value", args: { - modelValue: new Date(Date.UTC(2024, 4, 22, 22, 22)).toISOString(), label: "Date value", dateType: "datetime", + timeZone: "Europe/Berlin" }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - const calendar = within( - document.getElementsByClassName("flatpickr-calendar")[0] as HTMLElement, - ); - // Open datepicker + // Open datepicker by clicking the input wrapper await userEvent.click(canvas.getByRole("textbox")); - await waitUntil(() => document.getElementsByClassName("flatpickr-calendar").length > 0); - - // Enter 22 as hour - const hourInput = calendar.getByLabelText("Hour"); - await userEvent.clear(hourInput); - await userEvent.type(hourInput, "22"); - - // Enter 22 as minute - const minuteInput = calendar.getByLabelText("Minute"); - await userEvent.clear(minuteInput); - await userEvent.type(minuteInput, "22"); - - // Click label to close datepicker - await userEvent.click(canvas.getByText(args.label!)); - - // Expect input value to be the 24th of month XYZ - expect((canvas.getByRole("textbox") as HTMLInputElement).value).toContain("22:22"); + await waitUntil(() => document.getElementsByClassName("dp__menu").length > 0); + + // Open the hours panel + const hourButton = document.querySelector( + '[data-test="hours-toggle-overlay-btn-0"]', + ) as HTMLInputElement; + await userEvent.click(hourButton); + + // Select an hour + const selectedHour = document.querySelector('[data-test="12"]') as HTMLInputElement; + await userEvent.click(selectedHour); + + // Open the minutes panel + const hourMin = document.querySelector( + '[data-test="minutes-toggle-overlay-btn-0"]', + ) as HTMLInputElement; + await userEvent.click(hourMin); + + // Select minute + const selectedMin = document.querySelector('[data-test="40"]') as HTMLInputElement; + await userEvent.click(selectedMin); + + // Click date within calendar + const firstDate = document.getElementById("2024-10-12") as HTMLInputElement; + await userEvent.click(firstDate); + + // Check that the input value matches the date chosen + const input = document.querySelector('[data-test="dp-input"]') as HTMLInputElement; + expect(input.value).toContain("2024/10/12, 12:40"); + + // Expect updatemodelvalue to have been called with date + expect(args.updateModelValue).toHaveBeenCalledWith(expect.stringContaining("24-10-12")); }, }; -export const VisualTestTimeInputValue: MtDatepickerStory = { - name: "Should input time value", +export const VisualRangeInputValue: MtDatepickerStory = { + name: "Should input range", args: { - label: "Time value", - dateType: "time", - config: { - time_24hr: true, - }, + label: "Date value", + dateType: "datetime", + range:true }, - play: async ({ canvasElement, args }) => { + play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const calendar = within( - document.getElementsByClassName("flatpickr-calendar")[0] as HTMLElement, - ); - // Open datepicker + // Open datepicker by clicking the input wrapper await userEvent.click(canvas.getByRole("textbox")); - await waitUntil(() => document.getElementsByClassName("flatpickr-calendar").length > 0); - - // Enter 22 as hour - await userEvent.clear(calendar.getByLabelText("Hour")); - await userEvent.type(calendar.getByLabelText("Hour"), "22"); - - // Enter 22 as minute - await userEvent.clear(calendar.getByLabelText("Minute")); - await userEvent.type(calendar.getByLabelText("Minute"), "22"); - await userEvent.type(canvas.getByRole("textbox"), "{enter}"); - - // Click label to close datepicker - await userEvent.click(canvas.getByText(args.label!)); - - // Expect input value to be the 24th of month XYZ - expect((canvas.getByRole("textbox") as HTMLInputElement).value).toBe("22:22"); - }, -}; - -export const TestClearInputValue: MtDatepickerStory = { - name: "Should clear input value", - args: { - label: "Datepicker", - modelValue: new Date(Date.UTC(2012, 1, 21)).toISOString(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); + await waitUntil(() => document.getElementsByClassName("dp__menu").length > 0); - // The 21st of February 2012 is correct because the Date constructor takes the month as 0-based - expect((canvas.getByRole("textbox") as HTMLInputElement).value).toBe("2012-02-21"); + // Click first date on calendar + const firstDate = document.getElementById("2024-10-12") as HTMLInputElement; + await userEvent.click(firstDate); - await userEvent.click(canvas.getByTestId("mt-datepicker-clear-button")); + // Click second date on calendar + const secondDate = document.getElementById("2024-10-14") as HTMLInputElement; + await userEvent.click(secondDate); - // We need to loose focus - await userEvent.click(canvas.getByText(args.label!)); - - expect((canvas.getByRole("textbox") as HTMLInputElement).value).toBe(""); + // Check that the input value matches the dates chosen + const input = document.querySelector('[data-test="dp-input"]') as HTMLInputElement; + const dateRange = input.value.split(" - ").map((date) => date.split(",")[0].trim()); + expect(dateRange).toEqual(["2024/10/12", "2024/10/14"]); }, }; -export const TestDisabledDoesNotOpenFlatpickr: MtDatepickerStory = { - name: "Should not open flatpickr when disabled", +export const TestDisabledDoesNotOpenDatepicker: MtDatepickerStory = { + name: "Should not open datepicker when disabled", args: { label: "Disabled", disabled: true, @@ -166,37 +145,7 @@ export const TestDisabledDoesNotOpenFlatpickr: MtDatepickerStory = { // Try to open datepicker await userEvent.click(canvas.getByRole("textbox")); + // Expect the datepciker input to be disabled expect((canvas.getByRole("textbox") as HTMLInputElement).disabled).toBe(true); }, -}; - -export const TestManualInput: MtDatepickerStory = { - name: "Should emit date value when manually typed", - args: { - label: "Date value", - dateType: "date", - modelValue: null, - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - const input = canvas.getByRole("textbox"); - - // Focus input - await userEvent.click(input); - - // Clear input - await userEvent.clear(input); - - // Enter date manually - await userEvent.type(input, "2024-12-24"); - - // Click label to close datepicker - await userEvent.click(canvas.getByText(args.label!)); - - // Expect input value - expect((input as HTMLInputElement).value).toBe("2024-12-24"); - - // Expect input event is triggered - expect(args.updateModelValue).toHaveBeenCalledWith("2024-12-24T00:00:00"); - }, -}; +}; \ No newline at end of file diff --git a/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.spec.ts b/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.spec.ts index cc732c13..53ed33df 100644 --- a/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.spec.ts +++ b/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.spec.ts @@ -1,5 +1,4 @@ import { mount } from "@vue/test-utils"; -import flushPromises from "flush-promises"; import MtDatepicker from "./mt-datepicker.vue"; async function createWrapper(customOptions = {}) { @@ -17,224 +16,67 @@ describe("src/app/component/form/mt-datepicker", () => { } }); - it("should be a Vue.JS component", async () => { + it("is enabled by default", async () => { wrapper = await createWrapper(); - expect(wrapper.vm).toBeTruthy(); - }); - - it("should have enabled links", async () => { - wrapper = await createWrapper(); - - const field = wrapper.find(".mt-field"); - const flatpickrInput = wrapper.find(".flatpickr-input"); - - expect(field.attributes().disabled).toBeUndefined(); - expect(flatpickrInput.attributes().disabled).toBeUndefined(); - }); - - it("should show the dateformat, when no placeholderText is provided", async () => { - wrapper = await createWrapper(); - const flatpickrInput = wrapper.find(".flatpickr-input"); - - expect(flatpickrInput.attributes().placeholder).toBe("Y-m-d"); - }); - - it("should show the placeholderText, when provided", async () => { - const placeholderText = "Stop! Hammertime!"; - wrapper = await createWrapper({ - props: { - placeholderText, - }, - }); - const flatpickrInput = wrapper.find(".flatpickr-input"); - - expect(flatpickrInput.attributes().placeholder).toBe(placeholderText); - }); - - it("should use the admin locale", async () => { - wrapper = await createWrapper({ - props: { - locale: "de", - }, - }); - - // @ts-expect-error - expect(wrapper.vm.$data.flatpickrInstance.config.locale).toBe("de"); - - await wrapper.setProps({ - locale: "en", - }); - await flushPromises(); + const datepickerInput = wrapper.find(".dp__input"); - // @ts-expect-error - expect(wrapper.vm.$data.flatpickrInstance.config.locale).toBe("en"); + expect(datepickerInput.attributes().disabled).toBeUndefined(); }); - it("should show the label from the property", async () => { + it("is disabled", async () => { wrapper = await createWrapper({ props: { - label: "Label from prop", + disabled: true, }, }); + const datepickerInput = wrapper.find(".dp__input"); - expect(wrapper.find("label").text()).toBe("Label from prop"); + expect(datepickerInput.attributes()).toHaveProperty("disabled"); }); - it("should not show the actual user timezone as a hint when it is not a datetime", async () => { - wrapper = await createWrapper({ - props: { - dateType: "date", - timeZone: "Europe/Berlin", - }, - }); - - const hint = wrapper.find(".mt-field__hint .mt-icon"); - - expect(hint.exists()).toBeFalsy(); - }); - - it("should show the UTC timezone as a hint when no timezone was selected and when datetime is datetime", async () => { - wrapper = await createWrapper({ - props: { - dateType: "datetime", - }, - }); - - const hint = wrapper.find(".mt-field__hint"); - const clockIcon = hint.find('[data-testid="mt-icon__solid-clock"]'); - - expect(hint.text()).toContain("UTC"); - expect(clockIcon.isVisible()).toBeTruthy(); - }); - - it("should show the actual user timezone as a hint when datetime is datetime", async () => { - wrapper = await createWrapper({ - props: { - timeZone: "Europe/Berlin", - dateType: "datetime", - }, - }); - - const hint = wrapper.find(".mt-field__hint"); - const clockIcon = hint.find('[data-testid="mt-icon__solid-clock"]'); + it("shows the date format as a placeholder when no placeholder is explictly defined", async () => { + wrapper = await createWrapper(); + const datepickerInput = wrapper.find(".dp__input"); - expect(hint.text()).toContain("Europe/Berlin"); - expect(clockIcon.isVisible()).toBeTruthy(); + expect(datepickerInput.attributes().placeholder).toBe("Y-m-d ..."); }); - it("should not show the actual user timezone as a hint when the hideHint property is set to true", async () => { + it("shows the placeholder when provided", async () => { + const placeholderText = "Stop! Hammertime!"; wrapper = await createWrapper({ props: { - timeZone: "Europe/Berlin", - dateType: "datetime", - hideHint: true, + placeholder: placeholderText, }, }); + const datepickerInput = wrapper.find(".dp__input"); - const hint = wrapper.find(".mt-field__hint .mt-icon"); - - expect(hint.exists()).toBeFalsy(); + expect(datepickerInput.attributes().placeholder).toBe(placeholderText); }); - it("should not show the actual user timezone as a hint when hideHint is false and dateType is not dateTime", async () => { - wrapper = await createWrapper({ - props: { - timeZone: "Europe/Berlin", - }, - }); - - const hint = wrapper.find(".mt-field__hint .mt-icon"); - expect(hint.exists()).toBeFalsy(); - }); - - it("should not convert the date when a timezone is set (type=date)", async () => { - wrapper = await createWrapper({ - props: { - modelValue: "2023-03-27T00:00:00.000+00:00", - dateType: "date", - timeZone: "Europe/Berlin", - }, - }); - - // Can't test with DOM because of the flatpickr dependency - expect(wrapper.vm.timezoneFormattedValue).toBe("2023-03-27T00:00:00.000+00:00"); - }); - - it("should not emit a converted date when a timezone is set (type=date)", async () => { + it("should not show the timezone if datepicker is configured for date only", async () => { wrapper = await createWrapper({ props: { - modelValue: "2023-03-27T00:00:00.000+00:00", dateType: "date", timeZone: "Europe/Berlin", }, }); - // can't test with DOM because of the flatpickr dependency - wrapper.vm.timezoneFormattedValue = "2023-03-22T00:00:00.000+00:00"; - - expect(wrapper.emitted("update:modelValue")?.[0]).toStrictEqual([ - "2023-03-22T00:00:00.000+00:00", - ]); - }); - - it("should not convert the date when a timezone is set (type=time)", async () => { - wrapper = await createWrapper({ - props: { - modelValue: "2023-03-27T00:00:00.000+00:00", - dateType: "time", - timeZone: "Europe/Berlin", - }, - }); - - // Can't test with DOM because of the flatpickr dependency - expect(wrapper.vm.timezoneFormattedValue).toBe("2023-03-27T00:00:00.000+00:00"); + const timeZoneHint = wrapper.find('[data-test="time-zone-hint"]'); + expect(timeZoneHint.exists()).toBe(false); }); - it("should not emit a converted date when a timezone is set (type=time)", async () => { + it("should show the timezone if datepicker is configured to datetime", async () => { wrapper = await createWrapper({ props: { - modelValue: "2023-03-27T00:00:00.000+00:00", - dateType: "time", timeZone: "Europe/Berlin", - }, - }); - - // can't test with DOM because of the flatpickr dependency - wrapper.vm.timezoneFormattedValue = "2023-03-22T00:00:00.000+00:00"; - - expect(wrapper.emitted("update:modelValue")?.[0]).toStrictEqual([ - "2023-03-22T00:00:00.000+00:00", - ]); - }); - - it("should convert the date when a timezone is set (type=datetime)", async () => { - wrapper = await createWrapper({ - props: { - modelValue: "2023-03-27T00:00:00.000+00:00", dateType: "datetime", - timeZone: "Europe/Berlin", }, }); - // Skip this test because data-fns-tz is not working correctly in the test environment - // Can't test with DOM because of the flatpickr dependency - // expect(wrapper.vm.timezoneFormattedValue).toStrictEqual('2023-03-27T02:00:00.000Z'); + const timeZoneHint = wrapper.find('[data-test="time-zone-hint"]'); + expect(timeZoneHint.exists()).toBe(true); + expect(timeZoneHint.text()).toBe("Europe/Berlin"); }); - it("should emit a converted date when a timezone is set (type=datetime)", async () => { - wrapper = await createWrapper({ - props: { - modelValue: "2023-03-27T00:00:00.000+00:00", - dateType: "datetime", - timeZone: "Europe/Berlin", - }, - }); - - // can't test with DOM because of the flatpickr dependency - wrapper.vm.timezoneFormattedValue = "2023-03-22T00:00:00.000+00:00"; - - // Skip this test because data-fns-tz is not working correctly in the test environment - // expect(wrapper.emitted('update:modelValue')[0]).toStrictEqual(['2023-03-21T23:00:00.000Z']); - }); }); diff --git a/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.vue b/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.vue index 3fd6ec82..5f986c24 100644 --- a/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.vue +++ b/packages/component-library/src/components/form/mt-datepicker/mt-datepicker.vue @@ -1,744 +1,517 @@