diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Controlled_Component.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Controlled_Component.png index 897272a25fc..76c83b3b9e5 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Controlled_Component.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Controlled_Component.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Icon_Shape.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Icon_Shape.png index 76b42b78108..401121dcafd 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Icon_Shape.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Icon_Shape.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Playground.png index c570b904487..9443dc4bf34 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldNumber_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldPassword_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldPassword_Playground.png index f11c7d56bc7..e65ce7349ef 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldPassword_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldPassword_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldSearch_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldSearch_Playground.png index 240d08b8add..1c1e0894a3f 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldSearch_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldSearch_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldText_Icon_Shape.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldText_Icon_Shape.png index ce79e9cbd46..3c079f73a92 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldText_Icon_Shape.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldText_Icon_Shape.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldText_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldText_Playground.png index adb9412ebed..93fbde02a44 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldText_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiFieldText_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSelect_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSelect_Playground.png index a7706db03da..dd9937bbf7e 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSelect_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSelect_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperSelect_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperSelect_Playground.png index 682f2f6532b..782e071a80c 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperSelect_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiSuperSelect_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiTextArea_Icon_Shape.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiTextArea_Icon_Shape.png index e3fb5d6648a..499c602733f 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiTextArea_Icon_Shape.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiTextArea_Icon_Shape.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiTextArea_Playground.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiTextArea_Playground.png index 23f6400513b..a507d2bc7e0 100644 Binary files a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiTextArea_Playground.png and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiTextArea_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Controlled_Component.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Controlled_Component.png index f60a83766ba..e1782502e20 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Controlled_Component.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Controlled_Component.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Icon_Shape.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Icon_Shape.png index b549f45fe90..0e7ddb8bc0a 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Icon_Shape.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Icon_Shape.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Playground.png index 54a9d1b3047..745b3b35db4 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldNumber_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldPassword_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldPassword_Playground.png index b8de798f53f..526e6d26a7b 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldPassword_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldPassword_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldSearch_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldSearch_Playground.png index 899a5b6e612..5873d9f7345 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldSearch_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldSearch_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldText_Icon_Shape.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldText_Icon_Shape.png index 41ba657c2fa..65de86c6887 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldText_Icon_Shape.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldText_Icon_Shape.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldText_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldText_Playground.png index 8a2c59b54ce..fe985d9875a 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldText_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiFieldText_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSelect_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSelect_Playground.png index 69842e4a721..d31afe8a659 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSelect_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSelect_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperSelect_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperSelect_Playground.png index 056ab1e904a..2702b2c7b6d 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperSelect_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiSuperSelect_Playground.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiTextArea_Icon_Shape.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiTextArea_Icon_Shape.png index 1282ed9ea0f..dd1133e11c6 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiTextArea_Icon_Shape.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiTextArea_Icon_Shape.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiTextArea_Playground.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiTextArea_Playground.png index ed1a199d0c5..ca732808831 100644 Binary files a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiTextArea_Playground.png and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiTextArea_Playground.png differ 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/changelogs/upcoming/7776.md b/packages/eui/changelogs/upcoming/7776.md new file mode 100644 index 00000000000..ceaeea60bd3 --- /dev/null +++ b/packages/eui/changelogs/upcoming/7776.md @@ -0,0 +1,3 @@ +**CSS-in-JS conversions** + +- Updated the autofill colors of Chrome (and other webkit browsers) to better match EUI's light and dark mode diff --git a/packages/eui/changelogs/upcoming/7799.md b/packages/eui/changelogs/upcoming/7799.md new file mode 100644 index 00000000000..77f4bb71e7b --- /dev/null +++ b/packages/eui/changelogs/upcoming/7799.md @@ -0,0 +1 @@ +- Updated `EuiFormControlLayout` to automatically pass icon padding affordance down to child `input`s diff --git a/packages/eui/changelogs/upcoming/7802.md b/packages/eui/changelogs/upcoming/7802.md new file mode 100644 index 00000000000..0bc6a6a183a --- /dev/null +++ b/packages/eui/changelogs/upcoming/7802.md @@ -0,0 +1,5 @@ +**CSS-in-JS conversions** + +- Converted `EuiFieldNumber` to Emotion +- Converted `EuiFieldSearch` to Emotion +- Converted `EuiFieldPassword` to Emotion diff --git a/packages/eui/changelogs/upcoming/7812.md b/packages/eui/changelogs/upcoming/7812.md new file mode 100644 index 00000000000..ef4dd542e4f --- /dev/null +++ b/packages/eui/changelogs/upcoming/7812.md @@ -0,0 +1,5 @@ +**CSS-in-JS conversions** + +- Converted `EuiTextArea` to Emotion +- Converted `EuiSelect` to Emotion +- Converted `EuiSuperSelect` 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/form_controls/form_controls_example.js b/packages/eui/src-docs/src/views/form_controls/form_controls_example.js index 04e18077c66..35e8b168289 100644 --- a/packages/eui/src-docs/src/views/form_controls/form_controls_example.js +++ b/packages/eui/src-docs/src/views/form_controls/form_controls_example.js @@ -477,16 +477,6 @@ export const FormControlsExample = { the controlOnly and type props of EuiFieldText as the wrapped control.

