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_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/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/components/color_picker/color_palette_picker/__snapshots__/color_palette_picker.test.tsx.snap b/packages/eui/src/components/color_picker/color_palette_picker/__snapshots__/color_palette_picker.test.tsx.snap index 2b60ef54598..d8277f1cf44 100644 --- a/packages/eui/src/components/color_picker/color_palette_picker/__snapshots__/color_palette_picker.test.tsx.snap +++ b/packages/eui/src/components/color_picker/color_palette_picker/__snapshots__/color_palette_picker.test.tsx.snap @@ -17,7 +17,7 @@ exports[`EuiColorPalettePicker is rendered 1`] = ` @@ -56,7 +56,7 @@ exports[`EuiColorPalettePicker is rendered with a selected custom text 1`] = ` @@ -97,7 +97,7 @@ exports[`EuiColorPalettePicker is rendered with a selected fixed palette 1`] = ` @@ -166,7 +166,7 @@ exports[`EuiColorPalettePicker is rendered with a selected gradient palette 1`] @@ -215,7 +215,7 @@ exports[`EuiColorPalettePicker is rendered with a selected gradient palette with @@ -264,7 +264,7 @@ exports[`EuiColorPalettePicker is rendered with the prop selectionDisplay set as @@ -304,7 +304,7 @@ exports[`EuiColorPalettePicker more props are propagated to each option 1`] = ` > diff --git a/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap b/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap index 6d200d8fb67..b7df63978ef 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap +++ b/packages/eui/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap @@ -94,7 +94,7 @@ exports[`EuiQuickSelectPanels customQuickSelectPanels should render custom panel diff --git a/packages/eui/src/components/form/_index.scss b/packages/eui/src/components/form/_index.scss index c80dd014ae0..83c850af420 100644 --- a/packages/eui/src/components/form/_index.scss +++ b/packages/eui/src/components/form/_index.scss @@ -2,11 +2,6 @@ @import 'described_form_group/index'; @import 'file_picker/index'; @import 'form'; - -// Selects must come before form_control_layout for proper padding -@import 'select/index'; -@import 'super_select/index'; - @import 'form_control_layout/index'; @import 'form_error_text/index'; @import 'form_fieldset/index'; @@ -15,4 +10,3 @@ @import 'form_row/index'; @import 'radio/index'; @import 'switch/index'; -@import 'text_area/index'; diff --git a/packages/eui/src/components/form/select/__snapshots__/select.test.tsx.snap b/packages/eui/src/components/form/select/__snapshots__/select.test.tsx.snap index c1de6cf32e1..e554be7edab 100644 --- a/packages/eui/src/components/form/select/__snapshots__/select.test.tsx.snap +++ b/packages/eui/src/components/form/select/__snapshots__/select.test.tsx.snap @@ -11,7 +11,7 @@ exports[`EuiSelect is rendered 1`] = ` @@ -74,7 +74,7 @@ exports[`EuiSelect props isInvalid is rendered 1`] = ` isinvalid="true" > @@ -89,7 +89,7 @@ exports[`EuiSelect props isLoading is rendered 1`] = ` > @@ -104,7 +104,7 @@ exports[`EuiSelect props options are rendered 1`] = ` > = { title: 'Forms/EuiSelect', component: EuiSelect, + parameters: { + controls: { + // Excude onMouseUp from controls, as it's not a terribly useful prop to document + exclude: ['onMouseUp'], + }, + }, argTypes: { append: { control: 'radio', @@ -53,18 +59,13 @@ const meta: Meta = { }, }; // adding onChange for visibility -enableFunctionToggleControls(meta, ['onChange', 'onMouseUp']); +enableFunctionToggleControls(meta, ['onChange']); disableStorybookControls(meta, ['inputRef']); export default meta; type Story = StoryObj; export const Playground: Story = { - parameters: { - controls: { - exclude: ['onMouseUp'], - }, - }, args: { defaultValue: 'option-2', options: [ diff --git a/packages/eui/src/components/form/select/select.styles.ts b/packages/eui/src/components/form/select/select.styles.ts new file mode 100644 index 00000000000..dbf391938f1 --- /dev/null +++ b/packages/eui/src/components/form/select/select.styles.ts @@ -0,0 +1,67 @@ +/* + * 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, euiFormVariables } from '../form.styles'; + +export const euiSelectStyles = (euiThemeContext: UseEuiTheme) => { + const formStyles = euiFormControlStyles(euiThemeContext); + const formVariables = euiFormVariables(euiThemeContext); + + return { + euiSelect: css` + 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), + + // Ensure text descenders don't get cut off + // by making line-height match the input height + lineHeight: { + removePadding: `padding-block: 0;`, + uncompressed: `line-height: ${formVariables.controlHeight};`, + compressed: `line-height: ${formVariables.controlCompressedHeight};`, + inGroup: { + uncompressed: `line-height: ${formVariables.controlLayoutGroupInputHeight}`, + compressed: `line-height: ${formVariables.controlLayoutGroupInputCompressedHeight}`, + }, + }, + }; +}; diff --git a/packages/eui/src/components/form/select/select.test.tsx b/packages/eui/src/components/form/select/select.test.tsx index 7fd3b6104cf..4ef41eb63d2 100644 --- a/packages/eui/src/components/form/select/select.test.tsx +++ b/packages/eui/src/components/form/select/select.test.tsx @@ -119,7 +119,7 @@ describe('EuiSelect', () => { > -`); + `); }); it('can be reset to an empty initial selection', () => { @@ -165,7 +165,7 @@ describe('EuiSelect', () => { ); const select = container.querySelector('.euiSelect'); - expect(select).toHaveClass('euiSelect--fullWidth'); + expect(select!.className).toContain('fullWidth'); }); }); }); diff --git a/packages/eui/src/components/form/select/select.tsx b/packages/eui/src/components/form/select/select.tsx index cd75d3fc560..1674b649e42 100644 --- a/packages/eui/src/components/form/select/select.tsx +++ b/packages/eui/src/components/form/select/select.tsx @@ -11,16 +11,21 @@ import React, { OptionHTMLAttributes, Ref, FunctionComponent, + useCallback, } from 'react'; -import { CommonProps } from '../../common'; import classNames from 'classnames'; + +import { useEuiMemoizedStyles } from '../../../services'; +import { CommonProps } from '../../common'; + +import { useFormContext } from '../eui_form_context'; import { EuiFormControlLayout, EuiFormControlLayoutProps, } from '../form_control_layout'; import { EuiValidatableControl } from '../validatable_control'; -import { useFormContext } from '../eui_form_context'; -import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; + +import { euiSelectStyles } from './select.styles'; export interface EuiSelectOption extends OptionHTMLAttributes { @@ -96,48 +101,45 @@ export const EuiSelect: FunctionComponent = (props) => { // value needs to fallback to an empty string to interact properly with `defaultValue` const value = hasNoInitialSelection ? _value ?? '' : _value; - const handleMouseUp = (e: React.MouseEvent) => { - // Normalizes cross-browser mouse eventing by preventing propagation, - // notably for use in conjunction with EuiOutsideClickDetector. - // See https://github.com/elastic/eui/pull/1926 for full discussion on - // rationale and alternatives should this intervention become problematic. - e.nativeEvent.stopImmediatePropagation(); - if (onMouseUp) onMouseUp(e); - }; + // React HTML input can not have both value and defaultValue properties. + // https://reactjs.org/docs/uncontrolled-components.html#default-values + const selectDefaultValue = value != null ? undefined : defaultValue || ''; - const numIconsClass = getFormControlClassNameForIconCount({ - isInvalid, - isLoading, - isDropdown: true, - }); + const handleMouseUp = useCallback( + (e: React.MouseEvent) => { + // Normalizes cross-browser mouse eventing by preventing propagation, + // notably for use in conjunction with EuiOutsideClickDetector. + // See https://github.com/elastic/eui/pull/1926 for full discussion on + // rationale and alternatives should this intervention become problematic. + e.nativeEvent.stopImmediatePropagation(); + onMouseUp?.(e); + }, + [onMouseUp] + ); const classes = classNames( 'euiSelect', - numIconsClass, - { - 'euiSelect--fullWidth': fullWidth, - 'euiSelect--compressed': compressed, - 'euiSelect--inGroup': prepend || append, - 'euiSelect-isLoading': isLoading, - }, + { 'euiSelect-isLoading': isLoading }, className ); - let emptyOptionNode; - if (hasNoInitialSelection) { - emptyOptionNode = ( - - - - ); - } + const inGroup = !!(prepend || append); - // React HTML input can not have both value and defaultValue properties. - // https://reactjs.org/docs/uncontrolled-components.html#default-values - let selectDefaultValue; - if (value == null) { - selectDefaultValue = defaultValue || ''; - } + const styles = useEuiMemoizedStyles(euiSelectStyles); + const cssStyles = [ + styles.euiSelect, + compressed ? styles.compressed : styles.uncompressed, + fullWidth ? styles.fullWidth : styles.formWidth, + inGroup && styles.inGroup, + styles.lineHeight.removePadding, + inGroup + ? compressed + ? styles.lineHeight.inGroup.compressed + : styles.lineHeight.inGroup.uncompressed + : compressed + ? styles.lineHeight.compressed + : styles.lineHeight.uncompressed, + ]; return ( = (props) => { id={id} name={name} className={classes} + css={cssStyles} ref={inputRef} defaultValue={selectDefaultValue} value={value} @@ -163,7 +166,11 @@ export const EuiSelect: FunctionComponent = (props) => { disabled={disabled} {...rest} > - {emptyOptionNode} + {hasNoInitialSelection && ( + + + + )} {options.map((option, index) => { const { text, ...rest } = option; return ( diff --git a/packages/eui/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap b/packages/eui/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap index 1ed089c97e8..b430f7fa69b 100644 --- a/packages/eui/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap +++ b/packages/eui/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap @@ -16,7 +16,7 @@ exports[`EuiSuperSelect props compressed 1`] = ` > @@ -198,13 +198,13 @@ exports[`EuiSuperSelect renders 1`] = ` @@ -51,7 +51,7 @@ exports[`EuiSuperSelectControl props compressed is rendered 1`] = ` > Select an option @@ -311,7 +311,7 @@ exports[`EuiSuperSelectControl props value option is rendered 1`] = ` > Option #1 diff --git a/packages/eui/src/components/form/super_select/_index.scss b/packages/eui/src/components/form/super_select/_index.scss deleted file mode 100644 index ce308688e46..00000000000 --- a/packages/eui/src/components/form/super_select/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'super_select'; -@import 'super_select_control'; diff --git a/packages/eui/src/components/form/super_select/_super_select.scss b/packages/eui/src/components/form/super_select/_super_select.scss deleted file mode 100644 index 8d12623129b..00000000000 --- a/packages/eui/src/components/form/super_select/_super_select.scss +++ /dev/null @@ -1,16 +0,0 @@ -.euiSuperSelect__listbox { - @include euiScrollBar; - max-height: 300px; - overflow: hidden; - overflow-y: auto; -} - -.euiSuperSelect__item { - @include euiFontSizeS; - @include euiInteractiveStates; - padding: $euiSizeS; -} - -.euiSuperSelect__item--hasDividers:not(:last-of-type) { - border-bottom: $euiBorderThin; -} diff --git a/packages/eui/src/components/form/super_select/_super_select_control.scss b/packages/eui/src/components/form/super_select/_super_select_control.scss deleted file mode 100644 index c96bafd913f..00000000000 --- a/packages/eui/src/components/form/super_select/_super_select_control.scss +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 1. Ensure the descenders don't get cut off - * 2. Makes sure the height is correct when there's no selection - */ - -.euiSuperSelectControl { - @include euiFormControlStyle; - - display: block; /* 2 */ - text-align: left; - line-height: $euiFormControlHeight; /* 1 */ - padding-top: 0; /* 1 */ - padding-bottom: 0; /* 1 */ - // Truncate the text - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &-isInvalid { - @include euiFormControlInvalidStyle; - } - - &--compressed { - line-height: $euiFormControlCompressedHeight; /* 1 */ - padding-top: 0; /* 1 */ - padding-bottom: 0; /* 1 */ - } - - &__placeholder { - @include euiFormControlDisabledTextStyle; - } - - &.euiSuperSelect--isOpen__button { // since this is a button, we also want the visual indicator of active when options are shown - @include euiFormControlFocusStyle; - } -} diff --git a/packages/eui/src/components/form/super_select/index.ts b/packages/eui/src/components/form/super_select/index.ts index d043ad14022..eb2bc5d69c3 100644 --- a/packages/eui/src/components/form/super_select/index.ts +++ b/packages/eui/src/components/form/super_select/index.ts @@ -8,8 +8,7 @@ export type { EuiSuperSelectProps } from './super_select'; export { EuiSuperSelect } from './super_select'; -export type { - EuiSuperSelectControlProps, - EuiSuperSelectOption, -} from './super_select_control'; +export type { EuiSuperSelectOption } from './super_select_item'; + +export type { EuiSuperSelectControlProps } from './super_select_control'; export { EuiSuperSelectControl } from './super_select_control'; diff --git a/packages/eui/src/components/form/super_select/super_select.styles.ts b/packages/eui/src/components/form/super_select/super_select.styles.ts new file mode 100644 index 00000000000..dc6d9286afa --- /dev/null +++ b/packages/eui/src/components/form/super_select/super_select.styles.ts @@ -0,0 +1,104 @@ +/* + * 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 { + euiFontSize, + euiTextTruncate, + logicalCSS, + logicalCSSWithFallback, + logicalTextAlignCSS, +} from '../../../global_styling'; +import { euiFormControlStyles, euiFormVariables } from '../form.styles'; + +export const euiSuperSelectStyles = { + euiSuperSelect__listbox: css` + ${logicalCSS('max-height', '300px')} + ${logicalCSSWithFallback('overflow-y', 'auto')} + ${logicalCSSWithFallback('overflow-x', 'hidden')} + `, +}; + +export const euiSuperSelectItemStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + + return { + euiSuperSelect__item: css` + padding: ${euiTheme.size.s}; + ${euiFontSize(euiThemeContext, 's')} + `, + hasDividers: css` + &:not(:last-of-type) { + ${logicalCSS('border-bottom', euiTheme.border.thin)} + } + `, + }; +}; + +export const euiSuperSelectControlStyles = (euiThemeContext: UseEuiTheme) => { + const formStyles = euiFormControlStyles(euiThemeContext); + const formVariables = euiFormVariables(euiThemeContext); + + return { + euiSuperSelect__control: css` + ${formStyles.shared} + display: block; /* Makes sure the height is correct when there's no selection */ + ${logicalTextAlignCSS('left')} + ${euiTextTruncate( + '' // maxWidth is already set by width modifiers below + )} + + &:focus { + ${formStyles.focus} + } + `, + // Since the control is a button and not an actual input, we have to set + // certain state styles manually instead of relying on CSS selectors + open: css` + /* Also show focus indicator when the dropdown is open */ + ${formStyles.focus} + `, + invalid: css` + ${formStyles.invalid} + `, + disabled: css` + ${formStyles.disabled} + `, + readOnly: css` + ${formStyles.readOnly} + `, + + // Skip the css() on the default height to avoid generating a className + uncompressed: ` + ${formStyles.uncompressed} + /* Match line height with inputs */ + ${logicalCSS('padding-vertical', 0)} + line-height: ${formVariables.controlHeight}; + `, + compressed: css` + ${formStyles.compressed} + /* Match line height with inputs */ + ${logicalCSS('padding-vertical', 0)} + line-height: ${formVariables.controlCompressedHeight}; + `, + + // 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), + + // Children + euiSuperSelect__placeholder: css` + color: ${formVariables.controlDisabledColor}; + `, + }; +}; diff --git a/packages/eui/src/components/form/super_select/super_select.tsx b/packages/eui/src/components/form/super_select/super_select.tsx index 5e609b5f970..53f010feddb 100644 --- a/packages/eui/src/components/form/super_select/super_select.tsx +++ b/packages/eui/src/components/form/super_select/super_select.tsx @@ -9,22 +9,22 @@ import React, { Component, FocusEvent, ReactNode, createRef } from 'react'; import classNames from 'classnames'; +import { htmlIdGenerator, keys } from '../../../services'; import { CommonProps } from '../../common'; - +import { EuiI18n } from '../../i18n'; import { EuiScreenReaderOnly } from '../../accessibility'; -import { htmlIdGenerator, keys } from '../../../services'; +import { EuiInputPopover, type EuiInputPopoverProps } from '../../popover'; +import { type EuiContextMenuItemLayoutAlignment } from '../../context_menu'; + import { EuiSuperSelectControl, - EuiSuperSelectControlProps, - EuiSuperSelectOption, + type EuiSuperSelectControlProps, } from './super_select_control'; -import { EuiInputPopover, EuiInputPopoverProps } from '../../popover'; import { - EuiContextMenuItem, - EuiContextMenuItemLayoutAlignment, -} from '../../context_menu'; - -import { EuiI18n } from '../../i18n'; + EuiSuperSelectItem, + type EuiSuperSelectOption, +} from './super_select_item'; +import { euiSuperSelectStyles as styles } from './super_select.styles'; enum ShiftDirection { BACK = 'back', @@ -283,21 +283,6 @@ export class EuiSuperSelect extends Component< popoverProps?.className ); - const buttonClasses = classNames( - { - 'euiSuperSelect--isOpen__button': this.state.isPopoverOpen, - }, - className - ); - - const itemClasses = classNames( - 'euiSuperSelect__item', - { - 'euiSuperSelect__item--hasDividers': hasDividers, - }, - itemClassName - ); - const button = ( extends Component< this.state.isPopoverOpen ? this.closePopover : this.openPopover } onKeyDown={this.onSelectKeyDown} - className={buttonClasses} + className={className} fullWidth={fullWidth} isInvalid={isInvalid} compressed={compressed} {...rest} buttonRef={this.controlButtonRef} + isDropdownOpen={this.state.isPopoverOpen} /> ); @@ -321,21 +307,21 @@ export class EuiSuperSelect extends Component< if (value == null) return; return ( - this.itemClicked(value)} onKeyDown={this.onItemKeyDown} - layoutAlign={itemLayoutAlign} buttonRef={(node) => this.setItemNode(node, index)} - role="option" - id={String(value)} aria-selected={valueOfSelected === value} {...optionRest} > {dropdownDisplay || inputDisplay} - + ); }); @@ -364,7 +350,8 @@ export class EuiSuperSelect extends Component< { ); const control = container.querySelector('.euiSuperSelectControl'); - expect(control).toHaveClass('euiSuperSelectControl--fullWidth'); + expect(control!.className).toContain('fullWidth'); }); }); diff --git a/packages/eui/src/components/form/super_select/super_select_control.tsx b/packages/eui/src/components/form/super_select/super_select_control.tsx index 83bb39f40e6..ca5ab20c5f8 100644 --- a/packages/eui/src/components/form/super_select/super_select_control.tsx +++ b/packages/eui/src/components/form/super_select/super_select_control.tsx @@ -17,6 +17,7 @@ import React, { } from 'react'; import classNames from 'classnames'; +import { useEuiMemoizedStyles } from '../../../services'; import { CommonProps } from '../../common'; import { EuiScreenReaderOnly } from '../../accessibility'; @@ -24,16 +25,10 @@ import { EuiFormControlLayout, EuiFormControlLayoutProps, } from '../form_control_layout'; -import { getFormControlClassNameForIconCount } from '../form_control_layout/_num_icons'; import { useFormContext } from '../eui_form_context'; -export interface EuiSuperSelectOption { - value: NonNullable; - inputDisplay?: ReactNode; - dropdownDisplay?: ReactNode; - disabled?: boolean; - 'data-test-subj'?: string; -} +import { type EuiSuperSelectOption } from './super_select_item'; +import { euiSuperSelectControlStyles } from './super_select.styles'; export interface EuiSuperSelectControlProps extends CommonProps, @@ -79,7 +74,7 @@ export interface EuiSuperSelectControlProps } export const EuiSuperSelectControl: ( - props: EuiSuperSelectControlProps + props: EuiSuperSelectControlProps & { isDropdownOpen?: boolean } ) => ReturnType>> = (props) => { const { defaultFullWidth } = useFormContext(); const { @@ -91,6 +86,7 @@ export const EuiSuperSelectControl: ( fullWidth = defaultFullWidth, isLoading = false, isInvalid = false, + isDropdownOpen, readOnly, defaultValue, compressed = false, @@ -101,25 +97,28 @@ export const EuiSuperSelectControl: ( disabled, ...rest } = props; - const numIconsClass = getFormControlClassNameForIconCount({ - isInvalid, - isLoading, - isDropdown: true, - }); const classes = classNames( 'euiSuperSelectControl', - numIconsClass, { - 'euiSuperSelectControl--fullWidth': fullWidth, - 'euiSuperSelectControl--compressed': compressed, - 'euiSuperSelectControl--inGroup': prepend || append, 'euiSuperSelectControl-isLoading': isLoading, 'euiSuperSelectControl-isInvalid': isInvalid, }, className ); + const styles = useEuiMemoizedStyles(euiSuperSelectControlStyles); + const cssStyles = [ + styles.euiSuperSelect__control, + compressed ? styles.compressed : styles.uncompressed, + fullWidth ? styles.fullWidth : styles.formWidth, + (prepend || append) && styles.inGroup, + isDropdownOpen && styles.open, + isInvalid && styles.invalid, + readOnly && styles.readOnly, + disabled && !readOnly && styles.disabled, + ]; + const inputValue = value != null ? value : defaultValue; const selectedValue = useMemo(() => { @@ -176,17 +175,19 @@ export const EuiSuperSelectControl: ( {showPlaceholder ? ( - + {placeholder} ) : ( diff --git a/packages/eui/src/components/form/super_select/super_select_item.tsx b/packages/eui/src/components/form/super_select/super_select_item.tsx new file mode 100644 index 00000000000..dcb4b2ae3e8 --- /dev/null +++ b/packages/eui/src/components/form/super_select/super_select_item.tsx @@ -0,0 +1,55 @@ +/* + * 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 React, { FunctionComponent, ComponentProps, ReactNode } from 'react'; +import classNames from 'classnames'; + +import { useEuiMemoizedStyles } from '../../../services'; +import { EuiContextMenuItem } from '../../context_menu'; +import { euiSuperSelectItemStyles } from './super_select.styles'; + +// Type exposed to consumers via API +export interface EuiSuperSelectOption { + value: NonNullable; + inputDisplay?: ReactNode; + dropdownDisplay?: ReactNode; + disabled?: boolean; + 'data-test-subj'?: string; +} + +// Actual props used by below component, transmogged by parent EuiSuperSelect +// from consumer props to internal EUI props +type EuiSuperSelectItemProps = ComponentProps & { + hasDividers?: boolean; +}; + +// Internal subcomponent util, primarily for easier usage of hooks +export const EuiSuperSelectItem: FunctionComponent = ({ + children, + className, + hasDividers, + ...rest +}) => { + const classes = classNames('euiSuperSelect__item', className); + const styles = useEuiMemoizedStyles(euiSuperSelectItemStyles); + const cssStyles = [ + styles.euiSuperSelect__item, + hasDividers && styles.hasDividers, + ]; + + return ( + + {children} + + ); +}; diff --git a/packages/eui/src/components/form/text_area/__snapshots__/text_area.test.tsx.snap b/packages/eui/src/components/form/text_area/__snapshots__/text_area.test.tsx.snap index 222e43c6551..6f40f228304 100644 --- a/packages/eui/src/components/form/text_area/__snapshots__/text_area.test.tsx.snap +++ b/packages/eui/src/components/form/text_area/__snapshots__/text_area.test.tsx.snap @@ -2,14 +2,14 @@ exports[`EuiTextArea is rendered 1`] = ` diff --git a/packages/eui/src/components/form/text_area/_index.scss b/packages/eui/src/components/form/text_area/_index.scss deleted file mode 100644 index f14b8766066..00000000000 --- a/packages/eui/src/components/form/text_area/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'text_area'; diff --git a/packages/eui/src/components/form/text_area/_text_area.scss b/packages/eui/src/components/form/text_area/_text_area.scss deleted file mode 100644 index 3122fd44140..00000000000 --- a/packages/eui/src/components/form/text_area/_text_area.scss +++ /dev/null @@ -1,39 +0,0 @@ -.euiTextArea { - @include euiFormControlStyle; - line-height: $euiLineHeight; // give more spacing between multiple lines - - // s default to `inline-block`, which causes an extra 2-3px of - // unnecessary height within its parent `block` form control wrapper. - // @see https://stackoverflow.com/a/27536461/4294462 - display: block; - - // Textareas have their own sizing - &, - &--compressed { - height: auto; - } -} - -.euiFormControlLayout--euiTextArea { - height: auto; - - .euiFormControlLayoutIcons { - top: auto; - bottom: $euiFormControlPadding; - } -} - -// resize modifiers -$textareaResizing: ( - vertical: 'resizeVertical', - horizontal: 'resizeHorizontal', - both: 'resizeBoth', - none: 'resizeNone', -); - -// Create button modifiers based upon the map. -@each $direction, $modifier in $textareaResizing { - .euiTextArea--#{$modifier} { - resize: #{$direction}; - } -} diff --git a/packages/eui/src/components/form/text_area/text_area.styles.ts b/packages/eui/src/components/form/text_area/text_area.styles.ts new file mode 100644 index 00000000000..80ed465ba9d --- /dev/null +++ b/packages/eui/src/components/form/text_area/text_area.styles.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { css } from '@emotion/react'; + +import { UseEuiTheme } from '../../../services'; +import { logicalCSS } from '../../../global_styling'; +import { euiFormControlStyles, euiFormVariables } from '../form.styles'; + +export const euiTextAreaStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + const formStyles = euiFormControlStyles(euiThemeContext); + const formVars = euiFormVariables(euiThemeContext); + + return { + euiTextArea: css` + ${formStyles.shared} + + /* Give more spacing between multiple lines */ + line-height: ${euiTheme.font.lineHeightMultiplier}; + + /* s default to 'inline-block', which causes an extra 2-3px of + * unnecessary height within its parent 'block' form control wrapper. + * @see https://stackoverflow.com/a/27536461/4294462 */ + display: block; + + &:invalid { + ${formStyles.invalid} + } + + &:focus { + ${formStyles.focus} + } + + &:disabled { + ${formStyles.disabled} + } + + &[readOnly] { + ${formStyles.readOnly} + } + + &:autofill { + ${formStyles.autoFill} + } + `, + resize: { + vertical: css` + resize: vertical; + `, + horizontal: css` + resize: horizontal; + `, + both: css` + resize: both; + `, + none: css` + resize: none; + `, + }, + + // Skip the css() on the default height to avoid generating a className + uncompressed: ` + ${logicalCSS('height', 'auto')} + padding: ${formVars.controlPadding}; + border-radius: ${formVars.controlBorderRadius}; + `, + compressed: css` + ${logicalCSS('height', 'auto')} + padding: ${formVars.controlCompressedPadding}; + border-radius: ${formVars.controlCompressedBorderRadius}; + `, + + // Skip the css() on the default width to avoid generating a className + formWidth: formStyles.formWidth, + fullWidth: css(formStyles.fullWidth), + + // EuiFormControlLayout styles + formControlLayout: { + euiTextArea: css` + /* TODO: Remove extra && specificity override once EuiFormControlLayout is converted to Emotion */ + && { + ${logicalCSS('height', 'auto')} + } + + .euiFormControlLayoutIcons { + ${logicalCSS('top', 'auto')} + ${logicalCSS('bottom', formVars.controlPadding)} + } + `, + }, + }; +}; diff --git a/packages/eui/src/components/form/text_area/text_area.test.tsx b/packages/eui/src/components/form/text_area/text_area.test.tsx index 52caf44128b..2c45685da2d 100644 --- a/packages/eui/src/components/form/text_area/text_area.test.tsx +++ b/packages/eui/src/components/form/text_area/text_area.test.tsx @@ -29,7 +29,7 @@ describe('EuiTextArea', () => { ); const element = container.querySelector('.euiTextArea'); - expect(element).toHaveClass('euiTextArea--fullWidth'); + expect(element!.className).toContain('fullWidth'); }); }); }); diff --git a/packages/eui/src/components/form/text_area/text_area.tsx b/packages/eui/src/components/form/text_area/text_area.tsx index fb1e95074c2..42bed911394 100644 --- a/packages/eui/src/components/form/text_area/text_area.tsx +++ b/packages/eui/src/components/form/text_area/text_area.tsx @@ -16,13 +16,17 @@ import React, { import classNames from 'classnames'; import { CommonProps } from '../../common'; -import { useCombinedRefs } from '../../../services'; +import { useCombinedRefs, useEuiMemoizedStyles } from '../../../services'; import { EuiFormControlLayout } from '../form_control_layout'; import { EuiValidatableControl } from '../validatable_control'; import { useFormContext } from '../eui_form_context'; import { EuiFormControlLayoutIconsProps } from '../form_control_layout/form_control_layout_icons'; +import { euiTextAreaStyles } from './text_area.styles'; + +export const RESIZE = ['vertical', 'horizontal', 'both', 'none'] as const; + export type EuiTextAreaProps = TextareaHTMLAttributes & CommonProps & { icon?: EuiFormControlLayoutIconsProps['icon']; @@ -44,20 +48,11 @@ export type EuiTextAreaProps = TextareaHTMLAttributes & * Which direction, if at all, should the textarea resize * @default vertical */ - resize?: keyof typeof resizeToClassNameMap; + resize?: (typeof RESIZE)[number]; inputRef?: Ref; }; -const resizeToClassNameMap = { - vertical: 'euiTextArea--resizeVertical', - horizontal: 'euiTextArea--resizeHorizontal', - both: 'euiTextArea--resizeBoth', - none: 'euiTextArea--resizeNone', -}; - -export const RESIZE = Object.keys(resizeToClassNameMap); - export const EuiTextArea: FunctionComponent = (props) => { const { defaultFullWidth } = useFormContext(); const { @@ -78,25 +73,15 @@ export const EuiTextArea: FunctionComponent = (props) => { ...rest } = props; - const classes = classNames( - 'euiTextArea', - resizeToClassNameMap[resize], - { - 'euiTextArea--fullWidth': fullWidth, - 'euiTextArea--compressed': compressed, - }, - className - ); - - let definedRows: number; + const classes = classNames('euiTextArea', className); - if (rows) { - definedRows = rows; - } else if (compressed) { - definedRows = 3; - } else { - definedRows = 6; - } + const styles = useEuiMemoizedStyles(euiTextAreaStyles); + const cssStyles = [ + styles.euiTextArea, + styles.resize[resize], + compressed ? styles.compressed : styles.uncompressed, + fullWidth ? styles.fullWidth : styles.formWidth, + ]; const ref = useRef(null); const refs = useCombinedRefs([ref, inputRef]); @@ -137,12 +122,14 @@ export const EuiTextArea: FunctionComponent = (props) => { clear={clear} icon={icon} className="euiFormControlLayout--euiTextArea" + css={styles.formControlLayout.euiTextArea} >