Skip to content
This repository has been archived by the owner on Sep 18, 2023. It is now read-only.

Commit

Permalink
Add timezone support for dates package (#59)
Browse files Browse the repository at this point in the history
* add timezone support for dates package

* added timezone to docs example

* added docs for timezone

* fix prettier style
  • Loading branch information
wuifdesign authored Sep 16, 2023
1 parent b4d4258 commit 18ead2c
Show file tree
Hide file tree
Showing 37 changed files with 861 additions and 45 deletions.
3 changes: 3 additions & 0 deletions configuration/jest/global-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = async () => {
process.env.TZ = "America/New_York"; // -05:00 (STD) or -04:00 (DST)
};
31 changes: 31 additions & 0 deletions docs/pages/dates/dates-provider.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,36 @@ components exported from `@mantine/dates` package. `DatesProvider` supports the
- `locale` – dayjs locale, note that you also need to import corresponding locale module from dayjs. Default value is `en`.
- `firstDayOfWeek` – number from 0 to 6, where 0 is Sunday and 6 is Saturday. Default value is 1 – Monday.
- `weekendDays` – an array of numbers from 0 to 6, where 0 is Sunday and 6 is Saturday. Default value is `[0, 6]` – Saturday and Sunday.
- `timezone` – a textual representation of a time zone, for example, `UTC`.

<Demo data={DatesProviderDemos.usage} />

## Timezone

When working with the `DatesProvider`, parsing the `timezone` parameter offers a valuable feature for controlling how
dates and times are displayed within your application. By specifying a timezone of your choice, you can ensure
that date information is presented in the desired timezone, rather than relying on the user's browser settings.
If you don't provide a `timezone` parameter or explicitly set it to `undefined`, the application will default to using
the user's browser timezone.

The `timezone` parameter sets the timezone context for all components integrated within the `DatesProvider`. This means
that all date and time-related data displayed within your application will adhere to the specified timezone. This simplifies
the process of maintaining consistency in how dates and times are presented to users across various parts of your application.

### Accessing the Timezone Information

If you need to access the current timezone information from other parts of your application, you can leverage the
`getTimezone()` function provided by the `useDatesContext()` hook. This function allows you to retrieve the active
timezone setting and use it as needed.

### Date Format Considerations

It's important to note that the `DatesProvider` system supports the provision of dates in the user's local timezone.
However, many backend systems and data sources use Coordinated Universal Time (UTC) timestamps. In such cases, you can
easily convert and parse UTC timestamps into the user's timezone using JavaScript, as demonstrated by
the example: `new Date('2000-10-03 02:10:00Z')`.

By effectively utilizing the `timezone` parameter and the provided functions, you can tailor the presentation of date
and time data to meet the specific requirements of your application while maintaining compatibility with various data sources.

<Demo data={DatesProviderDemos.timezone} />
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = {
},
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
setupFilesAfterEnv: ['./configuration/jest/jsdom.mocks.js'],
globalSetup: "./configuration/jest/global-setup.js",
moduleNameMapper: {
'@mantine/(.*)': '<rootDir>/src/mantine-$1/src',
'\\.(css)$': 'identity-obj-proxy',
Expand Down
134 changes: 134 additions & 0 deletions src/mantine-dates/src/components/Calendar/Calendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import { render, tests, screen, userEvent } from '@mantine/tests';
import { datesTests } from '@mantine/dates-tests';
import { Calendar, CalendarProps, CalendarStylesNames } from './Calendar';
import { DatesProvider } from '../DatesProvider';

const defaultProps: CalendarProps = {
defaultDate: new Date(2022, 3, 11),
Expand Down Expand Up @@ -184,6 +185,36 @@ describe('@mantine/dates/Calendar', () => {
expect(screen.getByText('2020 – 2029')).toBeInTheDocument();
});

it('renders correct header labels with defaultDate (uncontrolled) with timezone (UTC)', async () => {
render(
<DatesProvider settings={{ timezone: 'UTC' }}>
<Calendar {...defaultProps} defaultDate={new Date(2021, 0, 31, 23)} />
</DatesProvider>
);
expectHeaderLevel('month', 'February 2021');

await userEvent.click(screen.getByLabelText('month-level'));
expectHeaderLevel('year', '2021');

await userEvent.click(screen.getByLabelText('year-level'));
expect(screen.getByText('2020 – 2029')).toBeInTheDocument();
});

it('renders correct header labels with defaultDate (uncontrolled) with timezone (America/Los_Angeles)', async () => {
render(
<DatesProvider settings={{ timezone: 'America/Los_Angeles' }}>
<Calendar {...defaultProps} defaultDate={new Date(2021, 0, 31, 23)} />
</DatesProvider>
);
expectHeaderLevel('month', 'January 2021');

await userEvent.click(screen.getByLabelText('month-level'));
expectHeaderLevel('year', '2021');

await userEvent.click(screen.getByLabelText('year-level'));
expect(screen.getByText('2020 – 2029')).toBeInTheDocument();
});

it('renders correct header labels with date (controlled)', async () => {
render(<Calendar {...defaultProps} date={new Date(2021, 3, 11)} />);
expectHeaderLevel('month', 'April 2021');
Expand All @@ -195,6 +226,21 @@ describe('@mantine/dates/Calendar', () => {
expect(screen.getByText('2020 – 2029')).toBeInTheDocument();
});

it('renders correct header labels with date (controlled) with timezone', async () => {
render(
<DatesProvider settings={{ timezone: 'UTC' }}>
<Calendar {...defaultProps} date={new Date(2021, 0, 31, 23)} />
</DatesProvider>
);
expectHeaderLevel('month', 'February 2021');

await userEvent.click(screen.getByLabelText('month-level'));
expectHeaderLevel('year', '2021');

await userEvent.click(screen.getByLabelText('year-level'));
expect(screen.getByText('2020 – 2029')).toBeInTheDocument();
});

it('changes displayed date when next/previous controls are clicked with defaultDate prop (uncontrolled)', async () => {
const { rerender } = render(<Calendar {...defaultProps} level="month" />);
expectHeaderLevel('month', 'April 2022');
Expand All @@ -218,6 +264,30 @@ describe('@mantine/dates/Calendar', () => {
expect(screen.getByText('2020 – 2029')).toBeInTheDocument();
});

it('changes displayed date when next/previous controls are clicked with defaultDate prop (uncontrolled) with timezone', async () => {
const { rerender } = render(
<DatesProvider settings={{ timezone: 'UTC' }}>
<Calendar {...defaultProps} defaultDate={new Date(2021, 0, 31, 23)} level="month" />
</DatesProvider>
);
expectHeaderLevel('month', 'February 2021');
await clickNext('month');
expectHeaderLevel('month', 'March 2021');
await clickPrevious('month');
expectHeaderLevel('month', 'February 2021');

rerender(
<DatesProvider settings={{ timezone: 'UTC' }}>
<Calendar {...defaultProps} defaultDate={new Date(2020, 11, 31, 23)} level="year" />
</DatesProvider>
);
expectHeaderLevel('year', '2021');
await clickNext('year');
expectHeaderLevel('year', '2022');
await clickPrevious('year');
expectHeaderLevel('year', '2021');
});

it('does not change date when next/previous controls are clicked with date prop (controlled)', async () => {
const { rerender } = render(
<Calendar {...defaultProps} date={new Date(2022, 3, 11)} level="month" />
Expand All @@ -237,6 +307,26 @@ describe('@mantine/dates/Calendar', () => {
expect(screen.getByText('2020 – 2029')).toBeInTheDocument();
});

it('changes displayed date when next/previous controls are clicked with date prop (controlled) with timezone', async () => {
const { rerender } = render(
<DatesProvider settings={{ timezone: 'UTC' }}>
<Calendar {...defaultProps} date={new Date(2021, 0, 31, 23)} level="month" />
</DatesProvider>
);
expectHeaderLevel('month', 'February 2021');
await clickNext('month');
expectHeaderLevel('month', 'February 2021');

rerender(
<DatesProvider settings={{ timezone: 'UTC' }}>
<Calendar {...defaultProps} date={new Date(2020, 11, 31, 23)} level="year" />
</DatesProvider>
);
expectHeaderLevel('year', '2021');
await clickNext('year');
expectHeaderLevel('year', '2021');
});

it('calls onDateChange when date changes', async () => {
const spy = jest.fn();
const { rerender } = render(
Expand Down Expand Up @@ -270,6 +360,50 @@ describe('@mantine/dates/Calendar', () => {
expect(spy).toHaveBeenLastCalledWith(new Date(2012, 3, 11));
});

it('calls onDateChange when date changes with timezone', async () => {
const spy = jest.fn();
const { rerender } = render(
<DatesProvider settings={{ timezone: 'UTC' }}>
<Calendar {...defaultProps} level="month" date={new Date(2022, 7, 11)} onDateChange={spy} />
</DatesProvider>
);

await clickNext('month');
expect(spy).toHaveBeenLastCalledWith(new Date(2022, 8, 11));

await clickPrevious('month');
expect(spy).toHaveBeenLastCalledWith(new Date(2022, 6, 11));

rerender(
<DatesProvider settings={{ timezone: 'UTC' }}>
<Calendar {...defaultProps} level="year" date={new Date(2022, 7, 11)} onDateChange={spy} />
</DatesProvider>
);

await clickNext('year');
expect(spy).toHaveBeenLastCalledWith(new Date(2023, 7, 11));

await clickPrevious('year');
expect(spy).toHaveBeenLastCalledWith(new Date(2021, 7, 11));

rerender(
<DatesProvider settings={{ timezone: 'UTC' }}>
<Calendar
{...defaultProps}
level="decade"
date={new Date(2022, 7, 11)}
onDateChange={spy}
/>
</DatesProvider>
);

await clickNext('decade');
expect(spy).toHaveBeenLastCalledWith(new Date(2032, 7, 11));

await clickPrevious('decade');
expect(spy).toHaveBeenLastCalledWith(new Date(2012, 7, 11));
});

it('supports maxLevel', async () => {
render(<Calendar {...defaultProps} defaultLevel="month" maxLevel="year" />);
expectLevelsCount([1, 0]);
Expand Down
18 changes: 14 additions & 4 deletions src/mantine-dates/src/components/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { clampLevel } from './clamp-level/clamp-level';
import { MonthLevelSettings } from '../MonthLevel';
import { YearLevelSettings } from '../YearLevel';
import { DecadeLevelSettings } from '../DecadeLevel';
import { useDatesContext } from '../DatesProvider';
import { shiftTimezone } from '../../utils';
import { useUncontrolledDates } from '../../hooks';

export type CalendarStylesNames =
| MonthLevelGroupStylesNames
Expand Down Expand Up @@ -77,6 +80,9 @@ export interface CalendarSettings
export interface CalendarBaseProps {
__staticSelector?: string;

/** Internal Variable to check if timezones were applied by parent component */
__timezoneApplied?: boolean;

/** Prevents focus shift when buttons are clicked */
__preventFocus?: boolean;

Expand Down Expand Up @@ -219,6 +225,7 @@ export const Calendar = factory<CalendarFactory>((_props, ref) => {
onNextMonth,
onPreviousMonth,
static: isStatic,
__timezoneApplied,
...others
} = props;

Expand All @@ -235,11 +242,12 @@ export const Calendar = factory<CalendarFactory>((_props, ref) => {
onChange: onLevelChange,
});

const [_date, setDate] = useUncontrolled({
const [_date, setDate] = useUncontrolledDates({
type: 'default',
value: date,
defaultValue: defaultDate,
finalValue: null,
onChange: onDateChange,
onChange: onDateChange as any,
applyTimezone: !__timezoneApplied,
});

const stylesApiProps = {
Expand All @@ -250,8 +258,10 @@ export const Calendar = factory<CalendarFactory>((_props, ref) => {
size,
};

const ctx = useDatesContext();

const _columnsToScroll = columnsToScroll || numberOfColumns || 1;
const currentDate = _date || new Date();
const currentDate = _date || shiftTimezone('add', new Date(), ctx.getTimezone());

const handleNextMonth = () => {
const nextDate = dayjs(currentDate).add(_columnsToScroll, 'month').toDate();
Expand Down
45 changes: 45 additions & 0 deletions src/mantine-dates/src/components/DateInput/DateInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '@mantine/dates-tests';
import { __InputStylesNames } from '@mantine/core';
import { DateInput, DateInputProps } from './DateInput';
import { DatesProvider } from '../DatesProvider';

const defaultProps: DateInputProps = {
popoverProps: { transitionProps: { duration: 0 }, withinPortal: false },
Expand Down Expand Up @@ -148,6 +149,17 @@ describe('@mantine/dates/DateInput', () => {
expectValue(container, 'April 1, 2022');
});

it('supports uncontrolled state (dropdown click) with timezone', async () => {
const { container } = render(
<DatesProvider settings={{ timezone: 'UTC' }}>
<DateInput date={new Date(2022, 0, 31, 23)} {...defaultProps} />
</DatesProvider>
);
await userEvent.tab();
await clickControl(container, 4);
expectValue(container, 'February 4, 2022');
});

it('supports controlled state (dropdown click)', async () => {
const spy = jest.fn();
const { container } = render(
Expand All @@ -164,6 +176,24 @@ describe('@mantine/dates/DateInput', () => {
expect(spy).toHaveBeenCalledWith(new Date(2022, 3, 1));
});

it('supports controlled state (dropdown click) with timezone', async () => {
const spy = jest.fn();
const { container } = render(
<DatesProvider settings={{ timezone: 'UTC' }}>
<DateInput
{...defaultProps}
date={new Date(2022, 0, 31, 23)}
value={new Date(2022, 0, 31, 23)}
onChange={spy}
/>
</DatesProvider>
);
await userEvent.tab();
await clickControl(container, 4);
expectValue(container, 'February 1, 2022');
expect(spy).toHaveBeenCalledWith(new Date(2022, 1, 3, 23));
});

it('supports uncontrolled state (free input)', async () => {
const { container } = render(<DateInput date={new Date(2022, 3, 11)} {...defaultProps} />);
await userEvent.tab();
Expand All @@ -185,6 +215,21 @@ describe('@mantine/dates/DateInput', () => {
expect(spy).toHaveBeenLastCalledWith(new Date(2022, 3, 1));
});

it('supports controlled state (free input) with timezone', async () => {
const spy = jest.fn();
const { container } = render(
<DatesProvider settings={{ timezone: 'UTC' }}>
<DateInput {...defaultProps} value={new Date(2022, 3, 11)} onChange={spy} />
</DatesProvider>
);
await userEvent.tab();
await userEvent.clear(getInput(container));
await userEvent.type(getInput(container), 'April 1, 2022');
await userEvent.tab();
expectValue(container, 'April 11, 2022');
expect(spy).toHaveBeenLastCalledWith(new Date(2022, 2, 31, 20));
});

it('clears input when clear button is clicked (uncontrolled)', async () => {
const { container } = render(
<DateInput
Expand Down
Loading

0 comments on commit 18ead2c

Please sign in to comment.