Skip to content

Commit

Permalink
feat(date, date-range): provides a mechanism that allows consumers to…
Browse files Browse the repository at this point in the history
… override the format of dates

A new property has been added to the locale interface for dates to allow customers to override the
displayed date formats in both the Date and DateRange inputs

fix #6930
  • Loading branch information
damienrobson-sage committed Nov 6, 2024
1 parent 04acd0a commit aed6c43
Show file tree
Hide file tree
Showing 19 changed files with 487 additions and 11 deletions.
8 changes: 5 additions & 3 deletions src/components/date-range/date-range.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export interface DateRangeProps
required?: boolean;
/** Flag to configure component as optional. */
isOptional?: boolean;
/** Date format string to be applied to the date inputs */
dateFormatOverride?: string;
}

export const DateRange = ({
Expand All @@ -141,10 +143,10 @@ export const DateRange = ({
: labelsInline;

const l = useLocale();
const { dateFnsLocale } = l.date;
const { dateFnsLocale, dateFormatOverride } = l.date;
const { format } = useMemo(
() => getFormatData(dateFnsLocale()),
[dateFnsLocale],
() => getFormatData(dateFnsLocale(), dateFormatOverride),
[dateFnsLocale, dateFormatOverride],
);
const inlineLabelWidth = 40;
const [lastChangedDate, setLastChangedDate] = useState("");
Expand Down
9 changes: 8 additions & 1 deletion src/components/date-range/date-range.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import DateRange from "carbon-react/lib/components/date-range";

### LabelsInline

**Note:** The `labelsInline` prop is not supported if the `validationRedesignOptIn` flag on the `CarbonProvider` is true.
**Note:** The `labelsInline` prop is not supported if the `validationRedesignOptIn` flag on the `CarbonProvider` is true.

<Canvas of={DateRangeStories.LabelsInline} />

Expand Down Expand Up @@ -110,6 +110,13 @@ the French locale. Required locales can be imported like so `import { fr } from

<Canvas of={DateRangeStories.LocaleOverrideExampleImplementation} />

### Locale format override

You can also override the format used to display dates in the date picker via the `dateFormatOverride` property. In the example below, the German locale is used
but the date format has been overridden to `y-m-ddd` as opposed to the default `dd.MM.yyyy`.

<Canvas of={DateRangeStories.LocaleFormatOverrideExampleImplementation} />

## Props

### Date Range
Expand Down
48 changes: 47 additions & 1 deletion src/components/date-range/date-range.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Meta, StoryObj } from "@storybook/react";
import { fr } from "date-fns/locale";
import { fr, de } from "date-fns/locale";

import generateStyledSystemProps from "../../../.storybook/utils/styled-system-props";

Expand Down Expand Up @@ -371,3 +371,49 @@ export const IsOptional: Story = () => {
);
};
IsOptional.storyName = "IsOptional";

export const LocaleFormatOverrideExampleImplementation: Story = ({
...args
}) => {
const [state, setState] = useState(["2016-10-01", "2016-10-30"]);
const handleChange = (ev: DateRangeChangeEvent) => {
const newValue = [
ev.target.value[0].formattedValue,
ev.target.value[1].formattedValue,
];
setState(newValue);
};

return (
<div>
<I18nProvider
locale={{
locale: () => "de-DE",
date: {
dateFnsLocale: () => de,
ariaLabels: {
previousMonthButton: () => "Vorheriger Monat",
nextMonthButton: () => "Nächster Monat",
},
dateFormatOverride: args.dateFormatOverride || "dd-MM-yyyy",
},
}}
>
<DateRange
startLabel="Start"
endLabel="End"
value={state}
onChange={handleChange}
/>
</I18nProvider>
</div>
);
};
LocaleFormatOverrideExampleImplementation.storyName =
"Locale Format Override Example Implementation";
LocaleFormatOverrideExampleImplementation.parameters = {
chromatic: { disableSnapshot: true },
};
LocaleFormatOverrideExampleImplementation.args = {
dateFormatOverride: "d-M-yyyy",
};
69 changes: 69 additions & 0 deletions src/components/date-range/date-range.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React from "react";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import enGBLocale from "date-fns/locale/en-GB";
import deLocale from "date-fns/locale/de";

import DateRange, { DateRangeChangeEvent } from "./date-range.component";
import { testStyledSystemMargin } from "../../__spec_helper__/__internal__/test-utils";
import CarbonProvider from "../carbon-provider";
import I18nProvider from "../i18n-provider";

testStyledSystemMargin(
(props) => (
Expand Down Expand Up @@ -1136,3 +1139,69 @@ test("should have the default styling when the `labelsInline` prop is set and `v
expect(screen.getByTestId("start")).toHaveStyle("vertical-align: bottom");
expect(screen.getByTestId("end")).toHaveStyle("vertical-align: bottom");
});

describe("Locale formatting overrides", () => {
test("should render with the input value matching the expected format when `dateFormatOverride` is set and the language is `de-DE`", () => {
render(
<I18nProvider
locale={{
locale: () => "de-DE",
date: {
ariaLabels: {
nextMonthButton: () => "foo",
previousMonthButton: () => "foo",
},
dateFnsLocale: () => deLocale,
dateFormatOverride: "y-m-ddd",
},
}}
>
<DateRange
startLabel="start"
endLabel="end"
value={["2016-10-10", "2016-11-11"]}
onChange={() => {}}
/>
</I18nProvider>,
);

expect(screen.getByRole("textbox", { name: "start" })).toHaveValue(
"2016-0-010",
);
expect(screen.getByRole("textbox", { name: "end" })).toHaveValue(
"2016-0-011",
);
});

test("should render with the input value matching the expected format when `dateFormatOverride` is set and the language is `en-GB`", () => {
render(
<I18nProvider
locale={{
locale: () => "en-GB",
date: {
ariaLabels: {
nextMonthButton: () => "foo",
previousMonthButton: () => "foo",
},
dateFnsLocale: () => enGBLocale,
dateFormatOverride: "y-m-ddd",
},
}}
>
<DateRange
startLabel="start"
endLabel="end"
value={["2016-10-10", "2016-11-11"]}
onChange={() => {}}
/>
</I18nProvider>,
);

expect(screen.getByRole("textbox", { name: "start" })).toHaveValue(
"2016-0-010",
);
expect(screen.getByRole("textbox", { name: "end" })).toHaveValue(
"2016-0-011",
);
});
});
30 changes: 30 additions & 0 deletions src/components/date/__internal__/date-formats/date-formats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,36 @@ test("should default to en-GB locale if no locale code string passed to `getForm
expect(format).toEqual(formatMap["en-GB"]);
});

describe("dateFormatOverride tests", () => {
const localeCodes = [
// handled locales
"en-CA",
"en-US",
"en-ZA",
"fr-CA",
"ar-EG",
// random locale to cover default switch scenario
"zh-HK",
];
const dateFormatOverride = "dd Mo yyyy";

test.each(localeCodes)(
"should support %s locale code string passed to `getFormatData` when dateFormatOverride is provided",
(code: string) => {
const { formats, format } = getFormatData({ code }, dateFormatOverride);

const expectedFormats = getExpectedFormatForLocale(code);

expect(
expectedFormats.every((formatStr) => formats.includes(formatStr)) &&
formats.length === expectedFormats.length,
).toEqual(true);

expect(format).toEqual(dateFormatOverride);
},
);
});

describe.each(euLocales)(
"when EU locales are passed to `getFormatData`",
(locale: string) => {
Expand Down
31 changes: 30 additions & 1 deletion src/components/date/__internal__/date-formats/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,36 @@ interface LocaleFormats {
format: string;
}

const getFormatData = ({ code = "en-GB" }: DateFnsLocale): LocaleFormats => {
const getFormatData = (
{ code = "en-GB" }: DateFnsLocale,
dateFormatOverride?: string,
): LocaleFormats => {
if (dateFormatOverride) {
const { format } = getOutputFormatForLocale(code);
let formatFromLocale;

switch (code) {
case "en-CA":
case "en-US":
formatFromLocale = "MM/dd/yyyy";
break;
case "ar-EG":
case "en-ZA":
case "fr-CA":
formatFromLocale = "dd/MM/yyyy";
break;
default:
formatFromLocale = format;
}

const formatsForLocale = getInputFormatsArrayForLocale(formatFromLocale);

return {
format: dateFormatOverride,
formats: generateFormats(formatsForLocale, "/"),
};
}

if (["en-CA", "en-US"].includes(code)) {
const format = "MM/dd/yyyy";
const formats = getInputFormatsArrayForLocale(format);
Expand Down
56 changes: 55 additions & 1 deletion src/components/date/date-test.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useState } from "react";
import { action } from "@storybook/addon-actions";

import { StoryObj } from "@storybook/react";
import deLocale from "date-fns/locale/de";

import DateInput, { DateChangeEvent } from "./date.component";
import {
CommonTextboxArgs,
Expand All @@ -12,6 +13,7 @@ import {
import CarbonProvider from "../carbon-provider/carbon-provider.component";
import Box from "../box";
import Confirm from "../confirm";
import I18nProvider from "../i18n-provider";

export default {
title: "Date Input/Test",
Expand Down Expand Up @@ -148,3 +150,55 @@ export const MultipleDates: StoryObj<typeof DateInput> = () => {
MultipleDates.storyName =
"Multiple Dates with onPickerOpen and onPickerClose callbacks";
MultipleDates.parameters = { chromatic: { disableSnapshot: true } };

interface I18nArgs extends CommonTextboxArgs {
/** The format used to override te displayed date format */
dateFormatOverride?: string;
}

export const I18NStory = (args: I18nArgs) => {
const [state, setState] = useState("2019-04-05");
const setValue = (ev: DateChangeEvent) => {
action("onChange")(ev.target.value);
setState(ev.target.value.formattedValue);
};
return (
<CarbonProvider validationRedesignOptIn>
<I18nProvider
locale={{
locale: () => "de-DE",
date: {
ariaLabels: {
nextMonthButton: () => "foo",
previousMonthButton: () => "foo",
},
dateFnsLocale: () => deLocale,
dateFormatOverride: args.dateFormatOverride || "dd/MM/yyyy",
},
}}
>
<DateInput
name="dateinput"
m={2}
value={state}
onChange={setValue}
onBlur={(ev) => {
action("onBlur")(ev.target.value);
}}
onKeyDown={(ev) =>
action("onKeyDown")((ev.target as HTMLInputElement).value)
}
onClick={(ev) =>
action("onClick")((ev.target as HTMLInputElement).value)
}
{...getCommonTextboxArgsWithSpecialCaracters(args)}
/>
</I18nProvider>
</CarbonProvider>
);
};
I18NStory.storyName = "i18n Story";
I18NStory.args = {
dateFormatOverride: "dd/MM/yyyy",
...getCommonTextboxArgs(),
};
8 changes: 5 additions & 3 deletions src/components/date/date.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export interface DateInputProps
onPickerOpen?: () => void;
/** Callback triggered when the picker is closed */
onPickerClose?: () => void;
/** Date format string to be applied to the date inputs */
dateFormatOverride?: string;
}

export const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
Expand Down Expand Up @@ -146,10 +148,10 @@ export const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
const focusedViaPicker = useRef(false);
const blockClose = useRef(false);
const locale = useLocale();
const { dateFnsLocale } = locale.date;
const { dateFnsLocale, dateFormatOverride } = locale.date;
const { format, formats } = useMemo(
() => getFormatData(dateFnsLocale()),
[dateFnsLocale],
() => getFormatData(dateFnsLocale(), dateFormatOverride),
[dateFnsLocale, dateFormatOverride],
);
const { inputRefMap, setInputRefMap } = useContext(DateRangeContext);
const [open, setOpen] = useState(false);
Expand Down
9 changes: 8 additions & 1 deletion src/components/date/date.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ be used to set the input value like in the example below, although the component

### With labelInline

**Note:** The `labelInline` prop is not supported if the `validationRedesignOptIn` flag on the `CarbonProvider` is true.
**Note:** The `labelInline` prop is not supported if the `validationRedesignOptIn` flag on the `CarbonProvider` is true.

<Canvas of={DateStories.WithLabelInline} />

Expand Down Expand Up @@ -149,6 +149,13 @@ the German (`de`) locale whilst the second is for a Chinese (`zh-CN`) locale. Re

<Canvas of={DateStories.LocaleOverrideExampleImplementation} />

### Locale format override

You can also override the format used to display dates in the date picker via the `dateFormatOverride` property. In the example below, the German locale is used
but the date format has been overridden to `yyyy-M-d` as opposed to the default `dd.MM.yyyy`.

<Canvas of={DateStories.LocaleFormatOverrideExampleImplementation} />

## Props

### DateInput
Expand Down
Loading

0 comments on commit aed6c43

Please sign in to comment.