From 65586a00ccd41115be326278aa58686c38cd4216 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Thu, 19 Dec 2024 11:46:21 +0100 Subject: [PATCH] Add date time form field (#9133) - Create a really simple abstraction to unify the date and date time fields. We might dissociate them sooner than expected. - The _relative_ setting is ignored --- .../components/FormFieldInput.tsx | 14 +- ...put.tsx => FormDateTimeFieldInputBase.tsx} | 24 +- .../FormDateFieldInput.stories.tsx | 370 --------- .../FormDateTimeFieldInputBase.stories.tsx | 765 ++++++++++++++++++ 4 files changed, 791 insertions(+), 382 deletions(-) rename packages/twenty-front/src/modules/object-record/record-field/form-types/components/{FormDateFieldInput.tsx => FormDateTimeFieldInputBase.tsx} (94%) delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateFieldInput.stories.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInputBase.stories.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx index 6d7424d18..a3fa74cc5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx @@ -1,6 +1,6 @@ import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput'; import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput'; -import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput'; +import { FormDateTimeFieldInputBase } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInputBase'; import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput'; import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput'; import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput'; @@ -23,6 +23,7 @@ import { import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate'; +import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime'; import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; @@ -108,7 +109,16 @@ export const FormFieldInput = ({ VariablePicker={VariablePicker} /> ) : isFieldDate(field) ? ( - + ) : isFieldDateTime(field) ? ( + void; VariablePicker?: VariablePickerComponent; }; -export const FormDateFieldInput = ({ +export const FormDateTimeFieldInputBase = ({ + mode, label, defaultValue, onPersist, VariablePicker, -}: FormDateFieldInputProps) => { +}: FormDateTimeFieldInputBaseProps) => { const { timeZone } = useContext(UserContext); const inputId = useId(); + const placeholder = mode === 'date' ? 'mm/dd/yyyy' : 'mm/dd/yyyy hh:mm'; + const [draftValue, setDraftValue] = useState( isStandaloneVariableString(defaultValue) ? { @@ -112,7 +116,7 @@ export const FormDateFieldInput = ({ isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue) ? parseDateToString({ date: draftValueAsDate, - isDateTimeInput: false, + isDateTimeInput: mode === 'datetime', userTimezone: timeZone, }) : '', @@ -141,7 +145,7 @@ export const FormDateFieldInput = ({ useListenClickOutside({ refs: [datePickerWrapperRef], - listenerId: 'FormDateFieldInput', + listenerId: 'FormDateTimeFieldInputBase', callback: (event) => { event.stopImmediatePropagation(); @@ -164,7 +168,7 @@ export const FormDateFieldInput = ({ isDefined(newDate) ? parseDateToString({ date: newDate, - isDateTimeInput: false, + isDateTimeInput: mode === 'datetime', userTimezone: timeZone, }) : '', @@ -222,7 +226,7 @@ export const FormDateFieldInput = ({ isDefined(newDate) ? parseDateToString({ date: newDate, - isDateTimeInput: false, + isDateTimeInput: mode === 'datetime', userTimezone: timeZone, }) : '', @@ -258,7 +262,7 @@ export const FormDateFieldInput = ({ const parsedInputDateTime = parseStringToDate({ dateAsString: inputDateTimeTrimmed, - isDateTimeInput: false, + isDateTimeInput: mode === 'datetime', userTimezone: timeZone, }); @@ -284,7 +288,7 @@ export const FormDateFieldInput = ({ setInputDateTime( parseDateToString({ date: validatedDate, - isDateTimeInput: false, + isDateTimeInput: mode === 'datetime', userTimezone: timeZone, }), ); @@ -328,7 +332,7 @@ export const FormDateFieldInput = ({ <> = { - title: 'UI/Data/Field/Form/Input/FormDateFieldInput', - component: FormDateFieldInput, - args: {}, - argTypes: {}, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText('Created At'); - await canvas.findByDisplayValue('12/09/2024'); - }, -}; - -export const WithDefaultEmptyValue: Story = { - args: { - label: 'Created At', - defaultValue: undefined, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - await canvas.findByText('Created At'); - await canvas.findByDisplayValue(''); - await canvas.findByPlaceholderText('mm/dd/yyyy'); - }, -}; - -export const SetsDateWithInput: Story = { - args: { - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - - await userEvent.click(input); - - const dialog = await canvas.findByRole('dialog'); - expect(dialog).toBeVisible(); - - await userEvent.type(input, '12/08/2024{enter}'); - - await waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith('2024-12-08T00:00:00.000Z'); - }); - - expect(dialog).toBeVisible(); - }, -}; - -export const SetsDateWithDatePicker: Story = { - args: { - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - const dayToChoose = await within(datePicker).findByRole('option', { - name: 'Choose Saturday, December 7th, 2024', - }); - - await Promise.all([ - userEvent.click(dayToChoose), - - waitForElementToBeRemoved(datePicker), - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith( - expect.stringMatching(/^2024-12-07/), - ); - }), - waitFor(() => { - expect(canvas.getByDisplayValue('12/07/2024')).toBeVisible(); - }), - ]); - }, -}; - -export const ResetsDateByClickingButton: Story = { - args: { - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - const clearButton = await canvas.findByText('Clear'); - - await Promise.all([ - userEvent.click(clearButton), - - waitForElementToBeRemoved(datePicker), - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(null); - }), - waitFor(() => { - expect(input).toHaveDisplayValue(''); - }), - ]); - }, -}; - -export const ResetsDateByErasingInputContent: Story = { - args: { - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - expect(input).toHaveDisplayValue('12/09/2024'); - - await userEvent.clear(input); - - await Promise.all([ - userEvent.type(input, '{Enter}'), - - waitForElementToBeRemoved(() => canvas.queryByRole('dialog')), - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(null); - }), - waitFor(() => { - expect(input).toHaveDisplayValue(''); - }), - ]); - }, -}; - -export const DefaultsToMinValueWhenTypingReallyOldDate: Story = { - args: { - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - await Promise.all([ - userEvent.type(input, '02/02/1500{Enter}'), - - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString()); - }), - waitFor(() => { - expect(input).toHaveDisplayValue( - parseDateToString({ - date: MIN_DATE, - isDateTimeInput: false, - userTimezone: undefined, - }), - ); - }), - waitFor(() => { - const expectedDate = DateTime.fromJSDate(MIN_DATE) - .toLocal() - .set({ - day: MIN_DATE.getUTCDate(), - month: MIN_DATE.getUTCMonth() + 1, - year: MIN_DATE.getUTCFullYear(), - hour: 0, - minute: 0, - second: 0, - millisecond: 0, - }); - - const selectedDay = within(datePicker).getByRole('option', { - selected: true, - name: (accessibleName) => { - // The name looks like "Choose Sunday, December 31st, 1899" - return accessibleName.includes(expectedDate.toFormat('yyyy')); - }, - }); - expect(selectedDay).toBeVisible(); - }), - ]); - }, -}; - -export const DefaultsToMaxValueWhenTypingReallyFarDate: Story = { - args: { - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - - await userEvent.click(input); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - await Promise.all([ - userEvent.type(input, '02/02/2500{Enter}'), - - waitFor(() => { - expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString()); - }), - waitFor(() => { - expect(input).toHaveDisplayValue( - parseDateToString({ - date: MAX_DATE, - isDateTimeInput: false, - userTimezone: undefined, - }), - ); - }), - waitFor(() => { - const expectedDate = DateTime.fromJSDate(MAX_DATE) - .toLocal() - .set({ - day: MAX_DATE.getUTCDate(), - month: MAX_DATE.getUTCMonth() + 1, - year: MAX_DATE.getUTCFullYear(), - hour: 0, - minute: 0, - second: 0, - millisecond: 0, - }); - - const selectedDay = within(datePicker).getByRole('option', { - selected: true, - name: (accessibleName) => { - // The name looks like "Choose Thursday, December 30th, 2100" - return accessibleName.includes(expectedDate.toFormat('yyyy')); - }, - }); - expect(selectedDay).toBeVisible(); - }), - ]); - }, -}; - -export const SwitchesToStandaloneVariable: Story = { - args: { - label: 'Created At', - defaultValue: undefined, - onPersist: fn(), - VariablePicker: ({ onVariableSelect }) => { - return ( - - ); - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const addVariableButton = await canvas.findByText('Add variable'); - await userEvent.click(addVariableButton); - - const variableTag = await canvas.findByText('test'); - expect(variableTag).toBeVisible(); - - const removeVariableButton = canvas.getByTestId(/^remove-icon/); - - await Promise.all([ - userEvent.click(removeVariableButton), - - waitForElementToBeRemoved(variableTag), - waitFor(() => { - const input = canvas.getByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - }), - ]); - }, -}; - -export const ClickingOutsideDoesNotResetInputState: Story = { - args: { - label: 'Created At', - defaultValue: '2024-12-09T13:20:19.631Z', - onPersist: fn(), - }, - play: async ({ canvasElement, args }) => { - const defaultValueAsDisplayString = parseDateToString({ - date: new Date(args.defaultValue!), - isDateTimeInput: false, - userTimezone: undefined, - }); - - const canvas = within(canvasElement); - - const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); - expect(input).toBeVisible(); - expect(input).toHaveDisplayValue(defaultValueAsDisplayString); - - await userEvent.type(input, '{Backspace}{Backspace}'); - - const datePicker = await canvas.findByRole('dialog'); - expect(datePicker).toBeVisible(); - - await Promise.all([ - userEvent.click(canvasElement), - - waitForElementToBeRemoved(datePicker), - ]); - - expect(args.onPersist).not.toHaveBeenCalled(); - - expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); - }, -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInputBase.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInputBase.stories.tsx new file mode 100644 index 000000000..cad93289e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormDateTimeFieldInputBase.stories.tsx @@ -0,0 +1,765 @@ +import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate'; +import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate'; +import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString'; +import { expect } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { + fn, + userEvent, + waitFor, + waitForElementToBeRemoved, + within, +} from '@storybook/test'; +import { DateTime } from 'luxon'; +import { FormDateTimeFieldInputBase } from '../FormDateTimeFieldInputBase'; + +const meta: Meta = { + title: 'UI/Data/Field/Form/Input/FormDateTimeFieldInputBase', + component: FormDateTimeFieldInputBase, + args: {}, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const DateDefault: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Created At'); + await canvas.findByDisplayValue('12/09/2024'); + }, +}; + +export const DateWithDefaultEmptyValue: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: undefined, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Created At'); + await canvas.findByDisplayValue(''); + await canvas.findByPlaceholderText('mm/dd/yyyy'); + }, +}; + +export const DateSetsDateWithInput: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + + await userEvent.click(input); + + const dialog = await canvas.findByRole('dialog'); + expect(dialog).toBeVisible(); + + await userEvent.type(input, '12/08/2024{enter}'); + + await waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith('2024-12-08T00:00:00.000Z'); + }); + + expect(dialog).toBeVisible(); + }, +}; + +export const DateSetsDateWithDatePicker: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + const dayToChoose = await within(datePicker).findByRole('option', { + name: 'Choose Saturday, December 7th, 2024', + }); + + await Promise.all([ + userEvent.click(dayToChoose), + + waitForElementToBeRemoved(datePicker), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith( + expect.stringMatching(/^2024-12-07/), + ); + }), + waitFor(() => { + expect(canvas.getByDisplayValue('12/07/2024')).toBeVisible(); + }), + ]); + }, +}; + +export const DateResetsDateByClickingButton: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + const clearButton = await canvas.findByText('Clear'); + + await Promise.all([ + userEvent.click(clearButton), + + waitForElementToBeRemoved(datePicker), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(null); + }), + waitFor(() => { + expect(input).toHaveDisplayValue(''); + }), + ]); + }, +}; + +export const DateResetsDateByErasingInputContent: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + expect(input).toHaveDisplayValue('12/09/2024'); + + await userEvent.clear(input); + + await Promise.all([ + userEvent.type(input, '{Enter}'), + + waitForElementToBeRemoved(() => canvas.queryByRole('dialog')), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(null); + }), + waitFor(() => { + expect(input).toHaveDisplayValue(''); + }), + ]); + }, +}; + +export const DateDefaultsToMinValueWhenTypingReallyOldDate: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.type(input, '02/02/1500{Enter}'), + + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString()); + }), + waitFor(() => { + expect(input).toHaveDisplayValue( + parseDateToString({ + date: MIN_DATE, + isDateTimeInput: false, + userTimezone: undefined, + }), + ); + }), + waitFor(() => { + const expectedDate = DateTime.fromJSDate(MIN_DATE) + .toLocal() + .set({ + day: MIN_DATE.getUTCDate(), + month: MIN_DATE.getUTCMonth() + 1, + year: MIN_DATE.getUTCFullYear(), + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }); + + const selectedDay = within(datePicker).getByRole('option', { + selected: true, + name: (accessibleName) => { + // The name looks like "Choose Sunday, December 31st, 1899" + return accessibleName.includes(expectedDate.toFormat('yyyy')); + }, + }); + expect(selectedDay).toBeVisible(); + }), + ]); + }, +}; + +export const DateDefaultsToMaxValueWhenTypingReallyFarDate: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.type(input, '02/02/2500{Enter}'), + + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString()); + }), + waitFor(() => { + expect(input).toHaveDisplayValue( + parseDateToString({ + date: MAX_DATE, + isDateTimeInput: false, + userTimezone: undefined, + }), + ); + }), + waitFor(() => { + const expectedDate = DateTime.fromJSDate(MAX_DATE) + .toLocal() + .set({ + day: MAX_DATE.getUTCDate(), + month: MAX_DATE.getUTCMonth() + 1, + year: MAX_DATE.getUTCFullYear(), + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }); + + const selectedDay = within(datePicker).getByRole('option', { + selected: true, + name: (accessibleName) => { + // The name looks like "Choose Thursday, December 30th, 2100" + return accessibleName.includes(expectedDate.toFormat('yyyy')); + }, + }); + expect(selectedDay).toBeVisible(); + }), + ]); + }, +}; + +export const DateSwitchesToStandaloneVariable: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + VariablePicker: ({ onVariableSelect }) => { + return ( + + ); + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const addVariableButton = await canvas.findByText('Add variable'); + await userEvent.click(addVariableButton); + + const variableTag = await canvas.findByText('test'); + expect(variableTag).toBeVisible(); + + const removeVariableButton = canvas.getByTestId(/^remove-icon/); + + await Promise.all([ + userEvent.click(removeVariableButton), + + waitForElementToBeRemoved(variableTag), + waitFor(() => { + const input = canvas.getByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + }), + ]); + }, +}; + +export const DateClickingOutsideDoesNotResetInputState: Story = { + args: { + mode: 'date', + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const defaultValueAsDisplayString = parseDateToString({ + date: new Date(args.defaultValue!), + isDateTimeInput: false, + userTimezone: undefined, + }); + + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy'); + expect(input).toBeVisible(); + expect(input).toHaveDisplayValue(defaultValueAsDisplayString); + + await userEvent.type(input, '{Backspace}{Backspace}'); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.click(canvasElement), + + waitForElementToBeRemoved(datePicker), + ]); + + expect(args.onPersist).not.toHaveBeenCalled(); + + expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); + }, +}; + +// ---- + +export const DateTimeDefault: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Created At'); + await canvas.findByDisplayValue(/12\/09\/2024 \d{2}:20/); + }, +}; + +export const DateTimeWithDefaultEmptyValue: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: undefined, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Created At'); + await canvas.findByDisplayValue(''); + await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + }, +}; + +export const DateTimeSetsDateTimeWithInput: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + + await userEvent.click(input); + + const dialog = await canvas.findByRole('dialog'); + expect(dialog).toBeVisible(); + + await userEvent.type(input, '12/08/2024 12:10{enter}'); + + await waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith( + expect.stringMatching(/2024-12-08T\d{2}:10:00.000Z/), + ); + }); + + expect(dialog).toBeVisible(); + }, +}; + +export const DateTimeDoesNotSetDateWithoutTime: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + + await userEvent.click(input); + + const dialog = await canvas.findByRole('dialog'); + expect(dialog).toBeVisible(); + + await userEvent.type(input, '12/08/2024{enter}'); + + expect(args.onPersist).not.toHaveBeenCalled(); + expect(dialog).toBeVisible(); + }, +}; + +export const DateTimeSetsDateTimeWithDatePicker: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + const dayToChoose = await within(datePicker).findByRole('option', { + name: 'Choose Saturday, December 7th, 2024', + }); + + await Promise.all([ + userEvent.click(dayToChoose), + + waitForElementToBeRemoved(datePicker), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith( + expect.stringMatching(/^2024-12-07/), + ); + }), + waitFor(() => { + expect( + canvas.getByDisplayValue(/12\/07\/2024 \d{2}:\d{2}/), + ).toBeVisible(); + }), + ]); + }, +}; + +export const DateTimeResetsDateByClickingButton: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + const clearButton = await canvas.findByText('Clear'); + + await Promise.all([ + userEvent.click(clearButton), + + waitForElementToBeRemoved(datePicker), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(null); + }), + waitFor(() => { + expect(input).toHaveDisplayValue(''); + }), + ]); + }, +}; + +export const DateTimeResetsDateByErasingInputContent: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + expect(input).toHaveDisplayValue(/12\/09\/2024 \d{2}:\d{2}/); + + await userEvent.clear(input); + + await Promise.all([ + userEvent.type(input, '{Enter}'), + + waitForElementToBeRemoved(() => canvas.queryByRole('dialog')), + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(null); + }), + waitFor(() => { + expect(input).toHaveDisplayValue(''); + }), + ]); + }, +}; + +export const DateTimeDefaultsToMinValueWhenTypingReallyOldDate: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.type(input, '02/02/1500 10:10{Enter}'), + + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString()); + }), + waitFor(() => { + expect(input).toHaveDisplayValue( + parseDateToString({ + date: MIN_DATE, + isDateTimeInput: true, + userTimezone: undefined, + }), + ); + }), + waitFor(() => { + const expectedDate = DateTime.fromJSDate(MIN_DATE) + .toLocal() + .set({ + day: MIN_DATE.getUTCDate(), + month: MIN_DATE.getUTCMonth() + 1, + year: MIN_DATE.getUTCFullYear(), + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }); + + const selectedDay = within(datePicker).getByRole('option', { + selected: true, + name: (accessibleName) => { + // The name looks like "Choose Sunday, December 31st, 1899" + return accessibleName.includes(expectedDate.toFormat('yyyy')); + }, + }); + expect(selectedDay).toBeVisible(); + }), + ]); + }, +}; + +export const DateTimeDefaultsToMaxValueWhenTypingReallyFarDate: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + + await userEvent.click(input); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.type(input, '02/02/2500 10:10{Enter}'), + + waitFor(() => { + expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString()); + }), + waitFor(() => { + expect(input).toHaveDisplayValue( + parseDateToString({ + date: MAX_DATE, + isDateTimeInput: true, + userTimezone: undefined, + }), + ); + }), + waitFor(() => { + const expectedDate = DateTime.fromJSDate(MAX_DATE) + .toLocal() + .set({ + day: MAX_DATE.getUTCDate(), + month: MAX_DATE.getUTCMonth() + 1, + year: MAX_DATE.getUTCFullYear(), + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }); + + const selectedDay = within(datePicker).getByRole('option', { + selected: true, + name: (accessibleName) => { + // The name looks like "Choose Thursday, December 30th, 2100" + return accessibleName.includes(expectedDate.toFormat('yyyy')); + }, + }); + expect(selectedDay).toBeVisible(); + }), + ]); + }, +}; + +export const DateTimeSwitchesToStandaloneVariable: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: undefined, + onPersist: fn(), + VariablePicker: ({ onVariableSelect }) => { + return ( + + ); + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const addVariableButton = await canvas.findByText('Add variable'); + await userEvent.click(addVariableButton); + + const variableTag = await canvas.findByText('test'); + expect(variableTag).toBeVisible(); + + const removeVariableButton = canvas.getByTestId(/^remove-icon/); + + await Promise.all([ + userEvent.click(removeVariableButton), + + waitForElementToBeRemoved(variableTag), + waitFor(() => { + const input = canvas.getByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + }), + ]); + }, +}; + +export const DateTimeClickingOutsideDoesNotResetInputState: Story = { + args: { + mode: 'datetime', + label: 'Created At', + defaultValue: '2024-12-09T13:20:19.631Z', + onPersist: fn(), + }, + play: async ({ canvasElement, args }) => { + const defaultValueAsDisplayString = parseDateToString({ + date: new Date(args.defaultValue!), + isDateTimeInput: true, + userTimezone: undefined, + }); + + const canvas = within(canvasElement); + + const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm'); + expect(input).toBeVisible(); + expect(input).toHaveDisplayValue(defaultValueAsDisplayString); + + await userEvent.type(input, '{Backspace}{Backspace}'); + + const datePicker = await canvas.findByRole('dialog'); + expect(datePicker).toBeVisible(); + + await Promise.all([ + userEvent.click(canvasElement), + + waitForElementToBeRemoved(datePicker), + ]); + + expect(args.onPersist).not.toHaveBeenCalled(); + + expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2)); + }, +};