Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiSuperDatePicker] Fix date validation to be locale-aware #7705

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelogs/upcoming/7705.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Bug fixes**

- Fixed `EuiSuperDatePicker` to validate date string with respect of locale on `EuiAbsoluteTab`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docs example is AMAZING. ✨ 😮 Thank you SO much for going the extra mile to document this!!! ❤️‍🔥

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import { GuideSectionTypes } from '../../components';

import {
EuiCallOut,
EuiCode,
EuiCodeBlock,
EuiIcon,
Expand Down Expand Up @@ -37,6 +38,9 @@ const autoRefreshOnlySource = require('!!raw-loader!./auto_refresh_only');
import SuperDatePickerPattern from './super_date_picker_pattern';
const superDatePickerPatternSource = require('!!raw-loader!./super_date_picker_pattern');

import SuperDatePickerLocale from './super_date_picker_locale';
const superDatePickerLocaleSource = require('!!raw-loader!./super_date_picker_locale');

const superDatePickerSnippet = `<EuiSuperDatePicker
onTimeChange={onTimeChange}
start="now-30m"
Expand All @@ -59,6 +63,14 @@ const superDatePickerCustomQuickSelectSnippet = `<EuiSuperDatePicker
/>
`;

const superDatePickerLocaleSnippet = `<EuiSuperDatePicker
start="now-1h"
end="now-15m"
locale="zh-CN"
dateFormat="MMM D, YYYY @ HH:mm:ss.SSS"
onTimeChange={onTimeChange}
/>`;

export const SuperDatePickerExample = {
title: 'Super date picker',
intro: (
Expand Down Expand Up @@ -346,5 +358,38 @@ if (!endMoment || !endMoment.isValid()) {
demo: <SuperDatePickerPattern />,
dempPanelProps: { color: 'subdued' },
},
{
title: 'Locale',
source: [
{
type: GuideSectionTypes.TSX,
code: superDatePickerLocaleSource,
},
],
text: (
<>
<p>
Locale formatting is achieved by using the <EuiCode>locale</EuiCode>
,<EuiCode>timeFormat</EuiCode>, and <EuiCode>dateFormat</EuiCode>{' '}
props. The latter will take any <EuiCode>moment()</EuiCode>{' '}
notation. Check{' '}
<a href="https://en.wikipedia.org/wiki/Date_format_by_country">
Date format by country
</a>{' '}
for formatting examples.
</p>
<EuiCallOut color="warning">
Moment will try to load the locale on demand when it is used.
Bundlers that do not support dynamic require statements will need to
explicitly import the locale, e.g.{' '}
<EuiCode>{"import 'moment/locale/zh-cn'"}</EuiCode>. See the below
demo TSX for examples.
</EuiCallOut>
</>
),
props: { EuiSuperDatePicker },
snippet: superDatePickerLocaleSnippet,
demo: <SuperDatePickerLocale />,
},
],
};
100 changes: 100 additions & 0 deletions src-docs/src/views/super_date_picker/super_date_picker_locale.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState } from 'react';

// NOTE: These explicit imports are required for CodeSandbox and any
// bundler that does not support Moment dynamically loading locales
import 'moment/locale/zh-cn';
import 'moment/locale/ja';
import 'moment/locale/fr';

import {
EuiButtonGroup,
EuiComboBox,
EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiSuperDatePicker,
OnTimeChangeProps,
} from '../../../../src/components';
import { htmlIdGenerator } from '../../../../src/services';

const localeId = htmlIdGenerator('locale');
const locales = [
{ id: localeId(), label: 'en' },
{ id: localeId(), label: 'zh-CN' },
{ id: localeId(), label: 'ja-JP' },
{ id: localeId(), label: 'fr-FR' },
];
const dateFormats = [
{ label: 'MMM D, YYYY @ HH:mm:ss.SSS' },
{ label: 'dddd, MMMM Do YYYY, h:mm:ss a' },
{ label: 'YYYY-MM-DDTHH:mm:ss.SSSZ' },
];

export default () => {
const [start, setStart] = useState('now-1h');
const [end, setEnd] = useState('now-15m');
const onTimeChange = ({ start, end }: OnTimeChangeProps) => {
setStart(start);
setEnd(end);
};

const [locale, setLocale] = useState<string | undefined>();
const [localeSelected, setLocaleSelected] = useState(locales[0].id);
const onLocaleChange = (optionId: React.SetStateAction<string>) => {
setLocale(locales.find(({ id }) => id === optionId)!.label);
setLocaleSelected(optionId);
};

const [dateFormat, setDateFormat] = useState<string | undefined>();
const [dateFormatsSelected, setDateFormatsSelected] = useState([
dateFormats[0],
]);
const onDateFormatChange = (selectedOptions: EuiComboBoxOptionOption[]) => {
setDateFormat(selectedOptions.length ? selectedOptions[0].label : '');
setDateFormatsSelected(selectedOptions);
};
const onDateFormatCreate = (searchValue: string) => {
const normalizedSearchValue = searchValue.trim().toLowerCase();
if (!normalizedSearchValue) return;

setDateFormat(searchValue);
setDateFormatsSelected([{ label: searchValue }]);
};

return (
<>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonGroup
legend={'Locale'}
options={locales}
idSelected={localeSelected}
onChange={onLocaleChange}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiComboBox
prepend="dateFormat"
placeholder="Select a dateFormat"
customOptionText="Add {searchValue} as a dateFormat"
singleSelection={{ asPlainText: true }}
options={dateFormats}
selectedOptions={dateFormatsSelected}
onChange={onDateFormatChange}
onCreateOption={onDateFormatCreate}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiSuperDatePicker
showUpdateButton={false}
start={start}
end={end}
locale={locale}
dateFormat={dateFormat}
onTimeChange={onTimeChange}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { fireEvent } from '@testing-library/react';
import { render } from '../../../../test/rtl';

import { EuiAbsoluteTab } from './absolute_tab';
import { LocaleSpecifier } from 'moment';

// Mock EuiDatePicker - 3rd party datepicker lib causes render issues
jest.mock('../../date_picker', () => ({
Expand Down Expand Up @@ -105,6 +106,27 @@ describe('EuiAbsoluteTab', () => {
expect(input).toHaveValue('Jan 31st 01');
});

describe('parses date string in locale', () => {
test.each<{
locale: LocaleSpecifier;
dateString: string;
}>([
{ locale: 'en', dateString: 'Mon Jan 1st' },
{ locale: 'zh-CN', dateString: '周一 1月 1日' },
{ locale: 'ja-JP', dateString: '月 1月 1日' },
{ locale: 'fr-FR', dateString: 'lun. janv. 1er' },
])('%p', ({ locale, dateString }) => {
const { getByTestSubject } = render(
<EuiAbsoluteTab {...props} dateFormat="ddd MMM Do" locale={locale} />
);
const input = getByTestSubject('superDatePickerAbsoluteDateInput');

changeInput(input, dateString);
expect(input).not.toBeInvalid();
expect(input).toHaveValue(dateString);
});
});

describe('allows several other common date formats, and autoformats them to the `dateFormat` prop', () => {
const assertOutput = (input: HTMLInputElement) => {
// Exclude hours from assertion, because moment uses local machine timezone
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,15 @@ export class EuiAbsoluteTab extends Component<
return this.setState(invalidDateState);
}

const { onChange, dateFormat } = this.props;
const { onChange, dateFormat, locale } = this.props;

// Attempt to parse with passed `dateFormat`
let valueAsMoment = moment(textInputValue, dateFormat, true);
// Attempt to parse with passed `dateFormat` and `locale`
let valueAsMoment = moment(
textInputValue,
dateFormat,
typeof locale === 'string' ? locale : 'en', // Narrow the union type to string
true
);
let dateIsValid = valueAsMoment.isValid();

// If not valid, try a few other other standardized formats
Expand Down
Loading