Skip to content

Commit

Permalink
feat: [M3-8611]- New DatePicker Component (#11151)
Browse files Browse the repository at this point in the history
* unit test coverage for HostNameTableCell

* Revert "unit test coverage for HostNameTableCell"

This reverts commit b274baf.

* chore: [M3-8662] - Update Github Actions actions (#11009)

* update actions

* add changeset

---------

Co-authored-by: Banks Nussman <banks@nussman.us>

* Basci date picker component

* Test coverage for date picker component

* DatePicker Stories

* Custom DateTimePicker component

* Reusable TimeZone Select Component

* Create custom DateTimeRangePicker component

* Storybook for DateTimePicker

* Fix tests and remove console warnings

* changeset

* Update packages/manager/src/components/DatePicker/DateTimeRangePicker.tsx

Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com>

* Adjust styles for DatePicker

* Adjust styles for DateTimePicker

* update imports

* Render time and timezone conditionally in DateTimePicker component

* Move DatePicker to UI package

* Add DatePicker dependencies

* Code cleanup

* PR feedback

* code cleanup

* Move DatePicker back to src/components

* Reverting changes

* Code cleanup

* Adjust broken tests

* Update TimeZoneSelect.tsx

* Code cleanup

* Add validation for start date agains end date.

* Adjust styles for TimePicker component.

* PR feedback - @jaalah-akamai

* allow error messages from props.

* Update storybook components with args

* Update props

* PR feedback - @hana-akamai

---------

Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com>
Co-authored-by: Banks Nussman <banks@nussman.us>
Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com>
  • Loading branch information
4 people authored Dec 12, 2024
1 parent 4f19394 commit 518d85d
Show file tree
Hide file tree
Showing 12 changed files with 1,287 additions and 1 deletion.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11151-added-1731944151381.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

New DatePicker Component ([#11151](https://github.com/linode/manager/pull/11151))
124 changes: 124 additions & 0 deletions packages/manager/src/components/DatePicker/DatePicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { action } from '@storybook/addon-actions';
import * as React from 'react';

import { DatePicker } from './DatePicker';

import type { Meta, StoryObj } from '@storybook/react';
import type { DateTime } from 'luxon';

type Story = StoryObj<typeof DatePicker>;

export const Default: Story = {
argTypes: {
errorText: {
control: 'text',
description: 'Error text to display below the input',
},
format: {
control: 'text',
description: 'Format of the date when rendered in the input field',
},
helperText: {
control: 'text',
description: 'Helper text to display below the input',
},
label: {
control: 'text',
description: 'Label to display for the date picker input',
},
onChange: {
action: 'date-changed',
description: 'Callback function fired when the value changes',
},
placeholder: {
control: 'text',
description: 'Placeholder text for the date picker input',
},
textFieldProps: {
control: 'object',
description:
'Additional props to pass to the underlying TextField component',
},
value: {
control: 'date',
description: 'The currently selected date',
},
},
args: {
errorText: '',
format: 'yyyy-MM-dd',
label: 'Select a Date',
onChange: action('date-changed'),
placeholder: 'yyyy-MM-dd',
textFieldProps: { label: 'Select a Date' },
value: null,
},
};

export const ControlledExample: Story = {
args: {
errorText: '',
format: 'yyyy-MM-dd',
helperText: 'This is a controlled DatePicker',
label: 'Controlled Date Picker',
placeholder: 'yyyy-MM-dd',
value: null,
},
render: (args) => {
const ControlledDatePicker = () => {
const [selectedDate, setSelectedDate] = React.useState<DateTime | null>();

const handleChange = (newDate: DateTime | null) => {
setSelectedDate(newDate);
action('Controlled date change')(newDate?.toISO());
};

return (
<DatePicker {...args} onChange={handleChange} value={selectedDate} />
);
};

return <ControlledDatePicker />;
},
};

const meta: Meta<typeof DatePicker> = {
argTypes: {
errorText: {
control: 'text',
},
format: {
control: 'text',
},
helperText: {
control: 'text',
},
label: {
control: 'text',
},
onChange: {
action: 'date-changed',
},
placeholder: {
control: 'text',
},
textFieldProps: {
control: 'object',
},
value: {
control: 'date',
},
},
args: {
errorText: '',
format: 'yyyy-MM-dd',
helperText: '',
label: 'Select a Date',
placeholder: 'yyyy-MM-dd',
value: null,
},
component: DatePicker,
title: 'Components/DatePicker/DatePicker',
};

export default meta;
80 changes: 80 additions & 0 deletions packages/manager/src/components/DatePicker/DatePicker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DateTime } from 'luxon';
import * as React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';

import { DatePicker } from './DatePicker';

import type { DatePickerProps } from './DatePicker';

const props: DatePickerProps = {
onChange: vi.fn(),
placeholder: 'Pick a date',
textFieldProps: { errorText: 'Invalid date', label: 'Select a date' },
value: null,
};

describe('DatePicker', () => {
it('should render the DatePicker component', () => {
renderWithTheme(<DatePicker {...props} />);
const DatePickerField = screen.getByRole('textbox', {
name: 'Select a date',
});

expect(DatePickerField).toBeVisible();
});

it('should handle value changes', async () => {
renderWithTheme(<DatePicker {...props} />);

const calendarButton = screen.getByRole('button', { name: 'Choose date' });

// Click the calendar button to open the date picker
await userEvent.click(calendarButton);

// Find a date button to click (e.g., the 15th of the month)
const dateToSelect = screen.getByRole('gridcell', { name: '15' });
await userEvent.click(dateToSelect);

// Check if onChange was called after selecting a date
expect(props.onChange).toHaveBeenCalled();
});

it('should display the error text when provided', () => {
renderWithTheme(<DatePicker {...props} />);
const errorMessage = screen.getByText('Invalid date');
expect(errorMessage).toBeVisible();
});

it('should display the helper text when provided', () => {
renderWithTheme(<DatePicker {...props} helperText="Choose a valid date" />);
const helperText = screen.getByText('Choose a valid date');
expect(helperText).toBeVisible();
});

it('should use the default format when no format is specified', () => {
renderWithTheme(
<DatePicker {...props} value={DateTime.fromISO('2024-10-25')} />
);
const datePickerField = screen.getByRole('textbox', {
name: 'Select a date',
});
expect(datePickerField).toHaveValue('2024-10-25');
});

it('should handle the custom format correctly', () => {
renderWithTheme(
<DatePicker
{...props}
format="dd/MM/yyyy"
value={DateTime.fromISO('2024-10-25')}
/>
);
const datePickerField = screen.getByRole('textbox', {
name: 'Select a date',
});
expect(datePickerField).toHaveValue('25/10/2024');
});
});
91 changes: 91 additions & 0 deletions packages/manager/src/components/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { TextField } from '@linode/ui';
import { useTheme } from '@mui/material/styles';
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon';
import { DatePicker as MuiDatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import React from 'react';

import type { TextFieldProps } from '@linode/ui';
import type { DatePickerProps as MuiDatePickerProps } from '@mui/x-date-pickers/DatePicker';
import type { DateTime } from 'luxon';

export interface DatePickerProps
extends Omit<MuiDatePickerProps<DateTime>, 'onChange' | 'value'> {
/** Error text to display below the input */
errorText?: string;
/** Format of the date when rendered in the input field. */
format?: string;
/** Helper text to display below the input */
helperText?: string;
/** Label to display for the date picker input */
label?: string;
/** Callback function fired when the value changes */
onChange: (newDate: DateTime | null) => void;
/** Placeholder text for the date picker input */
placeholder?: string;
/** Additional props to pass to the underlying TextField component */
textFieldProps?: Omit<TextFieldProps, 'onChange' | 'value'>;
/** The currently selected date */
value?: DateTime | null;
}

export const DatePicker = ({
format = 'yyyy-MM-dd',
helperText = '',
label = 'Select a date',
onChange,
placeholder = 'Pick a date',
textFieldProps,
value = null,
...props
}: DatePickerProps) => {
const theme = useTheme();

const onChangeHandler = (newDate: DateTime | null) => {
onChange(newDate);
};

return (
<LocalizationProvider dateAdapter={AdapterLuxon}>
<MuiDatePicker
format={format}
onChange={onChangeHandler}
reduceAnimations // disables the rendering animation
value={value}
{...props}
slotProps={{
// TODO: Move styling customization to global theme styles.
popper: {
sx: {
'& .MuiDayCalendar-weekDayLabel': {
fontSize: '0.875rem',
},
'& .MuiPickersCalendarHeader-label': {
fontWeight: 'bold',
},
'& .MuiPickersCalendarHeader-root': {
fontSize: '0.875rem',
},
'& .MuiPickersDay-root': {
fontSize: '0.875rem',
margin: `${theme.spacing(0.5)}px`,
},
backgroundColor: theme.bg.main,
borderRadius: `${theme.spacing(2)}`,
boxShadow: `0px 4px 16px ${theme.color.boxShadowDark}`,
},
},
textField: {
...textFieldProps,
helperText,
label,
placeholder,
},
}}
slots={{
textField: TextField,
}}
/>
</LocalizationProvider>
);
};
Loading

0 comments on commit 518d85d

Please sign in to comment.