- - -

- The padding on the input itself doesn’t - take into account the presence of the various icons supported by{' '} - EuiFormControlLayout. Any input component - provided to EuiFormControlLayout is responsible - for its own padding. -

-
), props: { diff --git a/packages/eui/src-docs/src/views/form_controls/prepend_append.js b/packages/eui/src-docs/src/views/form_controls/prepend_append.js index 4e5751a1222..3dad35c7594 100644 --- a/packages/eui/src-docs/src/views/form_controls/prepend_append.js +++ b/packages/eui/src-docs/src/views/form_controls/prepend_append.js @@ -1,6 +1,7 @@ -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import { + EuiFlexGroup, EuiButtonEmpty, EuiButtonIcon, EuiFieldText, @@ -19,24 +20,24 @@ export default () => { const [isReadOnly, setReadOnly] = useState(false); return ( - - setCompressed(e.target.checked)} - /> -   - setDisabled(e.target.checked)} - /> -   - setReadOnly(e.target.checked)} - /> + <> + + setCompressed(e.target.checked)} + /> + setDisabled(e.target.checked)} + /> + setReadOnly(e.target.checked)} + /> + { readOnly={isReadOnly} aria-label="Use aria labels when no actual label is in use" /> - + ); }; 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/basic_table/in_memory_table.test.tsx b/packages/eui/src/components/basic_table/in_memory_table.test.tsx index 5aedbd0eddc..6e0371bedfe 100644 --- a/packages/eui/src/components/basic_table/in_memory_table.test.tsx +++ b/packages/eui/src/components/basic_table/in_memory_table.test.tsx @@ -947,7 +947,7 @@ describe('EuiInMemoryTable', () => { // should render with all three results visible expect(component.find('.testTable EuiTableRow').length).toBe(3); - const searchField = component.find('EuiFieldSearch input[type="search"]'); + const searchField = component.find('input.euiFieldSearch'); searchField.simulate('keyUp', { target: { 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`] = ` @@ -56,7 +56,7 @@ exports[`EuiColorPalettePicker is rendered with a selected custom text 1`] = ` + `); + + fireEvent.click(getByTestSubject('toggleButton')); + expect(getByTestSubject('toggleButton')).toMatchInlineSnapshot(` + + `); }); }); }); @@ -152,7 +164,7 @@ describe('EuiFieldPassword', () => { ); const input = container.querySelector('.euiFieldPassword'); - expect(input).toHaveClass('euiFieldPassword--fullWidth'); + expect(input!.className).toContain('fullWidth'); }); }); }); diff --git a/packages/eui/src/components/form/field_password/field_password.tsx b/packages/eui/src/components/form/field_password/field_password.tsx index cc09af063b4..9b90dc4cf47 100644 --- a/packages/eui/src/components/form/field_password/field_password.tsx +++ b/packages/eui/src/components/form/field_password/field_password.tsx @@ -10,23 +10,26 @@ import React, { InputHTMLAttributes, FunctionComponent, useState, + useMemo, + useCallback, Ref, } from 'react'; -import { CommonProps } from '../../common'; import classNames from 'classnames'; +import { useCombinedRefs, useEuiMemoizedStyles } from '../../../services'; +import { CommonProps } from '../../common'; +import { useEuiI18n } from '../../i18n'; +import { EuiButtonIcon, EuiButtonIconPropsForButton } from '../../button'; + import { EuiFormControlLayout, EuiFormControlLayoutProps, } from '../form_control_layout'; - import { EuiValidatableControl } from '../validatable_control'; -import { EuiButtonIcon, EuiButtonIconPropsForButton } from '../../button'; -import { useEuiI18n } from '../../i18n'; -import { useCombinedRefs } from '../../../services'; -import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; import { useFormContext } from '../eui_form_context'; +import { euiFieldPasswordStyles } from './field_password.styles'; + export type EuiFieldPasswordProps = Omit< InputHTMLAttributes, 'type' | 'value' @@ -110,62 +113,73 @@ export const EuiFieldPassword: FunctionComponent = ( const [inputRef, _setInputRef] = useState(null); const setInputRef = useCombinedRefs([_setInputRef, _inputRef]); - const handleToggle = ( - event: React.MouseEvent, - isVisible: boolean - ) => { - setInputType(isVisible ? 'password' : 'text'); - if (inputRef) { - inputRef.focus(); - } + const handleToggle = useCallback( + (event: React.MouseEvent, isVisible: boolean) => { + setInputType(isVisible ? 'password' : 'text'); + inputRef?.focus(); - if (dualToggleProps && dualToggleProps.onClick) { - dualToggleProps.onClick(event); - } - }; + dualToggleProps?.onClick?.(event); + }, + [inputRef, dualToggleProps] + ); - // Convert any `append` elements to an array so the visibility - // toggle can be added to it - let appends = Array.isArray(append) ? append : []; - if (append && !Array.isArray(append)) appends.push(append); // Add a toggling button to switch between `password` and `input` if consumer wants `dual` // https://www.w3schools.com/howto/howto_js_toggle_password.asp - if (type === 'dual') { - const isVisible = inputType === 'text'; - - const visibilityToggle = ( - handleToggle(e, isVisible)} - /> - ); - appends = [...appends, visibilityToggle]; - } - - const finalAppend = appends.length ? appends : undefined; - - const numIconsClass = getFormControlClassNameForIconCount({ - isInvalid, - isLoading, - }); + const visibilityToggle = useMemo(() => { + if (type === 'dual') { + const isVisible = inputType === 'text'; + + return ( + handleToggle(e, isVisible)} + /> + ); + } + }, [ + type, + inputType, + maskPasswordLabel, + showPasswordLabel, + dualToggleProps, + handleToggle, + rest.disabled, + ]); + + const finalAppend = useMemo(() => { + if (!visibilityToggle) return append; + if (!append) return visibilityToggle; + + // Convert any `append` elements to an array so the visibility + // toggle can be added to it + const appendAsArray = append + ? Array.isArray(append) + ? append + : [append] + : []; + + return [...appendAsArray, visibilityToggle]; + }, [append, visibilityToggle]); const classes = classNames( 'euiFieldPassword', - numIconsClass, - { - 'euiFieldPassword--fullWidth': fullWidth, - 'euiFieldPassword--compressed': compressed, - 'euiFieldPassword--inGroup': prepend || finalAppend, - 'euiFieldPassword--withToggle': type === 'dual', - 'euiFieldPassword-isLoading': isLoading, - }, + { 'euiFieldPassword-isLoading': isLoading }, className ); + const styles = useEuiMemoizedStyles(euiFieldPasswordStyles); + const cssStyles = [ + styles.euiFieldPassword, + compressed ? styles.compressed : styles.uncompressed, + fullWidth ? styles.fullWidth : styles.formWidth, + (finalAppend || prepend) && styles.inGroup, + type === 'dual' && styles.withToggle, + ]; + return ( = ( name={name} placeholder={placeholder} className={classes} + css={cssStyles} value={value} ref={setInputRef} {...rest} diff --git a/packages/eui/src/components/form/field_search/__snapshots__/field_search.test.tsx.snap b/packages/eui/src/components/form/field_search/__snapshots__/field_search.test.tsx.snap index ef18f1ed02a..15aeba22095 100644 --- a/packages/eui/src/components/form/field_search/__snapshots__/field_search.test.tsx.snap +++ b/packages/eui/src/components/form/field_search/__snapshots__/field_search.test.tsx.snap @@ -11,7 +11,7 @@ exports[`EuiFieldSearch is rendered 1`] = ` @@ -49,7 +49,7 @@ exports[`EuiFieldSearch props fullWidth is rendered 1`] = ` > @@ -65,7 +65,7 @@ exports[`EuiFieldSearch props isClearable is accepted 1`] = ` > @@ -82,7 +82,7 @@ exports[`EuiFieldSearch props isClearable is rendered when a value exists 1`] = > @@ -102,7 +102,7 @@ exports[`EuiFieldSearch props isInvalid is rendered 1`] = ` isinvalid="true" > @@ -118,7 +118,7 @@ exports[`EuiFieldSearch props isLoading is rendered 1`] = ` > @@ -135,7 +135,7 @@ exports[`EuiFieldSearch props prepend is rendered 1`] = ` > diff --git a/packages/eui/src/components/form/field_search/_field_search.scss b/packages/eui/src/components/form/field_search/_field_search.scss deleted file mode 100644 index ea3b602e2fc..00000000000 --- a/packages/eui/src/components/form/field_search/_field_search.scss +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 1. Fix for Safari to ensure that it renders like a normal text input - * and doesn't add extra spacing around text -*/ -// stylelint-disable property-no-vendor-prefix, selector-no-vendor-prefix - -.euiFieldSearch { - @include euiFormControlStyle; - @include euiFormControlWithIcon($isIconOptional: false); - @include euiFormControlIsLoading; - - -webkit-appearance: textfield; /* 1 */ - - &::-webkit-search-decoration, - &::-webkit-search-cancel-button { - -webkit-appearance: none; /* 1, 2 */ - } -} - -.euiFieldSearch--compressed { - @include euiFormControlWithIcon($isIconOptional: false, $side: 'left', $compressed: true); -} diff --git a/packages/eui/src/components/form/field_search/_index.scss b/packages/eui/src/components/form/field_search/_index.scss deleted file mode 100644 index b9652e5dde1..00000000000 --- a/packages/eui/src/components/form/field_search/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'field_search'; diff --git a/packages/eui/src/components/form/field_search/field_search.styles.ts b/packages/eui/src/components/form/field_search/field_search.styles.ts new file mode 100644 index 00000000000..417fa27b643 --- /dev/null +++ b/packages/eui/src/components/form/field_search/field_search.styles.ts @@ -0,0 +1,63 @@ +/* + * 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 { euiFormControlStyles } from '../form.styles'; + +export const euiFieldSearchStyles = (euiThemeContext: UseEuiTheme) => { + const formStyles = euiFormControlStyles(euiThemeContext); + + return { + euiFieldSearch: css` + /* Fix for Safari to ensure that it renders like a normal text input + * and doesn't add extra spacing around text */ + /* stylelint-disable property-no-vendor-prefix */ + -webkit-appearance: textfield; + + &::-webkit-search-decoration, + &::-webkit-search-cancel-button { + -webkit-appearance: none; + } + + ${formStyles.shared} + + &:invalid { + ${formStyles.invalid} + } + + &:focus { + ${formStyles.focus} + } + + &:disabled { + ${formStyles.disabled} + } + + &[readOnly] { + ${formStyles.readOnly} + } + + &:autofill { + ${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), + }; +}; diff --git a/packages/eui/src/components/form/field_search/field_search.test.tsx b/packages/eui/src/components/form/field_search/field_search.test.tsx index 53e6541a35b..010fc17bcb0 100644 --- a/packages/eui/src/components/form/field_search/field_search.test.tsx +++ b/packages/eui/src/components/form/field_search/field_search.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { requiredProps } from '../../../test/required_props'; +import { shouldRenderCustomStyles } from '../../../test/internal'; import { render } from '../../../test/rtl'; import { EuiForm } from '../form'; @@ -21,6 +22,8 @@ jest.mock('../validatable_control', () => ({ })); describe('EuiFieldSearch', () => { + shouldRenderCustomStyles(); + test('is rendered', () => { const { container } = render( { ); const input = container.querySelector('.euiFieldSearch'); - expect(input).toHaveClass('euiFieldSearch--fullWidth'); + expect(input?.className).toContain('fullWidth'); }); }); }); diff --git a/packages/eui/src/components/form/field_search/field_search.tsx b/packages/eui/src/components/form/field_search/field_search.tsx index b3c9bc595ef..902d33fa932 100644 --- a/packages/eui/src/components/form/field_search/field_search.tsx +++ b/packages/eui/src/components/form/field_search/field_search.tsx @@ -8,19 +8,24 @@ import React, { Component, InputHTMLAttributes, KeyboardEvent } from 'react'; import classNames from 'classnames'; + import { Browser } from '../../../services/browser'; import { CommonProps } from '../../common'; -import { keys } from '../../../services'; +import { + keys, + withEuiStylesMemoizer, + WithEuiStylesMemoizerProps, +} from '../../../services'; import { EuiFormControlLayout, EuiFormControlLayoutProps, } from '../form_control_layout'; - import { EuiValidatableControl } from '../validatable_control'; -import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; import { FormContext, FormContextValue } from '../eui_form_context'; +import { euiFieldSearchStyles } from './field_search.styles'; + export interface EuiFieldSearchProps extends CommonProps, InputHTMLAttributes { @@ -74,8 +79,8 @@ interface EuiFieldSearchState { let isSearchSupported: boolean = false; -export class EuiFieldSearch extends Component< - EuiFieldSearchProps, +export class EuiFieldSearchClass extends Component< + EuiFieldSearchProps & WithEuiStylesMemoizerProps, EuiFieldSearchState > { static contextType = FormContext; @@ -205,6 +210,7 @@ export class EuiFieldSearch extends Component< render() { const { defaultFullWidth } = this.context as FormContextValue; const { + stylesMemoizer, className, id, name, @@ -230,19 +236,9 @@ export class EuiFieldSearch extends Component< _isClearable && value && !rest.readOnly && !rest.disabled ); - const numIconsClass = getFormControlClassNameForIconCount({ - clear: isClearable, - isInvalid, - isLoading, - }); - const classes = classNames( 'euiFieldSearch', - numIconsClass, { - 'euiFieldSearch--fullWidth': fullWidth, - 'euiFieldSearch--compressed': compressed, - 'euiFieldSearch--inGroup': prepend || append, 'euiFieldSearch-isLoading': isLoading, 'euiFieldSearch-isClearable': isClearable, 'euiFieldSearch-isInvalid': isInvalid, @@ -250,6 +246,14 @@ export class EuiFieldSearch extends Component< className ); + const styles = stylesMemoizer(euiFieldSearchStyles); + const cssStyles = [ + styles.euiFieldSearch, + compressed ? styles.compressed : styles.uncompressed, + fullWidth ? styles.fullWidth : styles.formWidth, + (prepend || append) && styles.inGroup, + ]; + return ( this.onKeyUp(e, incremental, onSearch)} ref={this.setRef} {...rest} @@ -281,3 +286,6 @@ export class EuiFieldSearch extends Component< ); } } + +export const EuiFieldSearch = + withEuiStylesMemoizer(EuiFieldSearchClass); diff --git a/packages/eui/src/components/form/field_text/__snapshots__/field_text.test.tsx.snap b/packages/eui/src/components/form/field_text/__snapshots__/field_text.test.tsx.snap index df2492e7a8a..68c5fee6a55 100644 --- a/packages/eui/src/components/form/field_text/__snapshots__/field_text.test.tsx.snap +++ b/packages/eui/src/components/form/field_text/__snapshots__/field_text.test.tsx.snap @@ -9,7 +9,7 @@ exports[`EuiFieldText 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`] = ` > ( +
+ In Chrome: Type any text, press Enter, then go back and select the + autofill suggestion. Test light+dark mode as well as invalid state +
+
+ + + ), + ], + args: { + name: 'autofill-test', + }, +}; diff --git a/packages/eui/src/components/form/field_text/field_text.styles.ts b/packages/eui/src/components/form/field_text/field_text.styles.ts new file mode 100644 index 00000000000..dd1b194e8ca --- /dev/null +++ b/packages/eui/src/components/form/field_text/field_text.styles.ts @@ -0,0 +1,58 @@ +/* + * 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 { euiFormControlStyles } from '../form.styles'; + +export const euiFieldTextStyles = (euiThemeContext: UseEuiTheme) => { + const formStyles = euiFormControlStyles(euiThemeContext); + + return { + euiFieldText: css` + ${formStyles.shared} + + &:invalid { + ${formStyles.invalid} + } + + &:focus { + ${formStyles.focus} + } + + &:disabled { + ${formStyles.disabled} + } + + &[readOnly] { + ${formStyles.readOnly} + } + + &:autofill { + ${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..4cd427d2b93 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 @@ -8,6 +8,7 @@ import React from 'react'; import { requiredProps } from '../../../test/required_props'; +import { shouldRenderCustomStyles } from '../../../test/internal'; import { render } from '../../../test/rtl'; import { EuiForm } from '../form'; @@ -25,6 +26,8 @@ jest.mock('../validatable_control', () => ({ })); describe('EuiFieldText', () => { + shouldRenderCustomStyles(); + test('is rendered', () => { const { container } = render( { ); 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..3f99adc1762 100644 --- a/packages/eui/src/components/form/field_text/field_text.tsx +++ b/packages/eui/src/components/form/field_text/field_text.tsx @@ -7,21 +7,19 @@ */ import React, { InputHTMLAttributes, Ref, FunctionComponent } from 'react'; -import { CommonProps } from '../../common'; 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 { useFormContext } from '../eui_form_context'; +import { euiFieldTextStyles } from './field_text.styles'; + export type EuiFieldTextProps = InputHTMLAttributes & CommonProps & { icon?: EuiFormControlLayoutProps['icon']; @@ -81,26 +79,19 @@ 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 control = ( = (props) => { name={name} placeholder={placeholder} className={classes} + css={cssStyles} 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..80ff5357c83 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,9 @@ 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)", "controlBorderRadius": "6px", "controlBoxShadow": "0 0 transparent", "controlCompressedBorderRadius": "4px", @@ -53,9 +52,12 @@ describe('euiFormVariables', () => { "controlPlaceholderText": "#646a77", "customControlBorderColor": "#f5f7fc", "customControlDisabledIconColor": "#cacfd8", + "iconAffordance": "24px", + "iconCompressedAffordance": "18px", "inputGroupBorder": "none", "inputGroupLabelBackground": "#e9edf3", "maxWidth": "400px", + "textColor": "#343741", } `); }); @@ -73,105 +75,150 @@ 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; + -webkit-box-shadow: inset 0 0 0 1px rgba(0,107,184,0.2), inset 0 0 0 100vw #f0f7fc; - 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;" - `); - }); + &:invalid { + -webkit-box-shadow: inset 0 0 0 1px #BD271E, inset 0 0 0 100vw #f0f7fc; + } + } + ", + "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..d47b4940fcc 100644 --- a/packages/eui/src/components/form/form.styles.ts +++ b/packages/eui/src/components/form/form.styles.ts @@ -15,10 +15,12 @@ import { makeHighContrastColor, } from '../../services'; import { + logicalCSS, mathWithUnits, euiCanAnimate, euiFontSize, } from '../../global_styling'; +import { euiButtonColor } from '../../themes/amsterdam/global_styling/mixins'; export const euiFormVariables = (euiThemeContext: UseEuiTheme) => { const { euiTheme, colorMode } = euiThemeContext; @@ -38,14 +40,21 @@ 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)( @@ -96,31 +105,67 @@ 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: euiFormControlAutoFillStyles(euiThemeContext), + }; }; export const euiCustomControl = ( @@ -165,14 +210,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 +229,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 +257,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 +267,80 @@ 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; + `; +}; + +export const euiFormControlAutoFillStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme, colorMode } = euiThemeContext; + + // Make the text color slightly less prominent than the default colors.text + const textColor = euiTheme.colors.darkestShade; + + const { backgroundColor } = euiButtonColor(euiThemeContext, 'primary'); + const tintedBackgroundColor = + colorMode === 'DARK' + ? shade(backgroundColor, 0.5) + : tint(backgroundColor, 0.7); + // Hacky workaround to background-color, since Chrome doesn't normally allow overriding its styles + // @see https://developer.mozilla.org/en-US/docs/Web/CSS/:autofill#sect1 + const backgroundShadow = `inset 0 0 0 100vw ${tintedBackgroundColor}`; + + // Re-create the border, since the above webkit box shadow overrides the default border box-shadow + // + change the border color to match states, since the underline background gradient no longer works + const borderColor = transparentize(euiTheme.colors.primaryText, 0.2); + const invalidBorder = euiTheme.colors.danger; + const borderShadow = (color: string) => + `inset 0 0 0 ${euiTheme.border.width.thin} ${color}`; + + // These styles only apply/override Chrome/webkit browsers - Firefox does not set autofill styles + return ` + &:-webkit-autofill { + -webkit-text-fill-color: ${textColor}; + -webkit-box-shadow: ${borderShadow(borderColor)}, ${backgroundShadow}; + + &:invalid { + -webkit-box-shadow: ${borderShadow(invalidBorder)}, ${backgroundShadow}; + } + } + `; +}; + 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/_form_control_layout.scss b/packages/eui/src/components/form/form_control_layout/_form_control_layout.scss index b0b41443cf1..d94c5af5e36 100644 --- a/packages/eui/src/components/form/form_control_layout/_form_control_layout.scss +++ b/packages/eui/src/components/form/form_control_layout/_form_control_layout.scss @@ -5,6 +5,7 @@ // Let the height expand as needed @include euiFormControlSize($includeAlternates: true); + // TODO: Remove this once all form controls are on Emotion/setting padding via CSS variables $iconSize: map-get($euiFormControlIconSizes, 'medium'); $iconPadding: $euiFormControlPadding; $marginBetweenIcons: $euiFormControlPadding / 2; 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..1dad9f03c73 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?: EuiFormControlLayoutIconsProps['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/form_control_layout/form_control_layout.tsx b/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx index 5039efa0268..00b20ab9cca 100644 --- a/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx @@ -8,21 +8,23 @@ import React, { cloneElement, - Component, + FunctionComponent, HTMLAttributes, ReactElement, ReactNode, + useCallback, + useMemo, } from 'react'; import classNames from 'classnames'; +import { getIconAffordanceStyles, isRightSideIcon } from './_num_icons'; import { EuiFormControlLayoutIcons, EuiFormControlLayoutIconsProps, - IconShape, } from './form_control_layout_icons'; import { CommonProps } from '../../common'; import { EuiFormLabel } from '../form_label'; -import { FormContext, FormContextValue } from '../eui_form_context'; +import { useFormContext } from '../eui_form_context'; type StringOrReactElement = string | ReactElement; type PrependAppendType = StringOrReactElement | StringOrReactElement[]; @@ -41,6 +43,11 @@ export type EuiFormControlLayoutProps = CommonProps & append?: PrependAppendType; children?: ReactNode; icon?: EuiFormControlLayoutIconsProps['icon']; + /** + * Determines whether icons are absolutely or statically rendered. For single inputs, + * absolute rendering is typically preferred. + * @default absolute + */ iconsPosition?: EuiFormControlLayoutIconsProps['iconsPosition']; clear?: EuiFormControlLayoutIconsProps['clear']; /** @@ -65,156 +72,134 @@ export type EuiFormControlLayoutProps = CommonProps & inputId?: string; }; -export class EuiFormControlLayout extends Component { - static contextType = FormContext; - - render() { - const { defaultFullWidth } = this.context as FormContextValue; - const { - children, +export const EuiFormControlLayout: FunctionComponent< + EuiFormControlLayoutProps +> = (props) => { + const { defaultFullWidth } = useFormContext(); + const { + inputId, + className, + children, + icon, + iconsPosition = 'absolute', + clear, + isDropdown, + isLoading, + isInvalid, + isDisabled, + readOnly, + compressed, + prepend, + append, + fullWidth = defaultFullWidth, + ...rest + } = props; + + const classes = classNames( + 'euiFormControlLayout', + { + 'euiFormControlLayout--fullWidth': fullWidth, + 'euiFormControlLayout--compressed': compressed, + 'euiFormControlLayout--readOnly': readOnly, + 'euiFormControlLayout--group': prepend || append, + 'euiFormControlLayout-isDisabled': isDisabled, + }, + className + ); + + const hasDropdownIcon = !readOnly && !isDisabled && isDropdown; + const hasRightIcon = isRightSideIcon(icon); + const hasLeftIcon = icon && !hasRightIcon; + const hasRightIcons = + hasRightIcon || clear || isLoading || isInvalid || hasDropdownIcon; + + const iconAffordanceStyles = useMemo(() => { + if (iconsPosition === 'static') return; // Static icons don't need padding affordance + + return getIconAffordanceStyles({ icon, - iconsPosition, clear, - fullWidth = defaultFullWidth, - isLoading, - isDisabled, - compressed, - className, - prepend, - append, - readOnly, isInvalid, - isDropdown, - inputId, - ...rest - } = this.props; - - const classes = classNames( - 'euiFormControlLayout', - { - 'euiFormControlLayout--fullWidth': fullWidth, - 'euiFormControlLayout--compressed': compressed, - 'euiFormControlLayout--readOnly': readOnly, - 'euiFormControlLayout--group': prepend || append, - 'euiFormControlLayout-isDisabled': isDisabled, - }, - className - ); - - const prependNodes = this.renderSideNode('prepend', prepend, inputId); - const appendNodes = this.renderSideNode('append', append, inputId); - - return ( -
- {prependNodes} -
- {this.renderLeftIcons()} - {children} - {this.renderRightIcons()} -
- {appendNodes} -
- ); - } - - renderLeftIcons = () => { - const { icon, iconsPosition, compressed } = this.props; - - const leftCustomIcon = - icon && (icon as IconShape)?.side !== 'right' ? icon : undefined; - - return leftCustomIcon ? ( - - ) : null; - }; - - renderRightIcons = () => { - const { - icon, - iconsPosition, - clear, - compressed, isLoading, - isInvalid, - isDisabled, - readOnly, - isDropdown, - } = this.props; - const hasDropdownIcon = !readOnly && !isDisabled && isDropdown; - - const rightCustomIcon = - icon && (icon as IconShape)?.side === 'right' ? icon : undefined; - - const hasRightIcons = - rightCustomIcon || clear || isLoading || isInvalid || hasDropdownIcon; - - return hasRightIcons ? ( - + - ) : null; - }; - - renderSideNode( - side: 'append' | 'prepend', - nodes?: PrependAppendType, - inputId?: string - ) { - if (!nodes) { - return; - } - - if (typeof nodes === 'string') { - return this.createFormLabel(side, nodes, inputId); - } - - const appendNodes = React.Children.map(nodes, (item, index) => - typeof item === 'string' - ? this.createFormLabel(side, item, inputId) - : this.createSideNode(side, item, index) - ); - - return appendNodes; - } - - createFormLabel( - side: 'append' | 'prepend', - string: string, - inputId?: string - ) { - return ( - - {string} + {hasLeftIcon && ( + + )} + + {children} + + {hasRightIcons && ( + + )} + + + + ); +}; + +/** + * Internal subcomponent utility for prepend/append nodes + */ +const EuiFormControlLayoutSideNodes: FunctionComponent<{ + side: 'append' | 'prepend'; + nodes?: PrependAppendType; // For some bizarre reason if you make this the `children` prop instead, React doesn't properly override cloned keys :| + inputId?: string; +}> = ({ side, nodes, inputId }) => { + const className = `euiFormControlLayout__${side}`; + + const renderFormLabel = useCallback( + (label: string) => ( + + {label} - ); - } - - createSideNode( - side: 'append' | 'prepend', - node: ReactElement, - key: React.Key - ) { - return cloneElement(node, { - className: classNames( - `euiFormControlLayout__${side}`, - node.props.className - ), - key: key, - }); - } -} + ), + [inputId, className] + ); + + if (!nodes) return null; + + return ( + <> + {React.Children.map(nodes, (node, index) => + typeof node === 'string' + ? renderFormLabel(node) + : cloneElement(node, { + className: classNames(className, node.props.className), + key: index, + }) + )} + + ); +}; 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..7b28bd5627b 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`] = `