diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png new file mode 100644 index 00000000000..7005743689c Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png new file mode 100644 index 00000000000..c5ad244f78e Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormControlLayout_Append_Prepend.png differ diff --git a/packages/eui/changelogs/upcoming/7954.md b/packages/eui/changelogs/upcoming/7954.md new file mode 100644 index 00000000000..9d27209a613 --- /dev/null +++ b/packages/eui/changelogs/upcoming/7954.md @@ -0,0 +1,4 @@ +**CSS-in-JS conversions** + +- Converted `EuiFormControlLayout` to Emotion + - Removed `.euiFormControlLayout--*icons` classNames and `--eui-form-control-layout-icons-padding` CSS var. Use `--euiFormControlRightIconsCount` or `--euiFormControlLeftIconsCount` instead 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 7d6a0f25ffe..b8b6cb24abe 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 @@ -5,10 +5,10 @@ exports[`EuiColorPicker color empty string 1`] = ` class="euiPopover euiColorPicker__popoverAnchor emotion-euiPopover-block" >
- + +
- + +
`; @@ -625,10 +633,10 @@ exports[`EuiColorPicker readOnly 1`] = ` class="euiPopover euiColorPicker__popoverAnchor emotion-euiPopover-block" >
+ +
- + +
- + +
{ it('renders as readonly', () => { cy.realMount(); - cy.get('.euiFormControlLayout--readOnly').should('exist'); + cy.get('.euiFormControlLayout-readOnly').should('exist'); cy.realPress('Tab'); cy.focused().should('not.exist'); diff --git a/packages/eui/src/components/date_picker/date_picker.test.tsx b/packages/eui/src/components/date_picker/date_picker.test.tsx index 58f27533c9d..c8a969a8edc 100644 --- a/packages/eui/src/components/date_picker/date_picker.test.tsx +++ b/packages/eui/src/components/date_picker/date_picker.test.tsx @@ -50,7 +50,7 @@ describe('EuiDatePicker', () => { test('compressed', () => { const { container } = render(); // TODO: Should probably be a visual snapshot test - expect(container.innerHTML).toContain('--compressed'); + expect(container.innerHTML).toContain('-compressed'); }); // TODO: These tests/snapshots don't really do anything in Jest without diff --git a/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap b/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap index 14166d5664d..293aae6dc8a 100644 --- a/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap +++ b/packages/eui/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap @@ -6,37 +6,41 @@ exports[`EuiSuperDatePicker props accepts data-test-subj and passes to EuiFormCo data-test-subj="mySuperDatePicker" >
- + +
+ +
+ +
- + +
- + +
+ +
- + +
+ +
+ +
+ +
+ +
+ +
@@ -16,10 +16,10 @@ exports[`EuiFormControlLayout is rendered 1`] = ` exports[`EuiFormControlLayout props clear onClick is rendered 1`] = `
`; exports[`EuiFormControlLayout props icon is rendered as a string 1`] = `
- - 1 - - - 2 - + + 1 + + + 2 + +
`; exports[`EuiFormControlLayout props multiple prepends are rendered 1`] = `
- - 1 - - - 2 - + + 1 + + + 2 + +
`; exports[`EuiFormControlLayout props one append node is rendered 1`] = `
- - 1 - + + 1 + +
`; exports[`EuiFormControlLayout props one append string is rendered 1`] = `
- + +
`; exports[`EuiFormControlLayout props one prepend node is rendered 1`] = `
- - 1 - + + 1 + +
`; exports[`EuiFormControlLayout props one prepend node is rendered with className 1`] = `
- - 1 - + + 1 + +
`; exports[`EuiFormControlLayout props one prepend string is rendered 1`] = `
- + +
`; exports[`EuiFormControlLayout props readOnly is rendered 1`] = `
`; diff --git a/packages/eui/src/components/form/form_control_layout/__snapshots__/form_control_layout_delimited.test.tsx.snap b/packages/eui/src/components/form/form_control_layout/__snapshots__/form_control_layout_delimited.test.tsx.snap index 043baa4c241..ad93640f2b3 100644 --- a/packages/eui/src/components/form/form_control_layout/__snapshots__/form_control_layout_delimited.test.tsx.snap +++ b/packages/eui/src/components/form/form_control_layout/__snapshots__/form_control_layout_delimited.test.tsx.snap @@ -3,11 +3,11 @@ exports[`EuiFormControlLayoutDelimited is rendered 1`] = `
*, - .euiButtonEmpty, - .euiText, - .euiFormLabel, - .euiButtonIcon { - height: 100%; - } - - .euiFormControlLayout__childrenWrapper { - flex-grow: 1; - overflow: hidden; /* 2 */ - } - - .euiFormControlLayout__prepend, - .euiFormControlLayout__append { - @include euiTextTruncate; - flex-shrink: 0; - height: 100%; - border-radius: 0; - - // ICONS - - &.euiIcon, - .euiIcon { - padding: 0 $euiSizeS; - width: $euiSizeXL; - border-radius: 0; - background-color: $euiFormInputGroupLabelBackground; - } - - &.euiButtonIcon, - &.euiButtonEmpty, - .euiButtonIcon, - .euiButtonEmpty { - transform: none !important; // stylelint-disable-line declaration-no-important - - // Undo sizing from icons inside buttons - .euiIcon { - background: none !important; // stylelint-disable-line declaration-no-important - padding: 0; - width: $euiSize; - } - } - } - - .euiButtonIcon { - padding: 0 $euiSizeS; - width: $euiSizeXL; - border-radius: 0; - - &:not(:focus) { - background-color: $euiFormInputGroupLabelBackground; - } - - &:focus-visible { - outline: 2px solid $euiFocusRingColor; - outline-offset: -2px; - } - } - - .euiToolTipAnchor > .euiIcon { - height: 100%; - background-color: $euiFormInputGroupLabelBackground; - padding: 0 $euiSizeS; - width: $euiSizeXL; - border-radius: 0; - } - - > .euiFormControlLayout__prepend, - > .euiFormControlLayout__append { - max-width: 50%; // Make sure max-width only applies to the outer most append/prepend element - } - - // stylelint-disable declaration-no-important - // This is the only way to target specific components to override styling - - // TEXT - - .euiFormLabel, - .euiText:not(.euiFormControlLayoutDelimited__delimiter) { - background-color: $euiFormInputGroupLabelBackground; - padding: $euiFormControlPadding; - line-height: $euiSize !important; - cursor: default !important; // pointer cursor on some form labels but not others is confusing - - // If the next sibling is not the input, pull it closer to the text to reduce space - + *:not(.euiFormControlLayout__childrenWrapper):not(input) { - margin-left: -$euiFormControlPadding; - } - } - - // If any child that is not the input has a next sibling that is text, pull it closer to the text to reduce space - > *:not(.euiFormControlLayout__childrenWrapper) { - + .euiFormLabel, - + .euiText { - margin-left: -$euiFormControlPadding; - } - } - - // BORDERS on buttons only - - .euiButtonEmpty { - border-right: $euiFormInputGroupBorder; - } - - // Any buttons after the children wrapper or inside any elements after the children wrapper - // Need to swap border sides - .euiFormControlLayout__childrenWrapper ~ .euiButtonEmpty, - .euiFormControlLayout__childrenWrapper ~ * .euiButtonEmpty { - border-right: none; - border-left: $euiFormInputGroupBorder; - } - - // Compressed alterations - - &.euiFormControlLayout--compressed { - @include euiFormControlDefaultShadow($borderOnly: true); - border-radius: $euiBorderRadius / 2; - overflow: hidden; // Keeps backgrounds inside border radius - - // Padding - .euiFormLabel, - .euiText:not(.euiFormControlLayoutDelimited__delimiter) { - padding: $euiFormControlCompressedPadding; - - // If the next sibling is not the input, pull it closer to the text to reduce space - + *:not(.euiFormControlLayout__childrenWrapper) { - margin-left: -$euiFormControlCompressedPadding; - } - } - - // If any child that is not the input has a next sibling that is text, pull it closer to the text to reduce space - > *:not(.euiFormControlLayout__childrenWrapper) { - + .euiFormLabel, - + .euiText { - margin-left: -$euiFormControlCompressedPadding; - } - } - } - - // ReadOnly alterations - &.euiFormControlLayout--readOnly { - cursor: default; - background: $euiFormInputGroupLabelBackground; - border-color: transparent; - box-shadow: inset 0 0 0 1px $euiFormInputGroupLabelBackground; - - input { - background-color: $euiFormBackgroundReadOnlyColor; - } - } -} diff --git a/packages/eui/src/components/form/form_control_layout/_form_control_layout_delimited.scss b/packages/eui/src/components/form/form_control_layout/_form_control_layout_delimited.scss index fd90253a29b..e48ffb67185 100644 --- a/packages/eui/src/components/form/form_control_layout/_form_control_layout_delimited.scss +++ b/packages/eui/src/components/form/form_control_layout/_form_control_layout_delimited.scss @@ -13,14 +13,14 @@ } // Target when the euiFormControlLayout is compressed without specifying the full class name in case it ever changes - &[class*='--compressed'] { + &[class*='-compressed'] { @include euiFormControlDefaultShadow($borderOnly: true); border-radius: $euiBorderRadius / 2; } // Target when the euiFormControlLayout is fullWidth without specifying the full class name in case it ever changes - &[class*='--fullWidth'] .euiFormControlLayout__childrenWrapper, - &[class*='--fullWidth'] .euiFormControlLayout__childrenWrapper > *:not(.euiFormControlLayoutDelimited__delimiter):not(.euiFormControlLayoutIcons) { + &[class*='-fullWidth'] .euiFormControlLayout__childrenWrapper, + &[class*='-fullWidth'] .euiFormControlLayout__childrenWrapper > *:not(.euiFormControlLayoutDelimited__delimiter):not(.euiFormControlLayoutIcons) { width: 100%; max-width: none; } @@ -35,7 +35,7 @@ } // Target when the euiFormControlLayout is readOnly without specifying the full class name in case it ever changes - &[class*='--readOnly'] { + &[class*='-readOnly'] { @include euiFormControlReadOnlyStyle; .euiFormControlLayout__childrenWrapper { diff --git a/packages/eui/src/components/form/form_control_layout/_form_control_layout_icons.scss b/packages/eui/src/components/form/form_control_layout/_form_control_layout_icons.scss index 2ea455d2212..bad4faf714e 100644 --- a/packages/eui/src/components/form/form_control_layout/_form_control_layout_icons.scss +++ b/packages/eui/src/components/form/form_control_layout/_form_control_layout_icons.scss @@ -13,7 +13,7 @@ bottom: 0; left: $euiFormControlPadding; - .euiFormControlLayout--compressed & { + [class*='euiFormControlLayout-compressed'] & { left: $euiFormControlCompressedPadding; } } @@ -27,7 +27,7 @@ flex-grow: 0; padding-inline: $euiFormControlPadding; - .euiFormControlLayout--compressed & { + [class*='euiFormControlLayout-compressed'] & { padding-inline: $euiFormControlCompressedPadding; } } @@ -41,7 +41,7 @@ left: auto; right: $euiFormControlPadding; - .euiFormControlLayout--compressed & { + [class*='euiFormControlLayout-compressed'] & { left: auto; right: $euiFormControlCompressedPadding; } diff --git a/packages/eui/src/components/form/form_control_layout/_index.scss b/packages/eui/src/components/form/form_control_layout/_index.scss index 338d1559451..f5bad883226 100644 --- a/packages/eui/src/components/form/form_control_layout/_index.scss +++ b/packages/eui/src/components/form/form_control_layout/_index.scss @@ -1,4 +1,3 @@ -@import 'form_control_layout'; @import 'form_control_layout_delimited'; @import 'form_control_layout_icons'; @import 'form_control_layout_clear_button'; 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 94ead8a1dd7..491dd665eec 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 @@ -6,11 +6,7 @@ * Side Public License, v 1. */ -import { - getFormControlClassNameForIconCount, - isRightSideIcon, - getIconAffordanceStyles, -} from './_num_icons'; +import { isRightSideIcon, getIconAffordanceStyles } from './_num_icons'; describe('getIconAffordanceStyles', () => { const noIcons = { @@ -71,46 +67,6 @@ describe('getIconAffordanceStyles', () => { }); }); -describe('getFormControlClassNameForIconCount', () => { - it('should return undefined if object is empty', () => { - const numberClass = getFormControlClassNameForIconCount({}); - expect(numberClass).toEqual(undefined); - }); - - it('should return undefined if all are false', () => { - const numberClass = getFormControlClassNameForIconCount({ - icon: false, - clear: false, - isLoading: false, - isInvalid: false, - isDropdown: false, - }); - expect(numberClass).toEqual(undefined); - }); - - it('should return 2 if 2 are true', () => { - const numberClass = getFormControlClassNameForIconCount({ - icon: false, - clear: true, - isLoading: true, - isInvalid: false, - isDropdown: false, - }); - expect(numberClass).toEqual('euiFormControlLayout--2icons'); - }); - - it('should return 5 if all are true', () => { - const numberClass = getFormControlClassNameForIconCount({ - icon: true, - clear: true, - isLoading: true, - isInvalid: true, - isDropdown: true, - }); - expect(numberClass).toEqual('euiFormControlLayout--5icons'); - }); -}); - describe('isRightSideIcon', () => { it('returns true if side has been set to right', () => { expect(isRightSideIcon({ side: 'right', type: 'warning' })).toEqual(true); 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 1dad9f03c73..d51c8dc2f4f 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 @@ -11,41 +11,6 @@ import { type EuiFormControlLayoutIconsProps, } from './form_control_layout_icons'; -/** - * The `getFormControlClassNameForIconCount` function helps setup the className appendum - * depending on the form control's current settings/state. - * - * @param icon {boolean} Does it contain a static icon like arrowDown - * @param clear {boolean} Is it currently clearable - * @param isLoading {boolean} Is is currently loading - * @param isInvalid {boolean} It is currently invalid - * @param isDropdown {boolean} It is as dropdown - * @returns {string | undefined} Returns the string to append to the base className of the form control; or `undefined` if all evaluate to false - */ - -export type _EuiFormControlLayoutNumIcons = { - icon?: boolean; - clear?: boolean; - isLoading?: boolean; - isInvalid?: boolean; - isDropdown?: boolean; -}; - -export const getFormControlClassNameForIconCount = ({ - icon, - clear, - isLoading, - isInvalid, - isDropdown, -}: _EuiFormControlLayoutNumIcons): string | undefined => { - const numIcons = [icon, clear, isInvalid, isLoading, isDropdown].filter( - (item) => item === true - ).length; - - // This className is also specifically used in `src/global_styling/mixins/_form.scss` - return numIcons > 0 ? `euiFormControlLayout--${numIcons}icons` : undefined; -}; - export const isRightSideIcon = ( icon?: EuiFormControlLayoutIconsProps['icon'] ): boolean => { diff --git a/packages/eui/src/components/form/form_control_layout/form_control_layout.stories.tsx b/packages/eui/src/components/form/form_control_layout/form_control_layout.stories.tsx index 5a1b5102e4e..acb511250a1 100644 --- a/packages/eui/src/components/form/form_control_layout/form_control_layout.stories.tsx +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout.stories.tsx @@ -11,8 +11,14 @@ import type { Meta, StoryObj } from '@storybook/react'; import { hideStorybookControls } from '../../../../.storybook/utils'; -import { EuiIcon } from '../../icon'; +import { useIsWithinMinBreakpoint } from '../../../services'; +import { EuiForm } from '../form'; import { EuiFieldText } from '../field_text'; +import { EuiIcon } from '../../icon'; +import { EuiIconTip, EuiToolTip } from '../../tool_tip'; +import { EuiPopover } from '../../popover'; +import { EuiButtonEmpty, EuiButtonIcon } from '../../button'; +import { EuiText } from '../../text'; import { EuiFormControlLayout, @@ -62,10 +68,19 @@ export default meta; type Story = StoryObj; export const Playground: Story = { - args: { - children: ( - - ), + // Several props need to be manually applied to the child EuiFieldText as well to render correctly + render: ({ children, ...args }) => { + const { readOnly, isDisabled, fullWidth, compressed } = args; + const childProps = { readOnly, isDisabled, fullWidth, compressed }; + return ( + + + + ); }, }; @@ -91,3 +106,153 @@ export const IconShape: Story = { }, }, }; + +export const AppendPrepend: Story = { + tags: ['vrt-only'], + render: function Render() { + const isDesktop = useIsWithinMinBreakpoint('xl'); + return ( + + + Tooltip + + } + /> + + Popover + + } + closePopover={() => {}} + /> + } + append={ + + Tooltip + + } + /> + + + + } + append={ + + Tooltip + + } + /> + } + append={} + /> + , + , + ]} + append={[ + } + closePopover={() => {}} + />, + , + ]} + /> + + } + closePopover={() => {}} + /> + } + prepend={ + + + + } + /> + , 'String']} + append={[ + 'String', + , + ]} + /> + + + , + ]} + append={[ + } + closePopover={() => {}} + />, + 'String', + ]} + /> + , + ]} + /> + } + closePopover={() => {}} + />, + ]} + /> + } + closePopover={() => {}} + /> + } + append="String" + /> + + ); + }, +}; diff --git a/packages/eui/src/components/form/form_control_layout/form_control_layout.styles.ts b/packages/eui/src/components/form/form_control_layout/form_control_layout.styles.ts new file mode 100644 index 00000000000..dbe9ae52c5c --- /dev/null +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout.styles.ts @@ -0,0 +1,184 @@ +/* + * 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 { + euiTextTruncate, + logicalCSS, + mathWithUnits, +} from '../../../global_styling'; + +import { euiFormControlDefaultShadow, euiFormVariables } from '../form.styles'; + +export const euiFormControlLayoutStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + const form = euiFormVariables(euiThemeContext); + + return { + euiFormControlLayout: css``, + // Skip the css`` on the default height to avoid generating a className + uncompressed: ` + ${logicalCSS('height', form.controlHeight)} + `, + compressed: css` + ${logicalCSS('height', form.controlCompressedHeight)} + `, + // Skip the css`` on the default width to avoid generating a className + formWidth: ` + ${logicalCSS('max-width', form.maxWidth)} + ${logicalCSS('width', '100%')} + `, + fullWidth: css` + ${logicalCSS('max-width', '100%')} + ${logicalCSS('width', '100%')} + `, + + group: { + group: css` + display: flex; + align-items: stretch; + + /* Account for inner box-shadow style border */ + padding: ${euiTheme.border.width.thin}; + ${euiFormControlDefaultShadow(euiThemeContext, { + withBackground: false, + })} + background-color: ${form.backgroundColor}; + /* Keep backgrounds inside border radius */ + overflow: hidden; + + /* Force the stretch of any children so they expand the full height of the control */ + > * { + ${logicalCSS('height', '100%')} + } + `, + // Skipping css`` to avoid repeated compressed/uncompressed classNames + uncompressed: ` + border-radius: ${form.controlBorderRadius}; + `, + compressed: ` + border-radius: ${form.controlCompressedBorderRadius}; + `, + }, + + children: { + euiFormControlLayout__childrenWrapper: css` + position: relative; + `, + inGroup: css` + flex-grow: 1; + overflow: hidden; /* Ensure truncation works in children elements */ + `, + prependOnly: css` + ${logicalCSS('border-top-right-radius', 'inherit')} + ${logicalCSS('border-bottom-right-radius', 'inherit')} + `, + appendOnly: css` + ${logicalCSS('border-top-left-radius', 'inherit')} + ${logicalCSS('border-bottom-left-radius', 'inherit')} + `, + }, + }; +}; + +export const euiFormControlLayoutSideNodeStyles = ( + euiThemeContext: UseEuiTheme +) => { + const { euiTheme } = euiThemeContext; + const form = euiFormVariables(euiThemeContext); + + const uncompressedHeight = mathWithUnits( + [form.controlHeight, euiTheme.border.width.thin], + (x, y) => x - y * 2 + ); + const compressedHeight = mathWithUnits( + [form.controlCompressedHeight, euiTheme.border.width.thin], + (x, y) => x - y * 2 + ); + + const buttons = '*:is(.euiButton, .euiButtonEmpty, .euiButtonIcon)'; + const text = '*:is(.euiFormLabel, .euiText)'; + + return { + euiFormControlLayout__side: css` + ${logicalCSS('height', '100%')} + ${euiTextTruncate('50%')} + flex-shrink: 0; + display: flex; + align-items: center; + gap: ${euiTheme.size.xs}; + background-color: ${form.appendPrependBackground}; + + /* Overrides */ + + ${buttons} { + /* Override button hover/active transform */ + transform: none !important; /* stylelint-disable-line declaration-no-important */ + + /* Account for border around focusable children */ + &:focus-visible { + outline-offset: -${euiTheme.focus.width}; + } + } + + ${text} { + /* Override .euiFormLabel CSS */ + cursor: default !important; /* stylelint-disable-line declaration-no-important */ + } + + /* Account for button padding when spacing children */ + /* Second > * selector accounts for buttons inside popover & tooltip wrappers */ + + &:not(:has(> ${buttons}:first-child, > *:first-child > ${buttons})) { + ${logicalCSS('padding-left', euiTheme.size.s)} + } + + &:not(:has(> ${buttons}:last-child, > *:last-child > ${buttons})) { + ${logicalCSS('padding-right', euiTheme.size.s)} + } + `, + append: css` + ${logicalCSS('border-top-right-radius', 'inherit')} + ${logicalCSS('border-bottom-right-radius', 'inherit')} + `, + prepend: css` + ${logicalCSS('border-top-left-radius', 'inherit')} + ${logicalCSS('border-bottom-left-radius', 'inherit')} + `, + uncompressed: ` + ${text} { + ${logicalCSS('padding-horizontal', euiTheme.size.xs)} + line-height: ${uncompressedHeight}; + } + + ${buttons} { + ${logicalCSS('height', uncompressedHeight)} + } + + .euiButtonIcon { + ${logicalCSS('width', euiTheme.size.xl)} + } + `, + compressed: css` + ${text} { + ${logicalCSS('padding-horizontal', euiTheme.size.xxs)} + line-height: ${compressedHeight}; + } + + ${buttons} { + ${logicalCSS('height', compressedHeight)} + } + + .euiButtonIcon { + ${logicalCSS('width', euiTheme.size.xl)} + } + `, + }; +}; diff --git a/packages/eui/src/components/form/form_control_layout/form_control_layout.test.tsx b/packages/eui/src/components/form/form_control_layout/form_control_layout.test.tsx index bd62efbac96..ab0cf5bc8ac 100644 --- a/packages/eui/src/components/form/form_control_layout/form_control_layout.test.tsx +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout.test.tsx @@ -7,9 +7,9 @@ */ import React from 'react'; -import { mount } from 'enzyme'; - -import { findTestSubject, requiredProps } from '../../../test'; +import { fireEvent } from '@testing-library/react'; +import { requiredProps } from '../../../test'; +import { shouldRenderCustomStyles } from '../../../test/internal'; import { render } from '../../../test/rtl'; import { EuiForm } from '../form'; @@ -22,6 +22,8 @@ jest.mock('../../', () => ({ })); describe('EuiFormControlLayout', () => { + shouldRenderCustomStyles(); + test('is rendered', () => { const { container } = render( @@ -77,11 +79,12 @@ describe('EuiFormControlLayout', () => { 'data-test-subj': 'myIcon', }; - const component = mount(); + const { getByTestSubject } = render( + + ); - const closeButton = findTestSubject(component, 'myIcon'); - closeButton.simulate('click'); - expect(icon.onClick).toBeCalled(); + fireEvent.click(getByTestSubject('myIcon')); + expect(icon.onClick).toHaveBeenCalled(); }); }); }); @@ -106,11 +109,12 @@ describe('EuiFormControlLayout', () => { 'data-test-subj': 'clearButton', }; - const component = mount(); + const { getByTestSubject } = render( + + ); - const closeButton = findTestSubject(component, 'clearButton'); - closeButton.simulate('click'); - expect(clear.onClick).toBeCalled(); + fireEvent.click(getByTestSubject('clearButton')); + expect(clear.onClick).toHaveBeenCalled(); }); }); }); @@ -238,7 +242,7 @@ describe('EuiFormControlLayout', () => { const layout = baseElement.querySelector('.euiFormControlLayout'); expect(layout).toBeDefined(); - expect(layout).toHaveClass('euiFormControlLayout--fullWidth'); + expect(layout!.className).toContain('fullWidth'); }); }); }); 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 00b20ab9cca..a1797de2c90 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 @@ -7,24 +7,28 @@ */ import React, { - cloneElement, FunctionComponent, HTMLAttributes, ReactElement, ReactNode, - useCallback, useMemo, } from 'react'; import classNames from 'classnames'; +import { useEuiMemoizedStyles } from '../../../services'; +import { CommonProps } from '../../common'; + +import { EuiFormLabel } from '../form_label'; +import { useFormContext } from '../eui_form_context'; import { getIconAffordanceStyles, isRightSideIcon } from './_num_icons'; import { EuiFormControlLayoutIcons, EuiFormControlLayoutIconsProps, } from './form_control_layout_icons'; -import { CommonProps } from '../../common'; -import { EuiFormLabel } from '../form_label'; -import { useFormContext } from '../eui_form_context'; +import { + euiFormControlLayoutStyles, + euiFormControlLayoutSideNodeStyles, +} from './form_control_layout.styles'; type StringOrReactElement = string | ReactElement; type PrependAppendType = StringOrReactElement | StringOrReactElement[]; @@ -95,18 +99,39 @@ export const EuiFormControlLayout: FunctionComponent< ...rest } = props; + const isGroup = !!(prepend || append); + const classes = classNames( 'euiFormControlLayout', { - 'euiFormControlLayout--fullWidth': fullWidth, - 'euiFormControlLayout--compressed': compressed, - 'euiFormControlLayout--readOnly': readOnly, - 'euiFormControlLayout--group': prepend || append, + 'euiFormControlLayout--group': isGroup, 'euiFormControlLayout-isDisabled': isDisabled, + 'euiFormControlLayout-readOnly': readOnly, }, className ); + const styles = useEuiMemoizedStyles(euiFormControlLayoutStyles); + + const cssStyles = [ + styles.euiFormControlLayout, + compressed ? styles.compressed : styles.uncompressed, + fullWidth ? styles.fullWidth : styles.formWidth, + ...(isGroup + ? [ + styles.group.group, + compressed ? styles.group.compressed : styles.group.uncompressed, + ] + : []), + ]; + + const childrenWrapperStyles = [ + styles.children.euiFormControlLayout__childrenWrapper, + isGroup && styles.children.inGroup, + isGroup && !append && styles.children.prependOnly, + isGroup && !prepend && styles.children.appendOnly, + ]; + const hasDropdownIcon = !readOnly && !isDisabled && isDropdown; const hasRightIcon = isRightSideIcon(icon); const hasLeftIcon = icon && !hasRightIcon; @@ -126,14 +151,16 @@ export const EuiFormControlLayout: FunctionComponent< }, [iconsPosition, icon, clear, isInvalid, isLoading, hasDropdownIcon]); return ( -
+
{hasLeftIcon && ( @@ -164,6 +191,7 @@ export const EuiFormControlLayout: FunctionComponent< side="append" nodes={append} inputId={inputId} + compressed={compressed} />
); @@ -176,30 +204,27 @@ 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 }) => { + compressed?: boolean; +}> = ({ side, nodes, inputId, compressed }) => { const className = `euiFormControlLayout__${side}`; - - const renderFormLabel = useCallback( - (label: string) => ( - - {label} - - ), - [inputId, className] - ); + const styles = useEuiMemoizedStyles(euiFormControlLayoutSideNodeStyles); + const cssStyles = [ + styles.euiFormControlLayout__side, + styles[side], + compressed ? styles.compressed : styles.uncompressed, + ]; 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, - }) +
+ {React.Children.map(nodes, (node) => + typeof node === 'string' ? ( + {node} + ) : ( + node + ) )} - +
); }; 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 8857bc5c0c2..e5c8677c753 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 @@ -149,10 +149,10 @@ exports[`EuiDualRange props maxInputProps allows overriding default props 1`] = class="euiRangeWrapper euiDualRange emotion-euiRangeWrapper-euiDualRange" >
- + +
`; @@ -135,10 +143,10 @@ exports[`EuiSuperSelect renders 1`] = ` value="" />