From 3fec66b9c0c91c1d6a4c92ac6d7432a8aca8ccf3 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:04:55 -0700 Subject: [PATCH] [EuiSuperDatePicker] Convert date popover styles to Emotion (#7908) --- packages/eui/changelogs/upcoming/7908.md | 3 + .../date_picker/super_date_picker/_index.scss | 1 - .../date_popover/_absolute_tab.scss | 19 - .../date_popover/_date_popover_content.scss | 17 - .../date_popover/_index.scss | 2 - .../date_popover/absolute_tab.styles.ts | 44 +++ .../date_popover/absolute_tab.tsx | 356 ++++++++---------- .../date_popover_content.styles.ts | 31 ++ .../date_popover/date_popover_content.tsx | 24 +- .../date_popover/relative_tab.tsx | 322 +++++++--------- 10 files changed, 400 insertions(+), 419 deletions(-) create mode 100644 packages/eui/changelogs/upcoming/7908.md delete mode 100644 packages/eui/src/components/date_picker/super_date_picker/date_popover/_absolute_tab.scss delete mode 100644 packages/eui/src/components/date_picker/super_date_picker/date_popover/_date_popover_content.scss delete mode 100644 packages/eui/src/components/date_picker/super_date_picker/date_popover/_index.scss create mode 100644 packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.styles.ts create mode 100644 packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.styles.ts diff --git a/packages/eui/changelogs/upcoming/7908.md b/packages/eui/changelogs/upcoming/7908.md new file mode 100644 index 00000000000..1582043ba5a --- /dev/null +++ b/packages/eui/changelogs/upcoming/7908.md @@ -0,0 +1,3 @@ +**CSS-in-JS conversions** + +- Converted `EuiSuperDatePicker`'s date popover content to Emotion diff --git a/packages/eui/src/components/date_picker/super_date_picker/_index.scss b/packages/eui/src/components/date_picker/super_date_picker/_index.scss index 6ba70ece893..06a7ba3c66d 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/_index.scss +++ b/packages/eui/src/components/date_picker/super_date_picker/_index.scss @@ -1,2 +1 @@ -@import 'date_popover/index'; @import 'quick_select_popover/index'; diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/_absolute_tab.scss b/packages/eui/src/components/date_picker/super_date_picker/date_popover/_absolute_tab.scss deleted file mode 100644 index 9e86650d7e2..00000000000 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/_absolute_tab.scss +++ /dev/null @@ -1,19 +0,0 @@ -.euiSuperDatePicker__absoluteDateForm { - padding: 0 $euiSizeS $euiSizeS; -} - -.euiSuperDatePicker__absoluteDateFormSubmit { - flex-shrink: 0; -} - -.euiSuperDatePicker__absoluteDateFormRow { - flex-grow: 1; - - // CSS hack to make the help/error text extend to the submit button. - // We can't actually put the submit button within an EuiFormRow due to - // cloneElement limitations (https://github.com/elastic/eui/issues/2493#issuecomment-561278494) - // TODO: Remove this and clean up DOM rendering once we can - .euiFormRow__text { - margin-inline-end: -1 * ($euiSizeXL + $euiSizeS); // XL - size of the button, S - size of the flex gap - } -} diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/_date_popover_content.scss b/packages/eui/src/components/date_picker/super_date_picker/date_popover/_date_popover_content.scss deleted file mode 100644 index 0182e258066..00000000000 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/_date_popover_content.scss +++ /dev/null @@ -1,17 +0,0 @@ -.euiDatePopoverContent, -.euiDatePopoverContent .react-datepicker { - width: $euiFormMaxWidth; - max-width: 100%; - - @include euiBreakpoint('xs') { - width: $euiDatePickerCalendarWidth; - } -} - -.euiDatePopoverContent__padded { - padding: $euiSizeS; -} - -.euiDatePopoverContent__padded--large { - padding: $euiSize; -} diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/_index.scss b/packages/eui/src/components/date_picker/super_date_picker/date_popover/_index.scss deleted file mode 100644 index b6086ab8484..00000000000 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'absolute_tab'; -@import 'date_popover_content'; diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.styles.ts b/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.styles.ts new file mode 100644 index 00000000000..a0530f4c23e --- /dev/null +++ b/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.styles.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/react'; + +import { UseEuiTheme } from '../../../../services'; +import { logicalCSS, mathWithUnits } from '../../../../global_styling'; + +export const euiAbsoluteTabDateFormStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + + return { + euiAbsoluteTabDateForm: css` + ${logicalCSS('padding-horizontal', euiTheme.size.s)} + ${logicalCSS('padding-bottom', euiTheme.size.s)} + `, + euiAbsoluteTabDateForm__submit: css` + flex-shrink: 0; + `, + euiAbsoluteTabDateForm__row: css` + flex-grow: 1; + + /* CSS hack to make the help/error text extend to the submit button. + * We can't actually put the submit button within an EuiFormRow due to + * cloneElement limitations (https://github.com/elastic/eui/issues/2493#issuecomment-561278494) + * TODO: Remove this and clean up DOM rendering once we can + */ + .euiFormRow__text { + ${logicalCSS( + 'margin-right', + mathWithUnits( + [euiTheme.size.xl, euiTheme.size.s], + (submitButtonSize, gapSize) => -1 * (submitButtonSize + gapSize) + ) + )} + } + `, + }; +}; diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx b/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx index 69035c0d22c..76b4b2717e2 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx @@ -6,20 +6,27 @@ * 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, useEuiMemoizedStyles } 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'; +import { euiAbsoluteTabDateFormStyles } from './absolute_tab.styles'; // Allow users to paste in and have the datepicker parse multiple common date formats, // in addition to the configured displayed `dateFormat` prop @@ -36,198 +43,165 @@ 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) => { - 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); - } - - 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(); +export const EuiAbsoluteTab: FunctionComponent = ({ + value, + onChange, + dateFormat, + timeFormat, + locale, + roundUp, + utcOffset, + labelPrefix, +}) => { + const styles = useEuiMemoizedStyles(euiAbsoluteTabDateFormStyles); + + const [valueAsMoment, setValueAsMoment] = useState(() => { + 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 submitButtonLabel = useEuiI18n( + 'euiAbsoluteTab.dateFormatButtonLabel', + 'Parse date' + ); + const dateFormatError = useEuiI18n( + 'euiAbsoluteTab.dateFormatError', + 'Allowed formats: {dateFormat}, ISO 8601, RFC 2822, or Unix timestamp.', + { dateFormat: {dateFormat} } + ); + const [textInputValue, setTextInputValue] = useState(() => + valueAsMoment!.locale(locale || 'en').format(dateFormat) + ); + const [hasUnparsedText, setHasUnparsedText] = useState(false); + const [isReadyToParse, setIsReadyToParse] = useState(false); + const [isTextInvalid, setIsTextInvalid] = useState(false); + + const handleTextChange = useCallback( + (event: ChangeEvent) => { + if (isReadyToParse) return; // Text paste event, don't continue + + setTextInputValue(event.target.value); + setHasUnparsedText(true); + setIsTextInvalid(false); + }, + [isReadyToParse] + ); + + 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 standardized formats + 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]); - 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 ( - <> - - {dateFormat} }} + }, [valueAsMoment]); + + return ( + <> + + { + e.preventDefault(); // Prevents a page refresh/reload + setIsReadyToParse(true); + }} + css={styles.euiAbsoluteTabDateForm} + gutterSize="s" + responsive={false} + > + - {([dateFormatButtonLabel, dateFormatError]: string[]) => ( - { - e.preventDefault(); // Prevents a page refresh/reload - this.parseUserDateInput(textInputValue); - }} - className="euiSuperDatePicker__absoluteDateForm" - gutterSize="s" - responsive={false} - > - - { - this.parseUserDateInput( - event.clipboardData.getData('text') - ); - }} - data-test-subj="superDatePickerAbsoluteDateInput" - prepend={{labelPrefix}} - /> - - {hasUnparsedText && ( - - )} - - )} - - - ); - } -} + { + setTextInputValue(event.clipboardData.getData('text')); + setIsReadyToParse(true); + }} + data-test-subj="superDatePickerAbsoluteDateInput" + prepend={{labelPrefix}} + /> + + {hasUnparsedText && ( + + )} + + + ); +}; diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.styles.ts b/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.styles.ts new file mode 100644 index 00000000000..9a7f4c3f45f --- /dev/null +++ b/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.styles.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/react'; + +import { UseEuiTheme } from '../../../../services'; +import { euiMaxBreakpoint, logicalCSS } from '../../../../global_styling'; +import { euiFormVariables } from '../../../form/form.styles'; + +export const euiDatePopoverContentStyles = (euiThemeContext: UseEuiTheme) => { + const { maxWidth } = euiFormVariables(euiThemeContext); + + return { + euiDatePopoverContent: css` + &, + & .react-datepicker { + ${logicalCSS('width', maxWidth)} + ${logicalCSS('max-width', '100%')} + + ${euiMaxBreakpoint(euiThemeContext, 's')} { + ${logicalCSS('width', '284px')}/* TODO: $euiDatePickerCalendarWidth */ + } + } + `, + }; +}; diff --git a/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx b/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx index 27f5df2eae8..cf62ac294ae 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx @@ -7,15 +7,15 @@ */ import React, { FunctionComponent } from 'react'; +import { LocaleSpecifier } from 'moment'; +import { useEuiMemoizedStyles } from '../../../../services'; +import { useEuiPaddingCSS } from '../../../../global_styling'; import { EuiI18n, useEuiI18n } from '../../../i18n'; import { EuiTabbedContent, EuiTabbedContentProps } from '../../../tabs'; import { EuiText } from '../../../text'; import { EuiButton } from '../../../button'; -import { EuiAbsoluteTab } from './absolute_tab'; -import { EuiRelativeTab } from './relative_tab'; - import { TimeOptions } from '../time_options'; import { getDateMode, @@ -23,7 +23,9 @@ import { toAbsoluteString, toRelativeString, } from '../date_modes'; -import { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named +import { EuiAbsoluteTab } from './absolute_tab'; +import { EuiRelativeTab } from './relative_tab'; +import { euiDatePopoverContentStyles } from './date_popover_content.styles'; export interface EuiDatePopoverContentProps { value: string; @@ -52,6 +54,8 @@ export const EuiDatePopoverContent: FunctionComponent< utcOffset, timeOptions, }) => { + const styles = useEuiMemoizedStyles(euiDatePopoverContentStyles); + const onTabClick: EuiTabbedContentProps['onTabClick'] = (selectedTab) => { switch (selectedTab.id) { case DATE_MODES.ABSOLUTE: @@ -95,7 +99,6 @@ export const EuiDatePopoverContent: FunctionComponent< value={value} onChange={onChange} roundUp={roundUp} - position={position} labelPrefix={labelPrefix} utcOffset={utcOffset} /> @@ -115,7 +118,6 @@ export const EuiDatePopoverContent: FunctionComponent< } onChange={onChange} roundUp={roundUp} - position={position} labelPrefix={labelPrefix} timeOptions={timeOptions} /> @@ -127,16 +129,11 @@ export const EuiDatePopoverContent: FunctionComponent< id: DATE_MODES.NOW, name: nowLabel, content: ( - +

{ - count: number | undefined; -} +export const EuiRelativeTab: FunctionComponent = ({ + timeOptions: { relativeOptions, relativeRoundingLabels }, + dateFormat, + locale, + value, + onChange, + roundUp, + labelPrefix, +}) => { + const initialRelativeParts = useRef(parseRelativeParts(value)); + const { roundUnit } = initialRelativeParts.current; -export class EuiRelativeTab extends Component< - EuiRelativeTabProps, - EuiRelativeTabState -> { - state: EuiRelativeTabState = { - ...parseRelativeParts(this.props.value), - }; + const [unit, setUnit] = useState( + initialRelativeParts.current.unit + ); + const onUnitChange = useCallback((event: ChangeEvent) => { + setUnit(event.target.value); + }, []); - relativeDateInputNumberDescriptionId = htmlIdGenerator()(); + const [round, setRound] = useState( + initialRelativeParts.current.round + ); + const onRoundChange = useCallback((event: EuiSwitchEvent) => { + setRound(event.target.checked); + }, []); - onCountChange: ChangeEventHandler = (event) => { + const [count, setCount] = useState( + initialRelativeParts.current.count + ); + const onCountChange = useCallback((event: ChangeEvent) => { const sanitizedValue = parseInt(event.target.value, 10); - this.setState( - { - count: isNaN(sanitizedValue) ? undefined : sanitizedValue, - }, - this.handleChange - ); - }; - - onUnitChange: ChangeEventHandler = (event) => { - this.setState( - { - unit: event.target.value, - }, - this.handleChange - ); - }; - - onRoundChange = (event: EuiSwitchEvent) => { - this.setState( - { - round: event.target.checked, - }, - this.handleChange - ); - }; - - handleChange = () => { - const { count, round, roundUnit, unit } = this.state; - const { onChange } = this.props; - if (count === undefined || count < 0) { - return; - } + const count = isNaN(sanitizedValue) ? undefined : sanitizedValue; + setCount(count); + }, []); + + useUpdateEffect(() => { + if (count === undefined || count < 0) return; + const date = toRelativeStringFromParts({ count, - round, + round: !!round, roundUnit, unit, }); onChange(date); - }; + }, [onChange, count, round, roundUnit, unit]); - render() { - const { relativeOptions, relativeRoundingLabels } = this.props.timeOptions; - const { count, unit } = this.state; - const invalidDate = this.props.value === INVALID_DATE; - const invalidValue = count === undefined || count < 0; - const isInvalid = invalidValue || invalidDate; + const invalidDate = value === INVALID_DATE; + const invalidValue = count === undefined || count < 0; + const isInvalid = invalidValue || invalidDate; - const parsedValue = dateMath.parse(this.props.value, { - roundUp: this.props.roundUp, - }); + const formattedValue = useMemo(() => { + if (isInvalid) return ''; - const formattedValue = - isInvalid || !parsedValue || !parsedValue.isValid() - ? '' - : parsedValue - .locale(this.props.locale || 'en') - .format(this.props.dateFormat); - - const getErrorMessage = ({ - numberInputError, - dateInputError, - }: { - numberInputError: string; - dateInputError: string; - }) => { - if (invalidValue) return numberInputError; - if (invalidDate) return dateInputError; - return null; - }; - - return ( - <> - - - - = 0', - 'Time span amount', - 'Must be a valid range', - ]} - > - {([ - numberInputError, - numberInputLabel, - dateInputError, - ]: string[]) => ( - - - - )} - - - - - {(unitInputLabel: string) => ( - - )} - - - - - {this.props.labelPrefix}} - /> - -

- = 0' + ); + const dateInputError = useEuiI18n( + 'euiRelativeTab.dateInputError', + 'Must be a valid range' + ); + const unitSelectAriaLabel = useEuiI18n( + 'euiRelativeTab.unitInputLabel', + 'Relative time span' + ); + + return ( + <> + + + + + -

-
-
- - - - - ); - } -} + + + + + + + + {labelPrefix}} + /> + +

+ +

+
+ + + + + + ); +};