import React, { useEffect, useState } from 'react' import { action } from '@storybook/addon-actions' import { type Meta, type StoryObj } from '@storybook/react' import { expect, userEvent, within } from '@storybook/test' import Highlight from 'react-highlight' import { Button } from '~components/ButtonV1' import { defaultMonthControls } from '~components/Calendar/_docs/controls/defaultMonthControls' import { weekStartsOnControls } from '~components/Calendar/_docs/controls/weekStartsOnControls' import { type FieldMessageStatus } from '~components/FieldMessage' import { Text } from '~components/Text' import { DatePicker, type ValidationResponse } from '../index' import { datePickerLocaleControls } from './controls/datePickerLocaleControls' import { disabledDayMatchersControls } from './controls/disabledDayMatchersControls' const meta = { title: 'Components/Datepickers/DatePicker', component: DatePicker, argTypes: { ...datePickerLocaleControls, ...defaultMonthControls, ...disabledDayMatchersControls, ...weekStartsOnControls, buttonRef: { control: false, }, selectedDay: { options: ['None', 'Today', 'May2022'], control: { type: 'select', labels: { None: 'undefined', May2022: '1st May 2022', }, }, mapping: { None: undefined, Today: new Date(), May2022: new Date('2022-05-01'), }, }, validationMessage: { control: 'text', }, description: { control: 'text', }, onValidate: { options: [undefined, 'Actions'], control: { type: 'radio', labels: { Actions: 'Log in Actions', }, }, mapping: { Actions: action('onValidate'), }, }, }, args: { labelText: 'Date', selectedDay: undefined, onDayChange: action('on day change'), }, } satisfies Meta export default meta type Story = StoryObj const DatePickerTemplate: Story = { render: (args) => { const [selectedDate, setValueDate] = useState(args.selectedDay) useEffect(() => { setValueDate(args.selectedDay) }, [args.selectedDay]) return }, } export const Playground: Story = { ...DatePickerTemplate, parameters: { docs: { canvas: { sourceState: 'shown', }, }, }, } export const LabelText: Story = { ...DatePickerTemplate, args: { labelText: 'Label text' }, } const sourceCodeControlled = ` const [selectedDate, setValueDate] = useState() return ( ) ` export const Controlled: Story = { ...DatePickerTemplate, args: { selectedDay: new Date() }, parameters: { docs: { source: { code: sourceCodeControlled, }, }, }, } export const Locale: Story = { ...DatePickerTemplate, args: { locale: 'en-US', selectedDay: new Date() }, } export const Description: Story = { args: { description: 'Custom description!' }, } export const Validation: Story = { render: () => { const [selectedDate, setValueDate] = useState(new Date('2022-05-05')) const [status, setStatus] = useState() const [response, setResponse] = useState() const [validationMessage, setValidationMessage] = useState() const handleValidation = (validationResponse: ValidationResponse): void => { setResponse(validationResponse) // An example of additional validation if ( validationResponse.isValidDate && validationResponse.date?.getFullYear() !== new Date().getFullYear() ) { setStatus('caution') setValidationMessage('Date is not this year') return } setStatus(validationResponse.status) setValidationMessage(validationResponse.validationMessage) } const submitRequest: React.FormEventHandler = (e) => { e.preventDefault() if (status === 'error' || status === 'caution') { setValidationMessage('There is an error') setStatus('error') return alert('Error') } alert('Success') } return ( <>
NOTE: This story includes additional custom validation to provide some guidance when dealing with validation other than date isInvalid or isDisabled.
  • There will be a caution when the selectedDay is valid but{' '} is not within this year.
  • There will be an error when the submit button is clicked and there is a current error within the DatePicker.
The onValidate callback returns a validationResponse object which provides data such as a default validation message, and can be utilised for custom validation.
{JSON.stringify(response, null, 4)}
  • isInvalid: A date that cannot be parsed. e.g "potato".
  • isDisabled: A date that have been set as disabled through the{' '} disabledDates prop.
  • isEmpty: Input is empty.
  • isValidDate: Date input that is not invalid nor{' '} disabled nor empty.
) }, parameters: { docs: { source: { type: 'code' } }, controls: { disable: true }, }, } export const DisabledDays: Story = { parameters: { controls: { include: /^disabled/ } }, } export const LimitedWindowWidth: Story = { name: 'At 400% window size', parameters: { controls: { disable: true }, viewport: { viewports: { ViewportAt400: { name: 'Viewport at 400%', styles: { width: '320px', height: '350px', }, }, }, defaultViewport: 'ViewportAt400', }, a11y: { disable: true }, // accessible label fix to be addressed in a separate PR }, play: async ({ canvasElement }) => { const canvas = within(canvasElement) await userEvent.click(canvas.getByRole('button', { name: 'Choose date' })) await expect(canvas.getByRole('dialog')).toBeInTheDocument() }, } export const AboveIfAvailable: Story = { name: 'Limited viewport autoplacement above', args: { labelText: 'Calendar with space above', }, parameters: { viewport: { viewports: { LimitedViewportAutoPlace: { name: 'Limited vertical space', styles: { width: '1024px', height: '500px', }, }, }, defaultViewport: 'LimitedViewportAutoPlace', }, a11y: { disable: true }, // accessible label fix to be addressed in a separate PR }, decorators: [ (Story) => (
), ], play: async ({ canvasElement }) => { const canvas = within(canvasElement) await userEvent.click(canvas.getByRole('button', { name: 'Choose date' })) await expect(canvas.getByRole('dialog')).toBeInTheDocument() }, } export const LimitedViewportHeight: Story = { name: 'Limited viewport height', args: { labelText: 'Calendar with reduced space below', }, parameters: { viewport: { viewports: { LimitedViewportHeight: { name: 'Limited vertical space', styles: { width: '1024px', height: '300px', }, }, }, defaultViewport: 'LimitedViewportHeight', }, a11y: { disable: true }, // accessible label fix to be addressed in a separate PR }, decorators: [ (Story) => (
), ], play: async ({ canvasElement }) => { const canvas = within(canvasElement) await userEvent.click(canvas.getByRole('button', { name: 'Choose date' })) await expect(canvas.getByRole('dialog')).toBeInTheDocument() }, } export const FullViewportHeight: Story = { name: 'Full viewport height', args: { labelText: 'Calendar with full space below', }, decorators: [ (Story) => (
), ], play: async ({ canvasElement }) => { const canvas = within(canvasElement) await userEvent.click(canvas.getByRole('button', { name: 'Choose date' })) await expect(canvas.getByRole('dialog')).toBeInTheDocument() }, parameters: { a11y: { disable: true }, // accessible label fix to be addressed in a separate PR }, }