diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png index 47293efefe6..3eca2fad4b8 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png index 47293efefe6..3eca2fad4b8 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png index 80ae8cc1f26..4fbe142a20c 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Custom_Quick_Select_Panel.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png index 80ae8cc1f26..4fbe142a20c 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperDatePicker_EuiSuperDatePicker_Playground.png differ diff --git a/packages/eui/changelogs/upcoming/7904.md b/packages/eui/changelogs/upcoming/7904.md new file mode 100644 index 00000000000..eabbbabb3d6 --- /dev/null +++ b/packages/eui/changelogs/upcoming/7904.md @@ -0,0 +1,8 @@ +**CSS-in-JS conversions** + +- Converted `EuiSuperDatePicker`'s form control to Emotion; + - Removed `$euiSuperDatePickerWidth` + - Removed `$euiSuperDatePickerButtonWidth` + - Removed `$euiSuperDatePickerNeedsUpdatingBackgroundColor` + - Removed `$euiSuperDatePickerNeedsUpdatingTextColor` + - Removed `@euiSuperDatePickerText` mixin diff --git a/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap b/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap index 15009f90921..1abdff6c019 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap +++ b/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap @@ -2,11 +2,11 @@ exports[`EuiSuperDatePicker props accepts data-test-subj and passes to EuiFormControlLayout 1`] = `
diff --git a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.styles.ts b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.styles.ts new file mode 100644 index 00000000000..e323f53f6e6 --- /dev/null +++ b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.styles.ts @@ -0,0 +1,214 @@ +/* + * 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, + tint, + shade, + makeHighContrastColor, +} from '../../../services'; +import { + euiFontSize, + euiMaxBreakpoint, + logicalCSS, + mathWithUnits, +} from '../../../global_styling'; +import { + euiFormVariables, + euiFormControlDefaultShadow, + euiFormControlInvalidStyles, + euiFormControlDisabledStyles, +} from '../../form/form.styles'; + +export const euiSuperDatePickerStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme, colorMode } = euiThemeContext; + const forms = euiFormVariables(euiThemeContext); + + const inputWidth = euiTheme.base * 30; + const buttonWidth = euiTheme.base * 7; // @see _button_display.styles.ts + const gap = euiTheme.size.s; + + // Default restricted width + const restrictedWidth = mathWithUnits( + gap, + (gap) => inputWidth + gap + buttonWidth + ); + + // Set a sensible min-width for when width is auto + const minFormWidth = parseFloat(forms.maxWidth) / 2; + const autoMinWidth = mathWithUnits( + gap, + (gap) => minFormWidth + gap + buttonWidth + ); + + // Needs updating colors + const needsUpdatingBackgroundColor = + colorMode === 'DARK' + ? shade(euiTheme.colors.success, 0.7) + : tint(euiTheme.colors.success, 0.9); + const needsUpdatingTextColor = makeHighContrastColor(euiTheme.colors.success)( + needsUpdatingBackgroundColor + ); + + return { + euiSuperDatePicker: css` + display: flex; + gap: ${gap}; + ${logicalCSS('max-width', '100%')} + + ${euiMaxBreakpoint(euiThemeContext, 'm')} { + ${logicalCSS('width', '100%')} + } + + /* Fix border radius clipping, but only if the auto refresh append item isn't rendered */ + .euiFormControlLayout__childrenWrapper:last-child { + ${logicalCSS('border-top-right-radius', 'inherit')} + ${logicalCSS('border-bottom-right-radius', 'inherit')} + } + `, + + widths: { + restricted: css` + ${logicalCSS('width', restrictedWidth)} + `, + full: css` + ${logicalCSS('width', '100%')} + `, + auto: css` + display: inline-flex; + ${logicalCSS('min-width', `min(${autoMinWidth}, 100%)`)} + ${logicalCSS('width', 'auto')} + `, + }, + + // Special rendering cases that override all permutations of the above widths + noUpdateButton: { + // Skipping css`` and using the `label` key instead to reduce repeat Emotion generated classNames + restricted: ` + label: noUpdateButton; + ${logicalCSS('width', `${inputWidth}px`)}; + `, + auto: ` + label: noUpdateButton; + ${logicalCSS('min-width', `min(${minFormWidth}px, 100%)`)}; + `, + full: ` + label: noUpdateButton; + `, + }, + isAutoRefreshOnly: { + // display: block over flex is required to have the nested .euiPopover wrap expand to the wrapper width + restricted: ` + label: isAutoRefreshOnly; + display: block; + ${logicalCSS('width', forms.maxWidth)} + `, + auto: ` + label: isAutoRefreshOnly; + `, + full: ` + label: isAutoRefreshOnly; + display: block; + `, + }, + // isQuickSelectOnly forces `width` to be `auto` + isQuickSelectOnly: css` + ${logicalCSS('min-width', 0)} + `, + + euiSuperDatePicker__range: css` + flex-grow: 1; + `, + euiSuperDatePicker__rangeInput: css` + flex-grow: 1; + /* Needs !important to override EuiFormControlLayoutDelimited's fullWidth CSS */ + ${logicalCSS('width', 'auto !important')} + `, + euiSuperDatePicker__prettyFormat: css` + ${_buttonStyles(euiThemeContext)} + `, + + // Form states + states: { + euiSuperDatePicker__formControlLayout: css` + .euiFormControlLayout__childrenWrapper { + ${euiFormControlDefaultShadow(euiThemeContext)} + box-shadow: none; + } + `, + default: css` + .euiFormControlLayout__childrenWrapper { + color: ${forms.textColor}; + background-color: ${forms.backgroundColor}; + } + + /* Focus/selection underline per-button */ + .euiDatePopoverButton { + ${euiFormControlDefaultShadow(euiThemeContext)} + box-shadow: none; + } + + .euiDatePopoverButton:focus, + .euiPopover-isOpen .euiDatePopoverButton { + --euiFormControlStateColor: ${euiTheme.colors.primary}; + background-size: 100% 100%; + } + `, + disabled: css` + .euiFormControlLayout__childrenWrapper { + ${euiFormControlDisabledStyles(euiThemeContext)} + } + `, + invalid: css` + .euiFormControlLayout__childrenWrapper { + color: ${euiTheme.colors.dangerText}; + background-color: ${forms.backgroundColor}; + ${euiFormControlInvalidStyles(euiThemeContext)} + } + `, + needsUpdating: css` + /* Extra specificity needed to override default delimited styles */ + .euiFormControlLayoutDelimited .euiFormControlLayout__childrenWrapper { + --euiFormControlStateColor: ${euiTheme.colors.success}; + color: ${needsUpdatingTextColor}; + background-color: ${needsUpdatingBackgroundColor}; + background-size: 100% 100%; + } + + .euiFormControlLayoutDelimited__delimiter { + color: inherit; + } + `, + }, + }; +}; + +export const _buttonStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + + return css` + ${logicalCSS('height', '100%')} + ${logicalCSS('width', '100%')} + ${logicalCSS('padding-horizontal', euiTheme.size.s)} + + /* Align content automatically for compressed heights */ + display: flex; + align-items: center; + + font-size: ${euiFontSize(euiThemeContext, 's').fontSize}; + word-break: break-all; + color: inherit; + background-color: inherit; + + &:disabled { + cursor: not-allowed; + } + `; +}; diff --git a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx index 03f6f151647..f6d9e552a08 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.test.tsx @@ -7,7 +7,6 @@ */ import React from 'react'; -import { mount, ReactWrapper } from 'enzyme'; import { fireEvent } from '@testing-library/react'; import { render } from '../../../test/rtl'; import { requiredProps } from '../../../test'; @@ -16,21 +15,10 @@ import { shouldRenderCustomStyles } from '../../../test/internal'; import { EuiSuperDatePicker, EuiSuperDatePickerProps, - EuiSuperDatePickerInternal, } from './super_date_picker'; -import { EuiButton } from '../../button'; const noop = () => {}; -// Test utils to handle diving into EuiSuperDatePickerInternal -const findInternalInstance = ( - wrapper: ReactWrapper -): [EuiSuperDatePickerInternal, ReactWrapper] => { - const component = wrapper.find('EuiSuperDatePickerInternal'); - const instance = component.instance() as EuiSuperDatePickerInternal; - return [instance, component]; -}; - describe('EuiSuperDatePicker', () => { // RTL doesn't automatically clean up portals/datepicker popovers between tests afterEach(() => { @@ -62,46 +50,26 @@ describe('EuiSuperDatePicker', () => { }); test('refresh is disabled by default', () => { - // By default we expect `asyncInterval` to be not set. - const component = mount(); - const [instancePaused, componentPaused] = findInternalInstance(component); + const { container } = render(); - expect(instancePaused.asyncInterval).toBe(undefined); - expect(componentPaused.prop('isPaused')).toBe(true); + expect( + container.querySelector('.euiAutoRefreshButton') + ).not.toBeInTheDocument(); }); test('updates refresh interval on isPaused prop update', () => { - // If refresh is enabled via `isPaused/onRefresh` we expect - // `asyncInterval` to be present and `asyncInterval.isStopped` to be `false`. const onRefresh = jest.fn(); - const component = mount( + const { container } = render( ); - const [instanceRefresh, componentRefresh] = findInternalInstance(component); - - expect(typeof instanceRefresh.asyncInterval).toBe('object'); - expect(instanceRefresh.asyncInterval!.isStopped).toBe(false); - expect(componentRefresh.prop('isPaused')).toBe(false); - - // If we update the prop `isPaused` we expect the interval to be stopped too. - component.setProps({ isPaused: true }); - const [instanceUpdatedPaused, componentUpdatedPaused] = - findInternalInstance(component); - expect(typeof instanceUpdatedPaused.asyncInterval).toBe('object'); - expect(instanceUpdatedPaused.asyncInterval!.isStopped).toBe(true); - expect(componentUpdatedPaused.prop('isPaused')).toBe(true); - - // Let's start refresh again for a final sanity check. - component.setProps({ isPaused: false }); - const [instanceUpdatedRefresh, componentUpdatedRefresh] = - findInternalInstance(component); - expect(typeof instanceUpdatedRefresh.asyncInterval).toBe('object'); - expect(instanceUpdatedRefresh.asyncInterval!.isStopped).toBe(false); - expect(componentUpdatedRefresh.prop('isPaused')).toBe(false); + const refreshButton = container.querySelector('.euiAutoRefreshButton'); + + expect(refreshButton).toBeInTheDocument(); + expect(refreshButton).toHaveTextContent('1 s'); }); test('Listen for consecutive super date picker refreshes', async () => { @@ -109,7 +77,7 @@ describe('EuiSuperDatePicker', () => { const onRefresh = jest.fn(); - const component = mount( + render( { refreshInterval={10} /> ); - const [instanceRefresh] = findInternalInstance(component); - expect(typeof instanceRefresh.asyncInterval).toBe('object'); - - jest.advanceTimersByTime(10); - await instanceRefresh.asyncInterval!.__pendingFn; jest.advanceTimersByTime(10); - await instanceRefresh.asyncInterval!.__pendingFn; + await jest.runAllTicks(); + expect(onRefresh).toHaveBeenCalledTimes(1); - expect(onRefresh).toBeCalledTimes(2); + jest.advanceTimersByTime(10); + await jest.runAllTicks(); + expect(onRefresh).toHaveBeenCalledTimes(2); jest.useRealTimers(); }); @@ -136,24 +102,31 @@ describe('EuiSuperDatePicker', () => { const onRefresh = jest.fn(); - const component = mount( + const { rerender } = render( ); - const [instanceRefresh] = findInternalInstance(component); jest.advanceTimersByTime(10); - expect(typeof instanceRefresh.asyncInterval).toBe('object'); - await instanceRefresh.asyncInterval!.__pendingFn; - component.setProps({ isPaused: true, refreshInterval: 0 }); - jest.advanceTimersByTime(10); - await instanceRefresh.asyncInterval!.__pendingFn; + await jest.runAllTicks(); + expect(onRefresh).toHaveBeenCalledTimes(1); - expect(onRefresh).toBeCalledTimes(1); + rerender( + + ); + + jest.advanceTimersByTime(10); + await jest.runAllTicks(); + expect(onRefresh).toHaveBeenCalledTimes(1); jest.useRealTimers(); }); @@ -165,20 +138,20 @@ describe('EuiSuperDatePicker', () => { color: 'danger', }; - const component = mount( + const { container } = render( ); - expect(component.find(EuiButton).last().props()).toMatchObject( - updateButtonProps - ); + const updateButton = container.querySelector('.euiSuperUpdateButton')!; + expect(updateButton.className).not.toContain('fill'); + expect(updateButton.className).toContain('danger'); }); it('invokes onFocus callbacks on the date popover buttons', () => { const focusMock = jest.fn(); - const component = mount( + const { getByTestSubject } = render( { /> ); - component - .find('button[data-test-subj="superDatePickerShowDatesButton"]') - .simulate('focus'); - expect(focusMock).toBeCalledTimes(1); + fireEvent.focus(getByTestSubject('superDatePickerShowDatesButton')); + expect(focusMock).toHaveBeenCalledTimes(1); - component - .find('button[data-test-subj="superDatePickerShowDatesButton"]') - .simulate('click'); + fireEvent.click(getByTestSubject('superDatePickerShowDatesButton')); - component - .find('button[data-test-subj="superDatePickerstartDatePopoverButton"]') - .simulate('focus'); - expect(focusMock).toBeCalledTimes(2); + fireEvent.focus( + getByTestSubject('superDatePickerstartDatePopoverButton') + ); + expect(focusMock).toHaveBeenCalledTimes(2); - component - .find('button[data-test-subj="superDatePickerstartDatePopoverButton"]') - .simulate('focus'); - expect(focusMock).toBeCalledTimes(3); + fireEvent.focus(getByTestSubject('superDatePickerendDatePopoverButton')); + expect(focusMock).toHaveBeenCalledTimes(3); }); describe('showUpdateButton', () => { diff --git a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.tsx b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.tsx index d7db318dd59..f9f070aee2d 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.tsx +++ b/packages/eui/src/components/date_picker/super_date_picker/super_date_picker.tsx @@ -16,6 +16,7 @@ import classNames from 'classnames'; import moment, { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named import dateMath from '@elastic/datemath'; +import { useEuiMemoizedStyles } from '../../../services'; import { isObject } from '../../../services/predicate'; import { EuiI18nConsumer } from '../../context'; import { CommonProps } from '../../common'; @@ -52,6 +53,8 @@ import { EuiAutoRefreshButton, } from '../auto_refresh/auto_refresh'; +import { euiSuperDatePickerStyles } from './super_date_picker.styles'; + export interface OnTimeChangeProps extends DurationRange { isInvalid: boolean; isQuickSelection: boolean; @@ -205,6 +208,7 @@ export type EuiSuperDatePickerProps = CommonProps & { }; type EuiSuperDatePickerInternalProps = EuiSuperDatePickerProps & { + memoizedStyles: ReturnType; timeOptions: TimeOptions; // The below options are marked as required because they have default fallbacks commonlyUsedRanges: DurationRange[]; @@ -512,6 +516,7 @@ export class EuiSuperDatePickerInternal extends Component< utcOffset, compressed, onFocus, + memoizedStyles: styles, } = this.props; const autoRefreshAppend: EuiFormControlLayoutProps['append'] = !isPaused ? ( @@ -533,6 +538,17 @@ export class EuiSuperDatePickerInternal extends Component< disabled: !!isDisabled, prepend: this.renderQuickSelect(), append: autoRefreshAppend, + fullWidth: true, + css: [ + styles.states.euiSuperDatePicker__formControlLayout, + isDisabled + ? styles.states.disabled + : isInvalid + ? styles.states.invalid + : hasChanged + ? styles.states.needsUpdating + : styles.states.default, + ], }; if (isQuickSelectOnly) { @@ -554,6 +570,7 @@ export class EuiSuperDatePickerInternal extends Component<