diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormRow_Column_Layout.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormRow_Column_Layout.png new file mode 100644 index 00000000000..a0cbb25741b Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormRow_Column_Layout.png differ diff --git a/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormRow_Inline_Layout.png b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormRow_Inline_Layout.png new file mode 100644 index 00000000000..dedd36eba49 Binary files /dev/null and b/packages/eui/.loki/reference/chrome_desktop_Forms_EuiForm_EuiFormRow_Inline_Layout.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormRow_Column_Layout.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormRow_Column_Layout.png new file mode 100644 index 00000000000..8eaa518dee4 Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormRow_Column_Layout.png differ diff --git a/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormRow_Inline_Layout.png b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormRow_Inline_Layout.png new file mode 100644 index 00000000000..aae8c03aecd Binary files /dev/null and b/packages/eui/.loki/reference/chrome_mobile_Forms_EuiForm_EuiFormRow_Inline_Layout.png differ diff --git a/packages/eui/changelogs/upcoming/7968.md b/packages/eui/changelogs/upcoming/7968.md new file mode 100644 index 00000000000..a44d6b91b01 --- /dev/null +++ b/packages/eui/changelogs/upcoming/7968.md @@ -0,0 +1,9 @@ +**Deprecations** + +- Deprecated `EuiFormRow`'s `columnCompressedSwitch` display prop. Use `columnCompressed` instead, which will automatically account for child `EuiSwitch`es +- Deprecated `EuiFormRow`'s `rowCompressed` display prop. Use `row` instead for vertical forms, or `centerCompressed` for inline forms +- (Styling) Updated `EuiFormRow`'s `hasEmptySpaceLabel` prop to no longer attempt to automatically align its content to a vertical center. Use the `display="center"` prop for that instead + +**CSS-in-JS conversions** + +- Converted `EuiFormRow` to Emotion diff --git a/packages/eui/changelogs/upcoming/7970.md b/packages/eui/changelogs/upcoming/7970.md new file mode 100644 index 00000000000..9ac2adfbed5 --- /dev/null +++ b/packages/eui/changelogs/upcoming/7970.md @@ -0,0 +1,3 @@ +**Accessibility** + +- Updated the `aria-label` attribute for the `EuiFieldSearch` clear button diff --git a/packages/eui/src-docs/src/views/color_picker/color_palette_display.js b/packages/eui/src-docs/src/views/color_picker/color_palette_display.js index 2c168b8b578..4c56ee61d00 100644 --- a/packages/eui/src-docs/src/views/color_picker/color_palette_display.js +++ b/packages/eui/src-docs/src/views/color_picker/color_palette_display.js @@ -188,7 +188,7 @@ export default () => { Display fixed } - display="columnCompressedSwitch" + display="columnCompressed" > { : undefined } > - + { }; return ( - + - + { /> - + - + { /> - + { /> - + To use compressed forms, pass{' '} - display="rowCompressed" to - the EuiFormRows and compressed=true{' '} - to the form controls themselves. + {'compressed={true}'} to all form + controls.

), props: { @@ -69,10 +68,7 @@ export const FormCompressedExample = { }, demo: , snippet: [ - ` + ` `, ], @@ -86,21 +82,12 @@ export const FormCompressedExample = { }, ], text: ( - -

- Editor-style controls can be displayed in a two column layout for - even better use of limited space, just pass{' '} - - display="columnCompressed" - {' '} - to align the labels and inputs side by side. -

-

- EuiSwitches are a special case in which so you must - pass {'"columnCompressedSwitch"'}{' '} - to the EuiFormRow as the display property. -

-
+

+ Editor-style controls can be displayed in a two column layout for even + better use of limited space, just pass{' '} + display="columnCompressed"{' '} + to align the labels and inputs side by side. +

), props: { EuiFormRow, diff --git a/packages/eui/src-docs/src/views/form_compressed/form_horizontal.js b/packages/eui/src-docs/src/views/form_compressed/form_horizontal.js index 36bf9687f52..f9197336bb8 100644 --- a/packages/eui/src-docs/src/views/form_compressed/form_horizontal.js +++ b/packages/eui/src-docs/src/views/form_compressed/form_horizontal.js @@ -82,7 +82,7 @@ export default () => { />
- + { return ( - + { /> Show something} >
- - this.onKeyUp(e, incremental, onSearch)} - disabled={disabled} - ref={this.setRef} - {...rest} - /> - - + {(clearSearchButtonLabel: string) => ( + + + this.onKeyUp(e, incremental, onSearch)} + disabled={disabled} + ref={this.setRef} + {...rest} + /> + + + )} + ); } } 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 index c1978bfbecb..fa66f0cca95 100644 --- 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 @@ -126,6 +126,8 @@ export const euiFormControlLayoutSideNodeStyles = ( ${text} { /* Override .euiFormLabel CSS */ cursor: default; + overflow: hidden; + text-overflow: ellipsis; } /* Account for button padding when spacing children */ @@ -152,6 +154,7 @@ export const euiFormControlLayoutSideNodeStyles = ( } .euiButtonIcon { + flex-shrink: 0; ${logicalCSS('width', euiTheme.size.xl)} } `, @@ -166,6 +169,7 @@ export const euiFormControlLayoutSideNodeStyles = ( } .euiButtonIcon { + flex-shrink: 0; ${logicalCSS('width', euiTheme.size.xl)} } `, diff --git a/packages/eui/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap b/packages/eui/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap index ec324b1d2b9..2bca5e5dcd4 100644 --- a/packages/eui/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap +++ b/packages/eui/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap @@ -3,7 +3,7 @@ exports[`EuiFormRow is rendered 1`] = `
@@ -19,7 +19,7 @@ exports[`EuiFormRow is rendered 1`] = ` exports[`EuiFormRow props describedByIds is rendered 1`] = `
+
@@ -223,7 +226,7 @@ exports[`EuiFormRow props hasEmptyLabelSpace is rendered 1`] = ` exports[`EuiFormRow props helpText is rendered 1`] = `
; export const Playground: Story = { + argTypes: { + hasEmptyLabelSpace: { if: { arg: 'label', truthy: false } }, + }, args: { children: , label: 'Text field label', helpText: 'I am some friendly help text.', }, }; + +export const ColumnLayout: Story = { + parameters: { + // Show for relevant props documentation, but don't allow configuring + controls: { include: ['display'] }, + codeSnippet: { + // Not 1:1 with actual rendering, just a bare-bones example to get devs started + snippet: ` + + + + `, + }, + }, + argTypes: { display: { control: false } }, + args: { display: 'columnCompressed' }, + render: ({ ..._args }) => ( +
+ + + + + {}} + compressed + /> + + + + + + + + + {}} + /> + +
+ ), +}; + +export const InlineLayout: Story = { + parameters: { + // Show for relevant props documentation, but don't allow configuring + controls: { include: ['display', 'hasEmptyLabelSpace'] }, + codeSnippet: { + // Not 1:1 with actual rendering, just a bare-bones example to get devs started + snippet: ` + + + + + + + + + Save + + + + `, + }, + }, + argTypes: { + display: { control: false }, + hasEmptyLabelSpace: { control: false }, + }, + render: ({ ..._args }) => ( + <> + + + + + ), +}; +const RenderInlineLayout = ({ compressed }: { compressed: boolean }) => ( + + + + + + + + + + + + + + {}} + /> + + + + + Save + + + +); diff --git a/packages/eui/src/components/form/form_row/form_row.styles.ts b/packages/eui/src/components/form/form_row/form_row.styles.ts new file mode 100644 index 00000000000..6b3ce4b0a92 --- /dev/null +++ b/packages/eui/src/components/form/form_row/form_row.styles.ts @@ -0,0 +1,129 @@ +/* + * 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 { euiFormVariables } from '../form.styles'; + +export const euiFormRowStyles = (euiThemeContext: UseEuiTheme) => { + const { euiTheme } = euiThemeContext; + const { maxWidth, controlHeight, controlCompressedHeight } = + euiFormVariables(euiThemeContext); + + return { + euiFormRow: css` + /* Coerce inline form elements to behave as block-level elements */ + display: flex; + + + .euiButton { + ${logicalCSS('margin-top', euiTheme.size.base)} + } + `, + // Skip css`` to avoid generating an Emotion className + formWidth: ` + ${logicalCSS('max-width', maxWidth)} + `, + fullWidth: css` + ${logicalCSS('max-width', '100%')} + `, + + // Skip css`` to avoid generating an extra className + row: ` + flex-direction: column; + row-gap: ${euiTheme.size.xs}; + + .euiFormRow__labelWrapper { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + } + + + .euiFormRow { + ${logicalCSS('margin-top', euiTheme.size.base)} + } + `, + // No difference from the uncompressed row for this current theme AFAICT + // TODO: Deprecate prop + get rowCompressed() { + return this.row; + }, + + columnCompressed: css` + flex-direction: row; + align-items: stretch; + column-gap: ${euiTheme.size.s}; + + .euiFormRow__label { + hyphens: auto; + } + + .euiFormRow__labelWrapper { + flex-basis: calc(33% - ${euiTheme.size.s}); /* Account for gap */ + ${logicalCSS('min-width', 0)} + line-height: ${controlCompressedHeight}; + } + + .euiFormRow__fieldWrapper { + flex-basis: 67%; + ${logicalCSS('min-width', 0)} + } + + + .euiFormRow { + ${logicalCSS('margin-top', euiTheme.size.s)} + } + + /* Increase spacing for switches */ + &:has(.euiSwitch) { + &:not(:first-child) { + ${logicalCSS('margin-top', euiTheme.size.m)} + } + + &:not(:last-child) { + ${logicalCSS('margin-bottom', euiTheme.size.m)} + } + + .euiFormRow__labelWrapper { + line-height: ${euiTheme.size.base}; + } + } + `, + // Handled by :has CSS now rather than a separate modifier/prop + // TODO: Deprecate prop + get columnCompressedSwitch() { + return this.columnCompressed; + }, + + // Center display is primarily for inline form rows, which may have have + // field content that is shorter than form controls (e.g. switches, text), + // and should vertically center said content + centerDisplayCss: (compressed: boolean) => ` + .euiFormRow__fieldWrapper { + display: flex; + align-items: center; + ${logicalCSS( + 'min-height', + compressed ? controlCompressedHeight : controlHeight + )} + } + `, + get center() { + return css` + ${this.row} + ${this.centerDisplayCss(false)} + `; + }, + get centerCompressed() { + return css` + ${this.row} + ${this.centerDisplayCss(true)} + `; + }, + }; +}; diff --git a/packages/eui/src/components/form/form_row/form_row.test.tsx b/packages/eui/src/components/form/form_row/form_row.test.tsx index 0c93b48f970..5c48ae151a0 100644 --- a/packages/eui/src/components/form/form_row/form_row.test.tsx +++ b/packages/eui/src/components/form/form_row/form_row.test.tsx @@ -7,14 +7,21 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import { requiredProps } from '../../../test'; +import { fireEvent } from '@testing-library/react'; import { render } from '../../../test/rtl'; +import { shouldRenderCustomStyles } from '../../../test/internal'; +import { requiredProps } from '../../../test'; import { EuiForm } from '../form'; import { EuiFormRow, DISPLAYS } from './form_row'; describe('EuiFormRow', () => { + shouldRenderCustomStyles( + + + + ); + test('is rendered', () => { const { container } = render( @@ -33,27 +40,28 @@ describe('EuiFormRow', () => { error: ['Error one', 'Error two'], }; - const tree = mount( + const { container } = render( ); + const input = container.querySelector('input')!; + const label = container.querySelector('label')!; + // Input is labeled by the label. - expect(tree.find('input').prop('id')).toEqual('generated-id'); - expect(tree.find('EuiFormLabel').prop('htmlFor')).toEqual('generated-id'); + expect(input).toHaveAttribute('id', 'generated-id'); + expect(label).toHaveAttribute('for', 'generated-id'); + + const helpText = container.querySelector('.euiFormHelpText')!; + const errorText = container.querySelectorAll('.euiFormErrorText'); // Input is described by help and error text. - expect(tree.find('EuiFormHelpText').prop('id')).toEqual( - 'generated-id-help-0' - ); - expect(tree.find('EuiFormErrorText').at(0).prop('id')).toEqual( - 'generated-id-error-0' - ); - expect(tree.find('EuiFormErrorText').at(1).prop('id')).toEqual( - 'generated-id-error-1' - ); - expect(tree.find('input').prop('aria-describedby')).toEqual( + expect(helpText).toHaveAttribute('id', 'generated-id-help-0'); + expect(errorText[0]).toHaveAttribute('id', 'generated-id-error-0'); + expect(errorText[1]).toHaveAttribute('id', 'generated-id-error-1'); + expect(input).toHaveAttribute( + 'aria-describedby', 'generated-id-help-0 generated-id-error-0 generated-id-error-1' ); }); @@ -227,80 +235,24 @@ describe('EuiFormRow', () => { }); describe('behavior', () => { - describe('onFocus', () => { - test('is called in child', () => { - const focusMock = jest.fn(); - - const component = mount( - Label}> - - - ); - - component.find('input').simulate('focus'); - - expect(focusMock).toBeCalledTimes(1); - - // Ensure the focus event is properly fired on the parent - // which will pass down to the EuiFormLabel - expect(component.find('label').getDOMNode()).toHaveClass( - 'euiFormLabel-isFocused' - ); - }); - - test('works in parent even if not in child', () => { - const component = mount( - Label}> - - - ); + it('onFocus and onBlur', () => { + const focusMock = jest.fn(); + const blurMock = jest.fn(); - component.find('input').simulate('focus'); - - // Ensure the focus event is properly fired on the parent - // which will pass down to the EuiFormLabel - expect(component.find('label').getDOMNode()).toHaveClass( - 'euiFormLabel-isFocused' - ); - }); - }); - - describe('onBlur', () => { - test('is called in child', () => { - const blurMock = jest.fn(); - - const component = mount( - Label}> - - - ); - - component.find('input').simulate('blur'); - - expect(blurMock).toBeCalledTimes(1); - - // Ensure the blur event is properly fired on the parent - // which will pass down to the EuiFormLabel - expect(component.find('label').getDOMNode()).not.toHaveClass( - 'euiFormLabel-isFocused' - ); - }); - - test('works in parent even if not in child', () => { - const component = mount( - Label}> - - - ); + const { getByRole, container } = render( + Label}> + + + ); + const label = container.querySelector('.euiFormLabel')!; - component.find('input').simulate('blur'); + fireEvent.focus(getByRole('textbox')); + expect(focusMock).toHaveBeenCalledTimes(1); + expect(label.className).toContain('isFocused'); - // Ensure the blur event is properly fired on the parent - // which will pass down to the EuiFormLabel - expect(component.find('label').getDOMNode()).not.toHaveClass( - 'euiFormLabel-isFocused' - ); - }); + fireEvent.blur(getByRole('textbox')); + expect(blurMock).toHaveBeenCalledTimes(1); + expect(label.className).not.toContain('isFocused'); }); }); @@ -315,7 +267,7 @@ describe('EuiFormRow', () => { ); const row = container.querySelector('.euiFormRow'); - expect(row).toHaveClass('euiFormRow--fullWidth'); + expect(row!.className).toContain('fullWidth'); }); }); }); diff --git a/packages/eui/src/components/form/form_row/form_row.tsx b/packages/eui/src/components/form/form_row/form_row.tsx index 38633c842b1..934d7fae4a1 100644 --- a/packages/eui/src/components/form/form_row/form_row.tsx +++ b/packages/eui/src/components/form/form_row/form_row.tsx @@ -8,54 +8,56 @@ import React, { cloneElement, - Component, + FunctionComponent, Children, HTMLAttributes, ReactElement, ReactNode, + useState, + useCallback, + useMemo, } from 'react'; import classNames from 'classnames'; -import { ExclusiveUnion, CommonProps, keysOf } from '../../common'; -import { get } from '../../../services/objects'; +import { useGeneratedHtmlId, useEuiMemoizedStyles } from '../../../services'; +import { ExclusiveUnion, CommonProps } from '../../common'; +import { EuiSpacer } from '../../spacer'; import { EuiFormHelpText } from '../form_help_text'; import { EuiFormErrorText } from '../form_error_text'; import { EuiFormLabel } from '../form_label'; -import { FormContext, FormContextValue } from '../eui_form_context'; - -import { htmlIdGenerator } from '../../../services/accessibility'; - -const displayToClassNameMap = { - row: null, - rowCompressed: 'euiFormRow--compressed', - columnCompressed: 'euiFormRow--compressed euiFormRow--horizontal', - center: null, - centerCompressed: 'euiFormRow--compressed', - columnCompressedSwitch: - 'euiFormRow--compressed euiFormRow--horizontal euiFormRow--hasSwitch', -}; - -export const DISPLAYS = keysOf(displayToClassNameMap); - -export type EuiFormRowDisplayKeys = keyof typeof displayToClassNameMap; +import { useFormContext } from '../eui_form_context'; +import { euiFormRowStyles } from './form_row.styles'; + +export const DISPLAYS = [ + 'row', + 'columnCompressed', + 'center', + 'centerCompressed', + /** + * @deprecated + */ + 'columnCompressedSwitch', + /** + * @deprecated + */ + 'rowCompressed', +] as const; -interface EuiFormRowState { - isFocused: boolean; - id: string; -} +export type EuiFormRowDisplayKeys = (typeof DISPLAYS)[number]; type EuiFormRowCommonProps = CommonProps & { /** - * When `rowCompressed`, just tightens up the spacing; - * Set to `columnCompressed` if compressed - * and horizontal layout is needed. - * Set to `center` or `centerCompressed` to align non-input - * content better with inline rows. - * Set to `columnCompressedSwitch` if the form control being passed - * as the child is a switch. + * - `columnCompressed` creates a compressed and horizontal layout + * - `columnCompressedSwitch` - **deprecated**, use `columnCompressed` instead + * - `center`/`centerCompressed` helps align non-input content better with inline form layouts + * - `rowCompressed` - **deprecated**, does not currently affect styling */ display?: EuiFormRowDisplayKeys; + /** + * Useful for inline form layouts, primarily for content that + * needs to be aligned with inputs but does not need a label + */ hasEmptyLabelSpace?: boolean; /** * Expand to fill 100% of the parent. @@ -111,225 +113,156 @@ type LegendProps = { export type EuiFormRowProps = ExclusiveUnion; -export class EuiFormRow extends Component { - static contextType = FormContext; - - static defaultProps: Partial = { - display: 'row', - hasEmptyLabelSpace: false, - describedByIds: [], - labelType: 'label', - hasChildLabel: true, - }; - - state: EuiFormRowState = { - isFocused: false, - id: this.props.id || htmlIdGenerator()(), - }; - - onFocus = (...args: any[]) => { - // Doing this to allow onFocus to be called correctly from the child input element as this component overrides it - const onChildFocus = get(this.props, 'children.props.onFocus'); - if (onChildFocus) { - onChildFocus(...args); - } - - this.setState(({ isFocused }) => { - if (!isFocused) { - return { - isFocused: true, - }; - } else { - return null; - } - }); - }; - - onBlur = (...args: any[]) => { - // Doing this to allow onBlur to be called correctly from the child input element as this component overrides it - const onChildBlur = get(this.props, 'children.props.onBlur'); - if (onChildBlur) { - onChildBlur(...args); - } - - this.setState({ - isFocused: false, +export const EuiFormRow: FunctionComponent = ({ + className, + children, + helpText, + isInvalid, + error, + label, + labelType = 'label', + labelAppend, + hasEmptyLabelSpace = false, + fullWidth: _fullWidth, + describedByIds, + display = 'row', + hasChildLabel = true, + id: propsId, + isDisabled, + ...rest +}) => { + const { defaultFullWidth } = useFormContext(); + const fullWidth = _fullWidth ?? defaultFullWidth; + const id = useGeneratedHtmlId({ conditionalId: propsId }); + const hasLabel = label || labelAppend; + + const [isFocused, setIsFocused] = useState(false); + const onFocusWithin = useCallback(() => setIsFocused(true), []); + const onBlurWithin = useCallback(() => setIsFocused(false), []); + + const classes = classNames( + 'euiFormRow', + { + 'euiFormRow--hasEmptyLabelSpace': hasEmptyLabelSpace, + 'euiFormRow--hasLabel': hasLabel, + }, + className + ); + + const styles = useEuiMemoizedStyles(euiFormRowStyles); + const cssStyles = [ + styles.euiFormRow, + fullWidth ? styles.fullWidth : styles.formWidth, + styles[display], + ]; + + const optionalHelpTexts = useMemo(() => { + if (!helpText) return; + const helpTexts = Array.isArray(helpText) ? helpText : [helpText]; + + return helpTexts.map((helpText, i) => { + const key = typeof helpText === 'string' ? helpText : i; + return ( + + {helpText} + + ); }); - }; - - render() { - const { defaultFullWidth } = this.context as FormContextValue; - - const { - children, - helpText, - isInvalid, - error, - label, - labelType, - labelAppend, - hasEmptyLabelSpace, - fullWidth = defaultFullWidth, - className, - describedByIds, - display, - hasChildLabel, - id: propsId, - isDisabled, - ...rest - } = this.props; - - const { id } = this.state; - const hasLabel = label || labelAppend; - - const classes = classNames( - 'euiFormRow', - { - 'euiFormRow--hasEmptyLabelSpace': hasEmptyLabelSpace, - 'euiFormRow--fullWidth': fullWidth, - 'euiFormRow--hasLabel': hasLabel, - }, - displayToClassNameMap[display!], // Safe use of ! as default prop is 'row' - className - ); - - let optionalHelpTexts; - - if (helpText) { - const helpTexts = Array.isArray(helpText) ? helpText : [helpText]; - optionalHelpTexts = helpTexts.map((helpText, i) => { - const key = typeof helpText === 'string' ? helpText : i; - return ( - - {helpText} - - ); - }); - } - - let optionalErrors; - - if (error && isInvalid) { - const errorTexts = Array.isArray(error) ? error : [error]; - optionalErrors = errorTexts.map((error, i) => { - const key = typeof error === 'string' ? error : i; - return ( - - {error} - - ); - }); - } - - let optionalLabel; - const isLegend = label && labelType === 'legend' ? true : false; - const labelId = `${id}-label`; - - if (hasLabel) { - let labelProps = {}; - if (isLegend) { - labelProps = { - type: labelType, - }; - } else { - labelProps = { - htmlFor: hasChildLabel ? id : undefined, - ...(!isDisabled && { isFocused: this.state.isFocused }), // If the row is disabled, don't pass the isFocused state. - type: labelType, - }; - } - - optionalLabel = ( -
- - {label} - - {labelAppend && ' '} - {labelAppend} -
+ }, [helpText, id]); + + const optionalErrors = useMemo(() => { + if (!(error && isInvalid)) return; + const errorTexts = Array.isArray(error) ? error : [error]; + + return errorTexts.map((error, i) => { + const key = typeof error === 'string' ? error : i; + return ( + + {error} + ); - } + }); + }, [error, isInvalid, id]); - const optionalProps: React.AriaAttributes = {}; - /** - * Safe use of ! as default prop is [] - */ - const describingIds = [...describedByIds!]; + const ariaDescribedBy = useMemo(() => { + const describingIds = [...(describedByIds || [])]; - if (optionalHelpTexts) { + if (optionalHelpTexts?.length) { optionalHelpTexts.forEach((optionalHelpText) => describingIds.push(optionalHelpText.props.id) ); } - - if (optionalErrors) { + if (optionalErrors?.length) { optionalErrors.forEach((error) => describingIds.push(error.props.id)); } - - if (describingIds.length > 0) { - optionalProps['aria-describedby'] = describingIds.join(' '); + if (describingIds.length) { + return describingIds.join(' '); } + }, [describedByIds, optionalHelpTexts, optionalErrors]); + const field = useMemo(() => { const child = Children.only(children); - const field = cloneElement(child, { + return cloneElement(child, { id, // Allow the child's disabled or isDisabled prop to supercede the `isDisabled` disabled: child.props.disabled ?? child.props.isDisabled ?? isDisabled, - onFocus: this.onFocus, - onBlur: this.onBlur, - ...optionalProps, + 'aria-describedby': ariaDescribedBy, }); - - const fieldWrapperClasses = classNames('euiFormRow__fieldWrapper', { - euiFormRow__fieldWrapperDisplayOnly: - /** - * Safe use of ! as default prop is 'row' - */ - display!.startsWith('center'), - }); - - const sharedProps = { - className: classes, - id: `${id}-row`, - }; - - const contents = ( - - {optionalLabel} -
- {field} - {optionalErrors} - {optionalHelpTexts} + }, [children, id, isDisabled, ariaDescribedBy]); + + const Element = labelType === 'legend' ? 'fieldset' : 'div'; + + return ( + )} + > + {hasLabel ? ( +
+ + {label} + + {labelAppend && ' '} + {labelAppend}
- - ); - - return labelType === 'legend' ? ( -
)} + ) : ( + hasEmptyLabelSpace && ( + + ) + )} +
- {contents} -
- ) : ( -
)}> - {contents} + {field} + {optionalErrors} + {optionalHelpTexts}
- ); - } -} +
+ ); +}; diff --git a/packages/eui/src/components/index.scss b/packages/eui/src/components/index.scss index 2daaa063c25..276f366acfd 100644 --- a/packages/eui/src/components/index.scss +++ b/packages/eui/src/components/index.scss @@ -1,4 +1,3 @@ // Components @import 'datagrid/index'; -@import 'form/index'; diff --git a/packages/eui/src/components/inline_edit/__snapshots__/inline_edit_form.test.tsx.snap b/packages/eui/src/components/inline_edit/__snapshots__/inline_edit_form.test.tsx.snap index 2a4b4f41a1c..46db6fd72d0 100644 --- a/packages/eui/src/components/inline_edit/__snapshots__/inline_edit_form.test.tsx.snap +++ b/packages/eui/src/components/inline_edit/__snapshots__/inline_edit_form.test.tsx.snap @@ -13,7 +13,7 @@ exports[`EuiInlineEditForm edit mode editModeProps.cancelButtonProps 1`] = ` class="euiFlexItem emotion-euiFlexItem-grow-1" >
@@ -116,7 +115,6 @@ exports[`EuiInlineEditForm edit mode editModeProps.formRowProps 1`] = ` class="euiFormControlLayout__childrenWrapper emotion-euiFormControlLayout__childrenWrapper" >