From 71c016aa652208ea7c7567da09f90d402b5c5727 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Mon, 20 May 2024 13:56:22 -0700 Subject: [PATCH 1/8] [Emotion] Convert `EuiFieldText` (#7770) --- packages/eui/changelogs/upcoming/7770.md | 7 + .../form_controls/form_control_layout.tsx | 17 +- .../super_date_picker/super_date_picker.tsx | 11 +- .../__snapshots__/color_picker.test.tsx.snap | 24 +- .../color_picker/_color_picker.scss | 14 +- .../components/color_picker/color_picker.tsx | 8 +- .../column_selector.test.tsx.snap | 6 +- .../__snapshots__/date_picker.test.tsx.snap | 2 +- .../date_picker_range.test.tsx.snap | 34 +-- .../__snapshots__/auto_refresh.test.tsx.snap | 6 +- .../components/date_picker/date_picker.tsx | 61 +++-- .../date_picker/date_picker_range.styles.ts | 23 +- .../date_picker/date_picker_range.tsx | 37 ++- .../react-datepicker/src/index.d.ts | 4 +- .../date_picker/react-datepicker/src/index.js | 11 +- .../super_date_picker.test.tsx.snap | 4 +- packages/eui/src/components/form/_index.scss | 1 - .../__snapshots__/field_text.test.tsx.snap | 12 +- .../form/field_text/_field_text.scss | 8 - .../components/form/field_text/_index.scss | 1 - .../form/field_text/field_text.styles.ts | 56 +++++ .../form/field_text/field_text.test.tsx | 2 +- .../components/form/field_text/field_text.tsx | 54 ++-- .../src/components/form/form.styles.test.tsx | 236 +++++++++++------- .../eui/src/components/form/form.styles.ts | 165 +++++++++--- .../form_control_layout/_num_icons.test.ts | 60 +++++ .../form/form_control_layout/_num_icons.ts | 37 +++ .../__snapshots__/dual_range.test.tsx.snap | 24 +- .../range/__snapshots__/range.test.tsx.snap | 28 +-- .../form/range/range_wrapper.styles.ts | 15 +- .../components/form/range/range_wrapper.tsx | 5 +- .../inline_edit_form.test.tsx.snap | 16 +- .../markdown_editor_drop_zone.styles.ts | 2 +- .../markdown_editor_text_area.styles.ts | 13 +- 34 files changed, 641 insertions(+), 363 deletions(-) create mode 100644 packages/eui/changelogs/upcoming/7770.md delete mode 100644 packages/eui/src/components/form/field_text/_field_text.scss delete mode 100644 packages/eui/src/components/form/field_text/_index.scss create mode 100644 packages/eui/src/components/form/field_text/field_text.styles.ts diff --git a/packages/eui/changelogs/upcoming/7770.md b/packages/eui/changelogs/upcoming/7770.md new file mode 100644 index 00000000000..cbf02ef5538 --- /dev/null +++ b/packages/eui/changelogs/upcoming/7770.md @@ -0,0 +1,7 @@ +**Bug fixes** + +- Fixed broken focus/invalid styling on compressed `EuiDatePickerRange`s + +**CSS-in-JS conversions** + +- Converted `EuiFieldText` to Emotion diff --git a/packages/eui/src-docs/src/views/form_controls/form_control_layout.tsx b/packages/eui/src-docs/src/views/form_controls/form_control_layout.tsx index 02821d6134e..7f2c4b6a086 100644 --- a/packages/eui/src-docs/src/views/form_controls/form_control_layout.tsx +++ b/packages/eui/src-docs/src/views/form_controls/form_control_layout.tsx @@ -100,12 +100,7 @@ export default () => { Label} > - + { } append={Button} > - + { > @@ -150,7 +138,6 @@ export default () => { > diff --git a/packages/eui/src-docs/src/views/super_date_picker/super_date_picker.tsx b/packages/eui/src-docs/src/views/super_date_picker/super_date_picker.tsx index c2575fe0da0..71da95be5ac 100644 --- a/packages/eui/src-docs/src/views/super_date_picker/super_date_picker.tsx +++ b/packages/eui/src-docs/src/views/super_date_picker/super_date_picker.tsx @@ -5,6 +5,7 @@ import { EuiSpacer, EuiFormControlLayoutDelimited, EuiFormLabel, + EuiFieldText, EuiPanel, EuiText, OnRefreshProps, @@ -58,21 +59,19 @@ export default () => { Dates} startControl={ - } endControl={ - } /> diff --git a/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap b/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap index 01a480b6704..371dea2b8a0 100644 --- a/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap +++ b/packages/eui/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap @@ -27,7 +27,7 @@ exports[`renders EuiColorPicker 1`] = ` = ({ clear: isClearable, isInvalid, }); - const inputClasses = classNames( - 'euiColorPicker__input', - { 'euiColorPicker__input--inGroup': prepend || append }, - // Manually account for input padding, since `controlOnly` disables that logic - 'euiFieldText--withIcon', - numIconsClass - ); + const inputClasses = classNames('euiColorPicker__input', numIconsClass); const handleOnChange = (text: string) => { const output = getOutput(text, showAlpha); diff --git a/packages/eui/src/components/datagrid/controls/__snapshots__/column_selector.test.tsx.snap b/packages/eui/src/components/datagrid/controls/__snapshots__/column_selector.test.tsx.snap index c18c44fbb19..ef96b659268 100644 --- a/packages/eui/src/components/datagrid/controls/__snapshots__/column_selector.test.tsx.snap +++ b/packages/eui/src/components/datagrid/controls/__snapshots__/column_selector.test.tsx.snap @@ -69,7 +69,7 @@ exports[`useDataGridColumnSelector columnSelector [React 16] renders a toolbar b > diff --git a/packages/eui/src/components/date_picker/__snapshots__/date_picker_range.test.tsx.snap b/packages/eui/src/components/date_picker/__snapshots__/date_picker_range.test.tsx.snap index 68e649270f8..d177cdb3634 100644 --- a/packages/eui/src/components/date_picker/__snapshots__/date_picker_range.test.tsx.snap +++ b/packages/eui/src/components/date_picker/__snapshots__/date_picker_range.test.tsx.snap @@ -33,7 +33,7 @@ exports[`EuiDatePickerRange is rendered 1`] = ` > @@ -56,7 +56,7 @@ exports[`EuiDatePickerRange is rendered 1`] = ` > @@ -98,7 +98,7 @@ exports[`EuiDatePickerRange props compressed 1`] = ` > @@ -121,7 +121,7 @@ exports[`EuiDatePickerRange props compressed 1`] = ` > @@ -163,7 +163,7 @@ exports[`EuiDatePickerRange props disabled 1`] = ` > @@ -253,7 +253,7 @@ exports[`EuiDatePickerRange props fullWidth 1`] = ` > @@ -266,7 +266,7 @@ exports[`EuiDatePickerRange props fullWidth 1`] = ` exports[`EuiDatePickerRange props inline renders 1`] = `
@@ -1226,7 +1226,7 @@ exports[`EuiDatePickerRange props isInvalid 1`] = ` @@ -1276,7 +1276,7 @@ exports[`EuiDatePickerRange props isLoading 1`] = ` > @@ -1299,7 +1299,7 @@ exports[`EuiDatePickerRange props isLoading 1`] = ` > @@ -1350,7 +1350,7 @@ exports[`EuiDatePickerRange props readOnly 1`] = ` > @@ -1442,7 +1442,7 @@ exports[`EuiDatePickerRange uses individual EuiDatePicker props 1`] = ` > diff --git a/packages/eui/src/components/date_picker/auto_refresh/__snapshots__/auto_refresh.test.tsx.snap b/packages/eui/src/components/date_picker/auto_refresh/__snapshots__/auto_refresh.test.tsx.snap index 5fc2252abd1..29e3652341e 100644 --- a/packages/eui/src/components/date_picker/auto_refresh/__snapshots__/auto_refresh.test.tsx.snap +++ b/packages/eui/src/components/date_picker/auto_refresh/__snapshots__/auto_refresh.test.tsx.snap @@ -34,7 +34,7 @@ exports[`EuiAutoRefresh is rendered 1`] = ` > = ({ 'euiDatePicker--shadow': inline && shadow, }); - const numIconsClass = controlOnly - ? false - : getFormControlClassNameForIconCount({ - isInvalid, - isLoading, - }); - - const datePickerClasses = classNames( - 'euiDatePicker', - 'euiFieldText', - numIconsClass, - !inline && { - 'euiFieldText--fullWidth': fullWidth, - 'euiFieldText-isLoading': isLoading, - 'euiFieldText--compressed': compressed, - 'euiFieldText--withIcon': showIcon, - 'euiFieldText--isClearable': selected && onClear, - }, - className - ); + const datePickerClasses = classNames('euiDatePicker', className); - let optionalIcon: EuiFormControlLayoutIconsProps['icon']; - if (inline || customInput || !showIcon) { - optionalIcon = undefined; - } else if (iconType) { - optionalIcon = iconType; - } else if (showTimeSelectOnly) { - optionalIcon = 'clock'; - } else { - optionalIcon = 'calendar'; - } + // Passed to the default EuiFieldText input, not passed to custom inputs + const defaultInputProps = + !inline && !customInput ? { compressed, fullWidth } : undefined; // In case the consumer did not alter the default date format but wants // to add the time select, we append the default time format @@ -251,6 +227,7 @@ export const EuiDatePicker: FunctionComponent = ({ adjustDateOnChange={adjustDateOnChange} calendarClassName={calendarClassName} className={datePickerClasses} + defaultInputProps={defaultInputProps} customInput={customInput} dateFormat={fullDateFormat} dayClassName={dayClassName} @@ -290,6 +267,27 @@ export const EuiDatePicker: FunctionComponent = ({ if (controlOnly) return control; + let optionalIcon: EuiFormControlLayoutIconsProps['icon']; + if (inline || customInput || !showIcon) { + optionalIcon = undefined; + } else if (iconType) { + optionalIcon = iconType; + } else if (showTimeSelectOnly) { + optionalIcon = 'clock'; + } else { + optionalIcon = 'calendar'; + } + + // TODO: DRY out icon affordance logic to EuiFormControlLayout in the next few PRs + const iconAffordanceStyles = !controlOnly + ? getIconAffordanceStyles({ + icon: optionalIcon, + clear: !!(selected && onClear), + isInvalid, + isLoading, + }) + : undefined; + return ( = ({ inline && isInvalid && !disabled && !readOnly, })} iconsPosition={inline ? 'static' : undefined} + style={iconAffordanceStyles} // TODO > {control} diff --git a/packages/eui/src/components/date_picker/date_picker_range.styles.ts b/packages/eui/src/components/date_picker/date_picker_range.styles.ts index 6738f3d3ec8..56549e75f85 100644 --- a/packages/eui/src/components/date_picker/date_picker_range.styles.ts +++ b/packages/eui/src/components/date_picker/date_picker_range.styles.ts @@ -10,19 +10,16 @@ import { css } from '@emotion/react'; import { logicalCSS } from '../../global_styling'; import { euiShadowMedium } from '../../themes/amsterdam/global_styling/mixins'; import { UseEuiTheme } from '../../services'; -import { euiFormVariables } from '../form/form.styles'; -export const euiDatePickerRangeStyles = (euiThemeContext: UseEuiTheme) => { - const { controlLayoutGroupInputHeight } = euiFormVariables(euiThemeContext); - - return { - euiDatePickerRange: css` - .euiFieldText.euiDatePicker { - /* Needed for correct focus/invalid box-shadow styles */ - ${logicalCSS('height', controlLayoutGroupInputHeight)} - } - `, - }; +export const euiDatePickerRangeStyles = { + euiDatePickerRange: css` + /* Needed for correct focus/invalid underline/linear-gradient styles */ + .euiPopover, + .react-datepicker__input-container, + .euiDatePicker { + ${logicalCSS('height', '100%')} + } + `, }; export const euiDatePickerRangeInlineStyles = ( @@ -55,7 +52,7 @@ export const euiDatePickerRangeInlineStyles = ( }`; return { - inline: css` + euiDatePickerRangeInline: css` .euiFormControlLayoutDelimited { /* Reset form control styling */ ${logicalCSS('height', 'auto')} diff --git a/packages/eui/src/components/date_picker/date_picker_range.tsx b/packages/eui/src/components/date_picker/date_picker_range.tsx index f246fb36905..c51804c7b89 100644 --- a/packages/eui/src/components/date_picker/date_picker_range.tsx +++ b/packages/eui/src/components/date_picker/date_picker_range.tsx @@ -24,9 +24,9 @@ import { import { IconType } from '../icon'; import { CommonProps } from '../common'; -import { useEuiTheme } from '../../services'; +import { useEuiMemoizedStyles } from '../../services'; import { - euiDatePickerRangeStyles, + euiDatePickerRangeStyles as styles, euiDatePickerRangeInlineStyles, } from './date_picker_range.styles'; @@ -124,25 +124,18 @@ export const EuiDatePickerRange: FunctionComponent = ({ const classes = classNames('euiDatePickerRange', className); - const euiTheme = useEuiTheme(); - const styles = euiDatePickerRangeStyles(euiTheme); - const cssStyles = [styles.euiDatePickerRange]; - - if (inline) { - // Determine the inline container query to use based on the width of the react-datepicker - const hasTimeSelect = - startDateControl.props.showTimeSelect || - endDateControl.props.showTimeSelect; - - const inlineStyles = euiDatePickerRangeInlineStyles(euiTheme); - cssStyles.push(inlineStyles.inline); - cssStyles.push( - hasTimeSelect - ? inlineStyles.responsiveWithTimeSelect - : inlineStyles.responsive - ); - if (shadow) cssStyles.push(inlineStyles.shadow); - } + const inlineStyles = useEuiMemoizedStyles(euiDatePickerRangeInlineStyles); + const cssStyles = !inline + ? styles.euiDatePickerRange + : [ + inlineStyles.euiDatePickerRangeInline, + // Determine the inline container query to use based on the width of the react-datepicker + startDateControl.props.showTimeSelect || + endDateControl.props.showTimeSelect + ? inlineStyles.responsiveWithTimeSelect + : inlineStyles.responsive, + shadow && inlineStyles.shadow, + ]; let startControl = startDateControl; let endControl = endDateControl; @@ -154,6 +147,7 @@ export const EuiDatePickerRange: FunctionComponent = ({ controlOnly: true, showIcon: false, inline, + compressed, fullWidth, readOnly, disabled: disabled || startDateControl.props.disabled, @@ -179,6 +173,7 @@ export const EuiDatePickerRange: FunctionComponent = ({ controlOnly: true, showIcon: false, inline, + compressed, fullWidth, readOnly, disabled: disabled || endDateControl.props.disabled, diff --git a/packages/eui/src/components/date_picker/react-datepicker/src/index.d.ts b/packages/eui/src/components/date_picker/react-datepicker/src/index.d.ts index c89db3e28e6..8af0ebe458b 100644 --- a/packages/eui/src/components/date_picker/react-datepicker/src/index.d.ts +++ b/packages/eui/src/components/date_picker/react-datepicker/src/index.d.ts @@ -22,7 +22,8 @@ import * as React from 'react'; import * as moment from 'moment'; -import { EuiPopoverProps, PopoverAnchorPosition } from '../../../popover'; +import type { EuiPopoverProps, PopoverAnchorPosition } from '../../../popover'; +import type { EuiFieldTextProps } from '../../../form/field_text'; export interface ReactDatePickerProps { /** @@ -44,6 +45,7 @@ export interface ReactDatePickerProps { * Added to the actual input of the calendar */ className?: string; + defaultInputProps?: Partial; /** * Replaces the input with any node, like a button diff --git a/packages/eui/src/components/date_picker/react-datepicker/src/index.js b/packages/eui/src/components/date_picker/react-datepicker/src/index.js index a8048cfcb3e..72c3d6885f9 100644 --- a/packages/eui/src/components/date_picker/react-datepicker/src/index.js +++ b/packages/eui/src/components/date_picker/react-datepicker/src/index.js @@ -64,7 +64,8 @@ import { getMonth } from "./date_utils"; -import {EuiPopover, popoverAnchorPosition} from '../../../popover/popover'; +import { EuiPopover, popoverAnchorPosition } from '../../../popover/popover'; +import { EuiFieldText } from '../../../form/field_text'; export { default as CalendarContainer } from "./calendar_container"; @@ -769,8 +770,12 @@ export default class DatePicker extends React.Component { [outsideClickIgnoreClass]: this.state.open }); - const customInput = this.props.customInput || ; - const customInputRef = this.props.customInputRef || "ref"; + const customInput = this.props.customInput || ( + + ); + const customInputRef = + this.props.customInputRef ?? + (this.props.customInput ? 'ref' : 'inputRef'); const inputValue = typeof this.props.value === "string" ? this.props.value 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 61d3d0abcea..15009f90921 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 @@ -176,7 +176,7 @@ exports[`EuiSuperDatePicker props isAutoRefreshOnly is rendered 1`] = ` > @@ -37,7 +37,7 @@ exports[`EuiFieldText props fullWidth is rendered 1`] = ` > @@ -54,7 +54,7 @@ exports[`EuiFieldText props isInvalid is rendered 1`] = ` isinvalid="true" > @@ -69,7 +69,7 @@ exports[`EuiFieldText props isLoading is rendered 1`] = ` > @@ -84,7 +84,7 @@ exports[`EuiFieldText props readOnly is rendered 1`] = ` > { + const formStyles = euiFormControlStyles(euiThemeContext); + + return { + euiFieldText: css` + ${formStyles.shared} + + &:invalid { + ${formStyles.invalid} + } + + &:focus { + ${formStyles.focus} + } + + &:disabled { + ${formStyles.disabled} + } + + &[readOnly] { + ${formStyles.readOnly} + } + + ${formStyles.autoFill} + `, + + // Skip the css() on the default height to avoid generating a className + uncompressed: formStyles.uncompressed, + compressed: css(formStyles.compressed), + + // Skip the css() on the default width to avoid generating a className + formWidth: formStyles.formWidth, + fullWidth: css(formStyles.fullWidth), + + // Layout modifiers + inGroup: css(formStyles.inGroup), + controlOnly: css` + .euiFormControlLayout--group & { + ${formStyles.inGroup} + } + `, + }; +}; diff --git a/packages/eui/src/components/form/field_text/field_text.test.tsx b/packages/eui/src/components/form/field_text/field_text.test.tsx index cd811d10c6a..1cf143a4828 100644 --- a/packages/eui/src/components/form/field_text/field_text.test.tsx +++ b/packages/eui/src/components/form/field_text/field_text.test.tsx @@ -83,7 +83,7 @@ describe('EuiFieldText', () => { ); const input = getByRole('textbox'); - expect(input).toHaveClass('euiFieldText--fullWidth'); + expect(input.className).toContain('fullWidth'); }); }); }); diff --git a/packages/eui/src/components/form/field_text/field_text.tsx b/packages/eui/src/components/form/field_text/field_text.tsx index fb89e2263b4..1c0d10a03b8 100644 --- a/packages/eui/src/components/form/field_text/field_text.tsx +++ b/packages/eui/src/components/form/field_text/field_text.tsx @@ -6,22 +6,26 @@ * Side Public License, v 1. */ -import React, { InputHTMLAttributes, Ref, FunctionComponent } from 'react'; -import { CommonProps } from '../../common'; +import React, { + InputHTMLAttributes, + Ref, + FunctionComponent, + useMemo, +} from 'react'; import classNames from 'classnames'; +import { useEuiMemoizedStyles } from '../../../services'; +import { CommonProps } from '../../common'; import { EuiFormControlLayout, EuiFormControlLayoutProps, } from '../form_control_layout'; - import { EuiValidatableControl } from '../validatable_control'; -import { - isRightSideIcon, - getFormControlClassNameForIconCount, -} from '../form_control_layout/_num_icons'; +import { getIconAffordanceStyles } from '../form_control_layout/_num_icons'; import { useFormContext } from '../eui_form_context'; +import { euiFieldTextStyles } from './field_text.styles'; + export type EuiFieldTextProps = InputHTMLAttributes & CommonProps & { icon?: EuiFormControlLayoutProps['icon']; @@ -68,6 +72,7 @@ export const EuiFieldText: FunctionComponent = (props) => { placeholder, value, className, + style, icon, isInvalid, inputRef, @@ -81,26 +86,25 @@ export const EuiFieldText: FunctionComponent = (props) => { ...rest } = props; - const hasRightSideIcon = isRightSideIcon(icon); - - const numIconsClass = controlOnly - ? false - : getFormControlClassNameForIconCount({ - isInvalid, - isLoading, - icon: hasRightSideIcon, - }); - - const classes = classNames('euiFieldText', className, numIconsClass, { - 'euiFieldText--fullWidth': fullWidth, - 'euiFieldText--compressed': compressed, - ...(!controlOnly && { - 'euiFieldText--withIcon': icon && !hasRightSideIcon, - 'euiFieldText--inGroup': prepend || append, - }), + const classes = classNames('euiFieldText', className, { 'euiFieldText-isLoading': isLoading, }); + const styles = useEuiMemoizedStyles(euiFieldTextStyles); + const cssStyles = [ + styles.euiFieldText, + compressed ? styles.compressed : styles.uncompressed, + fullWidth ? styles.fullWidth : styles.formWidth, + !controlOnly && (prepend || append) && styles.inGroup, + controlOnly && styles.controlOnly, + ]; + + const iconAffordanceStyles = useMemo(() => { + return !controlOnly + ? getIconAffordanceStyles({ icon, isInvalid, isLoading }) + : undefined; + }, [controlOnly, icon, isInvalid, isLoading]); + const control = ( = (props) => { name={name} placeholder={placeholder} className={classes} + css={cssStyles} + style={{ ...iconAffordanceStyles, ...style }} value={value} ref={inputRef} readOnly={readOnly} diff --git a/packages/eui/src/components/form/form.styles.test.tsx b/packages/eui/src/components/form/form.styles.test.tsx index dcf015f0142..d65acf01cfe 100644 --- a/packages/eui/src/components/form/form.styles.test.tsx +++ b/packages/eui/src/components/form/form.styles.test.tsx @@ -13,7 +13,7 @@ import { EuiProvider } from '../provider'; import { euiFormVariables, - euiFormControlSize, + euiFormControlStyles, euiCustomControl, } from './form.styles'; @@ -28,10 +28,10 @@ describe('euiFormVariables', () => { Object { "animationTiming": "150ms ease-in", "backgroundColor": "#f9fbfd", - "backgroundDisabledColor": "#eceff5", + "backgroundDisabledColor": "#eef1f7", "backgroundReadOnlyColor": "#FFF", - "borderColor": "rgba(211,218,230,0.9)", - "borderDisabledColor": "rgba(211,218,230,0.9)", + "borderColor": "rgba(32,38,47,0.1)", + "controlAutoFillColor": "#343741", "controlBorderRadius": "6px", "controlBoxShadow": "0 0 transparent", "controlCompressedBorderRadius": "4px", @@ -53,9 +53,12 @@ describe('euiFormVariables', () => { "controlPlaceholderText": "#646a77", "customControlBorderColor": "#f5f7fc", "customControlDisabledIconColor": "#cacfd8", + "iconAffordance": "24px", + "iconCompressedAffordance": "18px", "inputGroupBorder": "none", "inputGroupLabelBackground": "#e9edf3", "maxWidth": "400px", + "textColor": "#343741", } `); }); @@ -73,105 +76,148 @@ describe('euiFormVariables', () => { }); }); -describe('euiFormControlSize', () => { - it('outputs the logical properties for height, width, and max-width', () => { - const { result } = renderHook(() => euiFormControlSize(useEuiTheme())); - expect(result.current.trim()).toMatchInlineSnapshot(` - "max-inline-size: 400px; - inline-size: 100%; - block-size: 40px;" - `); - }); +describe('euiFormControlStyles', () => { + it('outputs an object of control states and modifiers', () => { + const { result } = renderHook(() => euiFormControlStyles(useEuiTheme())); + expect(result.current).toMatchInlineSnapshot(` + Object { + "autoFill": " + &:-webkit-autofill { + -webkit-text-fill-color: #343741; - it('allows passing in a custom height', () => { - const { result } = renderHook(() => - euiFormControlSize(useEuiTheme(), { height: '100px' }) - ); - expect(result.current.trim()).toMatchInlineSnapshot(` - "max-inline-size: 400px; - inline-size: 100%; - block-size: 100px;" - `); - }); + ~ .euiFormControlLayoutIcons { + color: #343741; + } + }", + "compressed": " + block-size: 32px; + padding-block: 8px; + padding-inline-start: calc(8px + (18px * var(--euiFormControlLeftIconsCount, 0))); + padding-inline-end: calc(8px + (18px * var(--euiFormControlRightIconsCount, 0))); + border-radius: 4px; + ", + "disabled": " + color: #98A2B3; + /* Required for Safari */ + -webkit-text-fill-color: #98A2B3; + background-color: #eef1f7; + cursor: not-allowed; - test('fullWidth', () => { - const { result } = renderHook(() => - euiFormControlSize(useEuiTheme(), { fullWidth: true }) - ); - expect(result.current.trim()).toMatchInlineSnapshot(` - "max-inline-size: 100%; - inline-size: 100%; - block-size: 40px;" - `); - }); + + &::-webkit-input-placeholder { + color: #98A2B3; + opacity: 1; + } + &::-moz-placeholder { + color: #98A2B3; + opacity: 1; + } + &:-ms-input-placeholder { + color: #98A2B3; + opacity: 1; + } + &:-moz-placeholder { + color: #98A2B3; + opacity: 1; + } + &::placeholder { + color: #98A2B3; + opacity: 1; + } - test('compressed', () => { - const { result } = renderHook(() => - euiFormControlSize(useEuiTheme(), { compressed: true }) - ); - expect(result.current.trim()).toMatchInlineSnapshot(` - "max-inline-size: 400px; - inline-size: 100%; - block-size: 32px;" - `); - }); + ", + "focus": " + --euiFormControlStateColor: #07C; + background-color: #FFF; + background-size: 100% 100%; + outline: none; /* Remove all outlines and rely on our own bottom border gradient */ + ", + "formWidth": " + max-inline-size: 400px; + inline-size: 100%; + ", + "fullWidth": " + max-inline-size: 100%; + inline-size: 100%; + ", + "inGroup": " + block-size: 100%; + box-shadow: none; + border-radius: 0; + ", + "invalid": " + --euiFormControlStateColor: #BD271E; + background-size: 100% 100%; + ", + "readOnly": " + cursor: default; + color: #343741; + -webkit-text-fill-color: #343741; /* Required for Safari */ - test('compressed & fullWidth', () => { - const { result } = renderHook(() => - euiFormControlSize(useEuiTheme(), { compressed: true, fullWidth: true }) - ); - expect(result.current.trim()).toMatchInlineSnapshot(` - "max-inline-size: 100%; - inline-size: 100%; - block-size: 32px;" - `); - }); + background-color: #FFF; + --euiFormControlStateColor: transparent; + ", + "shared": " + + font-family: 'Inter', BlinkMacSystemFont, Helvetica, Arial, sans-serif; + font-size: 1.0000rem; + color: #343741; - test('inGroup', () => { - const { result } = renderHook(() => - euiFormControlSize(useEuiTheme(), { inGroup: true }) - ); - expect(result.current.trim()).toMatchInlineSnapshot(` - "max-inline-size: 400px; - inline-size: 100%; - block-size: 100%;" - `); - }); + + &::-webkit-input-placeholder { + color: #646a77; + opacity: 1; + } + &::-moz-placeholder { + color: #646a77; + opacity: 1; + } + &:-ms-input-placeholder { + color: #646a77; + opacity: 1; + } + &:-moz-placeholder { + color: #646a77; + opacity: 1; + } + &::placeholder { + color: #646a77; + opacity: 1; + } - test('inGroup & fullWidth', () => { - const { result } = renderHook(() => - euiFormControlSize(useEuiTheme(), { inGroup: true, fullWidth: true }) - ); - expect(result.current.trim()).toMatchInlineSnapshot(` - "max-inline-size: 100%; - inline-size: 100%; - block-size: 100%;" - `); - }); + + + /* We use inset box-shadow instead of border to skip extra hight calculations */ + border: none; + box-shadow: inset 0 0 0 1px rgba(32,38,47,0.1); + background-color: #f9fbfd; - test('compressed overrides custom height', () => { - const { result } = renderHook(() => - euiFormControlSize(useEuiTheme(), { height: '500px', compressed: true }) - ); - expect(result.current.trim()).toMatchInlineSnapshot(` - "max-inline-size: 400px; - inline-size: 100%; - block-size: 32px;" - `); - }); + background-repeat: no-repeat; + background-size: 0% 100%; + background-image: linear-gradient(to top, + var(--euiFormControlStateColor), + var(--euiFormControlStateColor) 2px, + transparent 2px, + transparent 100% + ); - test('inGroup overrides compressed and custom height', () => { - const { result } = renderHook(() => - euiFormControlSize(useEuiTheme(), { - height: '500px', - compressed: true, - inGroup: true, - }) - ); - expect(result.current.trim()).toMatchInlineSnapshot(` - "max-inline-size: 400px; - inline-size: 100%; - block-size: 100%;" + @media screen and (prefers-reduced-motion: no-preference) { + transition: + box-shadow 150ms ease-in, + background-image 150ms ease-in, + background-size 150ms ease-in, + background-color 150ms ease-in; + } + + ", + "uncompressed": " + block-size: 40px; + padding-block: 12px; + padding-inline-start: calc(12px + (24px * var(--euiFormControlLeftIconsCount, 0))); + padding-inline-end: calc(12px + (24px * var(--euiFormControlRightIconsCount, 0))); + border-radius: 6px; + ", + } `); }); }); diff --git a/packages/eui/src/components/form/form.styles.ts b/packages/eui/src/components/form/form.styles.ts index 57834bafb09..3932e2ebfa0 100644 --- a/packages/eui/src/components/form/form.styles.ts +++ b/packages/eui/src/components/form/form.styles.ts @@ -15,6 +15,7 @@ import { makeHighContrastColor, } from '../../services'; import { + logicalCSS, mathWithUnits, euiCanAnimate, euiFontSize, @@ -38,19 +39,30 @@ export const euiFormVariables = (euiThemeContext: UseEuiTheme) => { controlCompressedPadding: euiTheme.size.s, controlBorderRadius: euiTheme.border.radius.medium, controlCompressedBorderRadius: euiTheme.border.radius.small, + iconAffordance: mathWithUnits(euiTheme.size.base, (x) => x * 1.5), + iconCompressedAffordance: mathWithUnits(euiTheme.size.m, (x) => x * 1.5), }; const colors = { + textColor: euiTheme.colors.text, backgroundColor: backgroundColor, - backgroundDisabledColor: darken(euiTheme.colors.lightestShade, 0.1), + backgroundDisabledColor: darken(euiTheme.colors.lightestShade, 0.05), backgroundReadOnlyColor: euiTheme.colors.emptyShade, - borderColor: transparentize(euiTheme.border.color, 0.9), - borderDisabledColor: transparentize(euiTheme.border.color, 0.9), + borderColor: transparentize( + colorMode === 'DARK' + ? euiTheme.colors.ghost + : darken(euiTheme.border.color, 4), + 0.1 + ), controlDisabledColor: euiTheme.colors.mediumShade, controlBoxShadow: '0 0 transparent', controlPlaceholderText: makeHighContrastColor(euiTheme.colors.subduedText)( backgroundColor ), + controlAutoFillColor: + colorMode === 'LIGHT' + ? euiTheme.colors.darkestShade + : euiTheme.colors.lightShade, inputGroupLabelBackground: isColorDark ? shade(euiTheme.colors.lightShade, 0.15) : tint(euiTheme.colors.lightShade, 0.5), @@ -96,31 +108,74 @@ export const euiFormVariables = (euiThemeContext: UseEuiTheme) => { }; }; -export const euiFormControlSize = ( - euiThemeContext: UseEuiTheme, - options: { - height?: string; - fullWidth?: boolean; - compressed?: boolean; - inGroup?: boolean; - } = {} -) => { +export const euiFormControlStyles = (euiThemeContext: UseEuiTheme) => { const form = euiFormVariables(euiThemeContext); - const width = '100%'; + return { + shared: ` + ${euiFormControlText(euiThemeContext)} + ${euiFormControlDefaultShadow(euiThemeContext)} + `, - let maxWidth = form.maxWidth; - if (options.fullWidth) maxWidth = '100%'; + // Sizes + uncompressed: ` + ${logicalCSS('height', form.controlHeight)} + ${logicalCSS('padding-vertical', form.controlPadding)} + ${logicalCSS( + 'padding-left', + `calc(${form.controlPadding} + (${form.iconAffordance} * var(--euiFormControlLeftIconsCount, 0)))` + )} + ${logicalCSS( + 'padding-right', + `calc(${form.controlPadding} + (${form.iconAffordance} * var(--euiFormControlRightIconsCount, 0)))` + )} + border-radius: ${form.controlBorderRadius}; + `, + compressed: ` + ${logicalCSS('height', form.controlCompressedHeight)} + ${logicalCSS('padding-vertical', form.controlCompressedPadding)} + ${logicalCSS( + 'padding-left', + `calc(${form.controlCompressedPadding} + (${form.iconCompressedAffordance} * var(--euiFormControlLeftIconsCount, 0)))` + )} + ${logicalCSS( + 'padding-right', + `calc(${form.controlCompressedPadding} + (${form.iconCompressedAffordance} * var(--euiFormControlRightIconsCount, 0)))` + )} + border-radius: ${form.controlCompressedBorderRadius}; + `, - let height = options.height || form.controlHeight; - if (options.compressed) height = form.controlCompressedHeight; - if (options.inGroup) height = '100%'; + // In group + inGroup: ` + ${logicalCSS('height', '100%')} + box-shadow: none; + border-radius: 0; + `, - return ` - max-inline-size: ${maxWidth}; - inline-size: ${width}; - block-size: ${height}; - `; + // Widths + formWidth: ` + ${logicalCSS('max-width', form.maxWidth)} + ${logicalCSS('width', '100%')} + `, + fullWidth: ` + ${logicalCSS('max-width', '100%')} + ${logicalCSS('width', '100%')} + `, + + // States + invalid: euiFormControlInvalidStyles(euiThemeContext), + focus: euiFormControlFocusStyles(euiThemeContext), + disabled: euiFormControlDisabledStyles(euiThemeContext), + readOnly: euiFormControlReadOnlyStyles(euiThemeContext), + autoFill: ` + &:-webkit-autofill { + -webkit-text-fill-color: ${form.controlAutoFillColor}; + + ~ .euiFormControlLayoutIcons { + color: ${form.controlAutoFillColor}; + } + }`, + }; }; export const euiCustomControl = ( @@ -165,14 +220,17 @@ export const euiCustomControl = ( export const euiFormControlText = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; const { fontSize } = euiFontSize(euiThemeContext, 's'); - const { controlPlaceholderText } = euiFormVariables(euiThemeContext); + const form = euiFormVariables(euiThemeContext); return ` font-family: ${euiTheme.font.family}; font-size: ${fontSize}; - color: ${euiTheme.colors.text}; + color: ${form.textColor}; - ${euiPlaceholderPerBrowser(`color: ${controlPlaceholderText}`)} + ${euiPlaceholderPerBrowser(` + color: ${form.controlPlaceholderText}; + opacity: 1; + `)} `; }; @@ -181,14 +239,16 @@ export const euiFormControlDefaultShadow = (euiThemeContext: UseEuiTheme) => { const form = euiFormVariables(euiThemeContext); return ` - box-shadow: inset 0 0 0 1px ${form.borderColor}; + /* We use inset box-shadow instead of border to skip extra hight calculations */ + border: none; + box-shadow: inset 0 0 0 ${euiTheme.border.width.thin} ${form.borderColor}; background-color: ${form.backgroundColor}; background-repeat: no-repeat; background-size: 0% 100%; background-image: linear-gradient(to top, - var(--euiFormStateColor), - var(--euiFormStateColor) ${euiTheme.border.width.thick}, + var(--euiFormControlStateColor), + var(--euiFormControlStateColor) ${euiTheme.border.width.thick}, transparent ${euiTheme.border.width.thick}, transparent 100% ); @@ -207,7 +267,7 @@ export const euiFormControlFocusStyles = ({ euiTheme, colorMode, }: UseEuiTheme) => ` - --euiFormStateColor: ${euiTheme.colors.primary}; + --euiFormControlStateColor: ${euiTheme.colors.primary}; background-color: ${ colorMode === 'DARK' ? shade(euiTheme.colors.emptyShade, 0.4) @@ -217,10 +277,45 @@ export const euiFormControlFocusStyles = ({ outline: none; /* Remove all outlines and rely on our own bottom border gradient */ `; +export const euiFormControlInvalidStyles = ({ euiTheme }: UseEuiTheme) => ` + --euiFormControlStateColor: ${euiTheme.colors.danger}; + background-size: 100% 100%; +`; + +export const euiFormControlDisabledStyles = (euiThemeContext: UseEuiTheme) => { + const form = euiFormVariables(euiThemeContext); + + return ` + color: ${form.controlDisabledColor}; + /* Required for Safari */ + -webkit-text-fill-color: ${form.controlDisabledColor}; + background-color: ${form.backgroundDisabledColor}; + cursor: not-allowed; + + ${euiPlaceholderPerBrowser(` + color: ${form.controlDisabledColor}; + opacity: 1; + `)} + `; +}; + +export const euiFormControlReadOnlyStyles = (euiThemeContext: UseEuiTheme) => { + const form = euiFormVariables(euiThemeContext); + + return ` + cursor: default; + color: ${form.textColor}; + -webkit-text-fill-color: ${form.textColor}; /* Required for Safari */ + + background-color: ${form.backgroundReadOnlyColor}; + --euiFormControlStateColor: transparent; + `; +}; + const euiPlaceholderPerBrowser = (content: string) => ` - &::-webkit-input-placeholder { ${content}; opacity: 1; } - &::-moz-placeholder { ${content}; opacity: 1; } - &:-ms-input-placeholder { ${content}; opacity: 1; } - &:-moz-placeholder { ${content}; opacity: 1; } - &::placeholder { ${content}; opacity: 1; } + &::-webkit-input-placeholder { ${content} } + &::-moz-placeholder { ${content} } + &:-ms-input-placeholder { ${content} } + &:-moz-placeholder { ${content} } + &::placeholder { ${content} } `; diff --git a/packages/eui/src/components/form/form_control_layout/_num_icons.test.ts b/packages/eui/src/components/form/form_control_layout/_num_icons.test.ts index 4b3f26693e7..2ac6beb228e 100644 --- a/packages/eui/src/components/form/form_control_layout/_num_icons.test.ts +++ b/packages/eui/src/components/form/form_control_layout/_num_icons.test.ts @@ -9,8 +9,68 @@ import { getFormControlClassNameForIconCount, isRightSideIcon, + getIconAffordanceStyles, } from './_num_icons'; +describe('getIconAffordanceStyles', () => { + const noIcons = { + icon: undefined, + clear: false, + isLoading: false, + isInvalid: false, + isDropdown: false, + }; + const allIcons = { + icon: { type: 'search', side: 'right' as const }, + clear: true, + isLoading: true, + isInvalid: true, + isDropdown: true, + }; + + test('empty object', () => { + const styles = getIconAffordanceStyles({}); + expect(styles).toEqual(undefined); + }); + + test('false values', () => { + const styles = getIconAffordanceStyles(noIcons); + expect(styles).toEqual(undefined); + }); + + test('all icons', () => { + const styles = getIconAffordanceStyles(allIcons); + expect(styles).toMatchInlineSnapshot(` + Object { + "--euiFormControlRightIconsCount": 5, + } + `); + }); + + test('some icons', () => { + const styles = getIconAffordanceStyles({ + isLoading: true, + isInvalid: true, + }); + expect(styles).toMatchInlineSnapshot(` + Object { + "--euiFormControlRightIconsCount": 2, + } + `); + }); + + test('left icon', () => { + const styles = getIconAffordanceStyles({ + icon: 'search', + }); + expect(styles).toMatchInlineSnapshot(` + Object { + "--euiFormControlLeftIconsCount": 1, + } + `); + }); +}); + describe('getFormControlClassNameForIconCount', () => { it('should return undefined if object is empty', () => { const numberClass = getFormControlClassNameForIconCount({}); diff --git a/packages/eui/src/components/form/form_control_layout/_num_icons.ts b/packages/eui/src/components/form/form_control_layout/_num_icons.ts index 95c792c4cf2..da6be253d96 100644 --- a/packages/eui/src/components/form/form_control_layout/_num_icons.ts +++ b/packages/eui/src/components/form/form_control_layout/_num_icons.ts @@ -51,3 +51,40 @@ export const isRightSideIcon = ( ): boolean => { return !!icon && isIconShape(icon) && icon.side === 'right'; }; + +export const getIconAffordanceStyles = ({ + icon, + clear, + isLoading, + isInvalid, + isDropdown, +}: { + icon?: EuiFormControlLayoutIconsProps['icon']; + clear?: boolean; + isLoading?: boolean; + isInvalid?: boolean; + isDropdown?: boolean; +}) => { + const cssVariables = { + '--euiFormControlLeftIconsCount': 0, + '--euiFormControlRightIconsCount': 0, + }; + + if (icon) { + if (isRightSideIcon(icon)) { + cssVariables['--euiFormControlRightIconsCount']++; + } else { + cssVariables['--euiFormControlLeftIconsCount']++; + } + } + + if (clear) cssVariables['--euiFormControlRightIconsCount']++; + if (isLoading) cssVariables['--euiFormControlRightIconsCount']++; + if (isInvalid) cssVariables['--euiFormControlRightIconsCount']++; + if (isDropdown) cssVariables['--euiFormControlRightIconsCount']++; + + const filtered = Object.entries(cssVariables).filter( + ([, count]) => count > 0 + ); + return filtered.length ? Object.fromEntries(filtered) : undefined; +}; diff --git a/packages/eui/src/components/form/range/__snapshots__/dual_range.test.tsx.snap b/packages/eui/src/components/form/range/__snapshots__/dual_range.test.tsx.snap index 341ba7373e8..2adf9255bfe 100644 --- a/packages/eui/src/components/form/range/__snapshots__/dual_range.test.tsx.snap +++ b/packages/eui/src/components/form/range/__snapshots__/dual_range.test.tsx.snap @@ -2,7 +2,7 @@ exports[`EuiDualRange props isDraggable renders draggable track when isDraggable=true 1`] = `