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] Convert date popover styles to Emotion #7908

Merged
merged 9 commits into from
Jul 25, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@
* Side Public License, v 1.
*/

import React, { Component, ChangeEvent, FormEvent } from 'react';

import moment, { Moment, LocaleSpecifier } from 'moment'; // eslint-disable-line import/named

import React, {
FunctionComponent,
ChangeEvent,
FormEvent,
useState,
useEffect,
useCallback,
} from 'react';
import moment, { Moment, LocaleSpecifier } from 'moment';
import dateMath from '@elastic/datemath';

import { useUpdateEffect } from '../../../../services';
import { useEuiI18n } from '../../../i18n';
import { EuiFormRow, EuiFieldText, EuiFormLabel } from '../../../form';
import { EuiFlexGroup } from '../../../flex';
import { EuiButtonIcon } from '../../../button';
import { EuiCode } from '../../../code';
import { EuiI18n } from '../../../i18n';

import { EuiDatePicker, EuiDatePickerProps } from '../../date_picker';
import { EuiDatePopoverContentProps } from './date_popover_content';
Expand All @@ -36,198 +42,161 @@ export interface EuiAbsoluteTabProps {
value: string;
onChange: EuiDatePopoverContentProps['onChange'];
roundUp: boolean;
position: 'start' | 'end';
labelPrefix: string;
utcOffset?: number;
}

interface EuiAbsoluteTabState {
hasUnparsedText: boolean;
isTextInvalid: boolean;
textInputValue: string;
valueAsMoment: Moment | null;
}

export class EuiAbsoluteTab extends Component<
EuiAbsoluteTabProps,
EuiAbsoluteTabState
> {
state: EuiAbsoluteTabState;
isParsing = false; // Store outside of state as a ref for faster/unbatched updates

constructor(props: EuiAbsoluteTabProps) {
super(props);

const parsedValue = dateMath.parse(props.value, { roundUp: props.roundUp });
const valueAsMoment =
parsedValue && parsedValue.isValid() ? parsedValue : moment();

const textInputValue = valueAsMoment
.locale(this.props.locale || 'en')
.format(this.props.dateFormat);

this.state = {
hasUnparsedText: false,
isTextInvalid: false,
textInputValue,
valueAsMoment,
};
}

handleChange: EuiDatePickerProps['onChange'] = (date) => {
const { onChange } = this.props;
if (date === null) {
return;
}
onChange(date.toISOString());

const valueAsMoment = moment(date);
this.setState({
valueAsMoment,
textInputValue: valueAsMoment.format(this.props.dateFormat),
hasUnparsedText: false,
isTextInvalid: false,
});
};

handleTextChange = (event: ChangeEvent<HTMLInputElement>) => {
if (this.isParsing) return;

this.setState({
textInputValue: event.target.value,
hasUnparsedText: true,
isTextInvalid: false,
});
};

parseUserDateInput = (textInputValue: string) => {
this.isParsing = true;
// Wait a tick for state to finish updating (whatever gets returned),
// and then allow `onChange` user input to continue setting state
requestAnimationFrame(() => {
this.isParsing = false;
});

const invalidDateState = {
textInputValue,
isTextInvalid: true,
valueAsMoment: null,
};
if (!textInputValue) {
return this.setState(invalidDateState);
export const EuiAbsoluteTab: FunctionComponent<EuiAbsoluteTabProps> = ({
value,
onChange,
dateFormat,
timeFormat,
locale,
roundUp,
utcOffset,
labelPrefix,
}) => {
const [valueAsMoment, setValueAsMoment] = useState<Moment | null>(() => {
const parsedValue = dateMath.parse(value, { roundUp });
return parsedValue && parsedValue.isValid() ? parsedValue : moment();
});
const handleChange: EuiDatePickerProps['onChange'] = useCallback(
(date: Moment | null) => {
if (date === null) return;

const valueAsMoment = moment(date);
setValueAsMoment(valueAsMoment);
setTextInputValue(valueAsMoment.format(dateFormat));
setHasUnparsedText(false);
setIsTextInvalid(false);
},
[dateFormat]
);

const [textInputValue, setTextInputValue] = useState<string>(() =>
valueAsMoment!.locale(locale || 'en').format(dateFormat)
);
const handleTextChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setTextInputValue(event.target.value);
setHasUnparsedText(true);
setIsTextInvalid(false);
},
[]
);

const submitButtonLabel = useEuiI18n(
'euiAbsoluteTab.dateFormatButtonLabel',
'Parse date'
);
const dateFormatError = useEuiI18n(
'euiAbsoluteTab.dateFormatError',
'Allowed formats: {dateFormat}, ISO 8601, RFC 2822, or Unix timestamp.',
{ dateFormat: <EuiCode>{dateFormat}</EuiCode> }
);
const [hasUnparsedText, setHasUnparsedText] = useState(false);
const [isReadyToParse, setIsReadyToParse] = useState(false);
const [isTextInvalid, setIsTextInvalid] = useState(false);

useEffect(() => {
if (isReadyToParse) {
if (!textInputValue) {
setIsTextInvalid(true);
setValueAsMoment(null);
return;
}

// 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
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
if (!dateIsValid) {
valueAsMoment = moment(textInputValue, ALLOWED_USER_DATE_FORMATS, true);
dateIsValid = valueAsMoment.isValid();
}

if (dateIsValid) {
setTextInputValue(valueAsMoment.format(dateFormat));
setValueAsMoment(valueAsMoment);
setHasUnparsedText(false);
setIsTextInvalid(false);
} else {
setIsTextInvalid(true);
setValueAsMoment(null);
}
setIsReadyToParse(false);
}
}, [isReadyToParse, textInputValue, dateFormat, locale]);

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

// 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
if (!dateIsValid) {
valueAsMoment = moment(textInputValue, ALLOWED_USER_DATE_FORMATS, true);
dateIsValid = valueAsMoment.isValid();
}

if (dateIsValid) {
useUpdateEffect(() => {
if (valueAsMoment) {
onChange(valueAsMoment.toISOString());
this.setState({
textInputValue: valueAsMoment.format(this.props.dateFormat),
valueAsMoment: valueAsMoment,
hasUnparsedText: false,
isTextInvalid: false,
});
} else {
this.setState(invalidDateState);
}
};

render() {
const { dateFormat, timeFormat, locale, utcOffset, labelPrefix } =
this.props;
const { valueAsMoment, isTextInvalid, hasUnparsedText, textInputValue } =
this.state;

return (
<>
<EuiDatePicker
inline
showTimeSelect
shadow={false}
selected={valueAsMoment}
onChange={this.handleChange}
dateFormat={dateFormat}
timeFormat={timeFormat}
locale={locale}
utcOffset={utcOffset}
/>
<EuiI18n
tokens={[
'euiAbsoluteTab.dateFormatButtonLabel',
'euiAbsoluteTab.dateFormatError',
]}
defaults={[
'Parse date',
'Allowed formats: {dateFormat}, ISO 8601, RFC 2822, or Unix timestamp.',
]}
values={{ dateFormat: <EuiCode>{dateFormat}</EuiCode> }}
}, [valueAsMoment]);

return (
<>
<EuiDatePicker
inline
showTimeSelect
shadow={false}
selected={valueAsMoment}
onChange={handleChange}
dateFormat={dateFormat}
timeFormat={timeFormat}
locale={locale}
utcOffset={utcOffset}
/>
<EuiFlexGroup
component="form"
onSubmit={(e: FormEvent) => {
e.preventDefault(); // Prevents a page refresh/reload
setIsReadyToParse(true);
}}
className="euiSuperDatePicker__absoluteDateForm"
gutterSize="s"
responsive={false}
>
<EuiFormRow
className="euiSuperDatePicker__absoluteDateFormRow"
isInvalid={isTextInvalid}
error={isTextInvalid ? dateFormatError : undefined}
helpText={
hasUnparsedText && !isTextInvalid ? dateFormatError : undefined
}
>
{([dateFormatButtonLabel, dateFormatError]: string[]) => (
<EuiFlexGroup
component="form"
onSubmit={(e: FormEvent) => {
e.preventDefault(); // Prevents a page refresh/reload
this.parseUserDateInput(textInputValue);
}}
className="euiSuperDatePicker__absoluteDateForm"
gutterSize="s"
responsive={false}
>
<EuiFormRow
className="euiSuperDatePicker__absoluteDateFormRow"
isInvalid={isTextInvalid}
error={isTextInvalid ? dateFormatError : undefined}
helpText={
hasUnparsedText && !isTextInvalid
? dateFormatError
: undefined
}
>
<EuiFieldText
compressed
isInvalid={isTextInvalid}
value={textInputValue}
onChange={this.handleTextChange}
onPaste={(event) => {
this.parseUserDateInput(
event.clipboardData.getData('text')
);
}}
data-test-subj="superDatePickerAbsoluteDateInput"
prepend={<EuiFormLabel>{labelPrefix}</EuiFormLabel>}
/>
</EuiFormRow>
{hasUnparsedText && (
<EuiButtonIcon
type="submit"
className="euiSuperDatePicker__absoluteDateFormSubmit"
size="s"
display="base"
iconType="check"
aria-label={dateFormatButtonLabel}
title={dateFormatButtonLabel}
data-test-subj="parseAbsoluteDateFormat"
/>
)}
</EuiFlexGroup>
)}
</EuiI18n>
</>
);
}
}
<EuiFieldText
compressed
isInvalid={isTextInvalid}
value={textInputValue}
onChange={handleTextChange}
onPaste={(event) => {
setTextInputValue(event.clipboardData.getData('text'));
setIsReadyToParse(true);
}}
data-test-subj="superDatePickerAbsoluteDateInput"
prepend={<EuiFormLabel>{labelPrefix}</EuiFormLabel>}
/>
</EuiFormRow>
{hasUnparsedText && (
<EuiButtonIcon
type="submit"
className="euiSuperDatePicker__absoluteDateFormSubmit"
size="s"
display="base"
iconType="check"
aria-label={submitButtonLabel}
title={submitButtonLabel}
data-test-subj="parseAbsoluteDateFormat"
/>
)}
</EuiFlexGroup>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export const EuiDatePopoverContent: FunctionComponent<
value={value}
onChange={onChange}
roundUp={roundUp}
position={position}
labelPrefix={labelPrefix}
utcOffset={utcOffset}
/>
Expand Down