From 1ef42a0236f47f982f3334147811ae42b947c7f2 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 30 May 2024 08:58:15 -0700 Subject: [PATCH 1/4] [tech debt] Convert EuiFormControlLayout from a class to a function component - which will allow us to more easily use hooks etc for styles --- .../form_control_layout.tsx | 297 ++++++++---------- 1 file changed, 129 insertions(+), 168 deletions(-) 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 48d227f97e9..99845d8b81c 100644 --- a/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx @@ -8,22 +8,23 @@ import React, { cloneElement, - Component, + FunctionComponent, HTMLAttributes, ReactElement, ReactNode, + useCallback, + useMemo, } from 'react'; import classNames from 'classnames'; -import { getIconAffordanceStyles } from './_num_icons'; +import { getIconAffordanceStyles, isRightSideIcon } from './_num_icons'; import { EuiFormControlLayoutIcons, EuiFormControlLayoutIconsProps, - IconShape, } from './form_control_layout_icons'; import { CommonProps } from '../../common'; import { EuiFormLabel } from '../form_label'; -import { FormContext, FormContextValue } from '../eui_form_context'; +import { useFormContext } from '../eui_form_context'; type StringOrReactElement = string | ReactElement; type PrependAppendType = StringOrReactElement | StringOrReactElement[]; @@ -66,174 +67,134 @@ export type EuiFormControlLayoutProps = CommonProps & inputId?: string; }; -export class EuiFormControlLayout extends Component { - static contextType = FormContext; - - static defaultProps = { - iconsPosition: 'absolute', - }; - - render() { - const { defaultFullWidth } = this.context as FormContextValue; - const { - children, +export const EuiFormControlLayout: FunctionComponent< + EuiFormControlLayoutProps +> = (props) => { + const { defaultFullWidth } = useFormContext(); + const { + inputId, + className, + children, + icon, + iconsPosition, + clear, + isDropdown, + isLoading, + isInvalid, + isDisabled, + readOnly, + compressed, + prepend, + append, + fullWidth = defaultFullWidth, + ...rest + } = props; + + const classes = classNames( + 'euiFormControlLayout', + { + 'euiFormControlLayout--fullWidth': fullWidth, + 'euiFormControlLayout--compressed': compressed, + 'euiFormControlLayout--readOnly': readOnly, + 'euiFormControlLayout--group': prepend || append, + 'euiFormControlLayout-isDisabled': isDisabled, + }, + className + ); + + const hasDropdownIcon = !readOnly && !isDisabled && isDropdown; + const hasRightIcon = isRightSideIcon(icon); + const hasLeftIcon = icon && !hasRightIcon; + const hasRightIcons = + hasRightIcon || clear || isLoading || isInvalid || hasDropdownIcon; + + const iconAffordanceStyles = useMemo(() => { + if (iconsPosition === 'static') return; // Static icons don't need padding affordance + + return getIconAffordanceStyles({ icon, - iconsPosition, clear, - fullWidth = defaultFullWidth, - isLoading, - isDisabled, - compressed, - className, - prepend, - append, - readOnly, isInvalid, - isDropdown, - inputId, - ...rest - } = this.props; - - const classes = classNames( - 'euiFormControlLayout', - { - 'euiFormControlLayout--fullWidth': fullWidth, - 'euiFormControlLayout--compressed': compressed, - 'euiFormControlLayout--readOnly': readOnly, - 'euiFormControlLayout--group': prepend || append, - 'euiFormControlLayout-isDisabled': isDisabled, - }, - className - ); - - const iconAffordanceStyles = - iconsPosition === 'absolute' // Static icons don't need padding affordance - ? getIconAffordanceStyles({ - icon, - clear, - isInvalid, - isLoading, - isDropdown, - }) - : undefined; - - const prependNodes = this.renderSideNode('prepend', prepend, inputId); - const appendNodes = this.renderSideNode('append', append, inputId); - - return ( -
- {prependNodes} -
- {this.renderLeftIcons()} - {children} - {this.renderRightIcons()} -
- {appendNodes} -
- ); - } - - renderLeftIcons = () => { - const { icon, iconsPosition, compressed } = this.props; - - const leftCustomIcon = - icon && (icon as IconShape)?.side !== 'right' ? icon : undefined; - - return leftCustomIcon ? ( - - ) : null; - }; - - renderRightIcons = () => { - const { - icon, - iconsPosition, - clear, - compressed, isLoading, - isInvalid, - isDisabled, - readOnly, - isDropdown, - } = this.props; - const hasDropdownIcon = !readOnly && !isDisabled && isDropdown; - - const rightCustomIcon = - icon && (icon as IconShape)?.side === 'right' ? icon : undefined; - - const hasRightIcons = - rightCustomIcon || clear || isLoading || isInvalid || hasDropdownIcon; - - return hasRightIcons ? ( - + - ) : null; - }; - - renderSideNode( - side: 'append' | 'prepend', - nodes?: PrependAppendType, - inputId?: string - ) { - if (!nodes) { - return; - } - - if (typeof nodes === 'string') { - return this.createFormLabel(side, nodes, inputId); - } - - const appendNodes = React.Children.map(nodes, (item, index) => - typeof item === 'string' - ? this.createFormLabel(side, item, inputId) - : this.createSideNode(side, item, index) - ); - - return appendNodes; - } - - createFormLabel( - side: 'append' | 'prepend', - string: string, - inputId?: string - ) { - return ( - - {string} + {hasLeftIcon && ( + + )} + + {children} + + {hasRightIcons && ( + + )} + + + + ); +}; + +/** + * Internal subcomponent utility for prepend/append nodes + */ +const EuiFormControlLayoutSideNodes: FunctionComponent<{ + side: 'append' | 'prepend'; + nodes?: PrependAppendType; // For some bizarre reason if you make this the `children` prop instead, React doesn't properly override cloned keys :| + inputId?: string; +}> = ({ side, nodes, inputId }) => { + const className = `euiFormControlLayout__${side}`; + + const renderFormLabel = useCallback( + (label: string) => ( + + {label} - ); - } - - createSideNode( - side: 'append' | 'prepend', - node: ReactElement, - key: React.Key - ) { - return cloneElement(node, { - className: classNames( - `euiFormControlLayout__${side}`, - node.props.className - ), - key: key, - }); - } -} + ), + [inputId, className] + ); + + if (!nodes) return null; + + return ( + <> + {React.Children.map(nodes, (node, index) => + typeof node === 'string' + ? renderFormLabel(node) + : cloneElement(node, { + className: classNames(className, node.props.className), + key: index, + }) + )} + + ); +}; From c6473d555e5cc4a64db15a43b021b79c1614daa6 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Thu, 30 May 2024 08:58:24 -0700 Subject: [PATCH 2/4] [docs] Clean up append/prepend example slightly --- .../src/views/form_controls/prepend_append.js | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/eui/src-docs/src/views/form_controls/prepend_append.js b/packages/eui/src-docs/src/views/form_controls/prepend_append.js index 4e5751a1222..3dad35c7594 100644 --- a/packages/eui/src-docs/src/views/form_controls/prepend_append.js +++ b/packages/eui/src-docs/src/views/form_controls/prepend_append.js @@ -1,6 +1,7 @@ -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import { + EuiFlexGroup, EuiButtonEmpty, EuiButtonIcon, EuiFieldText, @@ -19,24 +20,24 @@ export default () => { const [isReadOnly, setReadOnly] = useState(false); return ( - - setCompressed(e.target.checked)} - /> -   - setDisabled(e.target.checked)} - /> -   - setReadOnly(e.target.checked)} - /> + <> + + setCompressed(e.target.checked)} + /> + setDisabled(e.target.checked)} + /> + setReadOnly(e.target.checked)} + /> + { readOnly={isReadOnly} aria-label="Use aria labels when no actual label is in use" /> - + ); }; From 6ff135da3475c7ef089e4645bcd0e47b3f13ed27 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 4 Jun 2024 08:22:23 -0700 Subject: [PATCH 3/4] [PR feedback] prop default Co-authored-by: Lene Gadewoll --- .../components/form/form_control_layout/form_control_layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 99845d8b81c..c74d5969d57 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 @@ -76,7 +76,7 @@ export const EuiFormControlLayout: FunctionComponent< className, children, icon, - iconsPosition, + iconsPosition = 'absolute', clear, isDropdown, isLoading, From 1095caa9f7283f07b0b80e1d184055770af7ccd3 Mon Sep 17 00:00:00 2001 From: Cee Chen <549407+cee-chen@users.noreply.github.com> Date: Tue, 4 Jun 2024 08:57:47 -0700 Subject: [PATCH 4/4] `iconsPosition` prop jsdoc --- .../form/form_control_layout/form_control_layout.tsx | 5 +++++ 1 file changed, 5 insertions(+) 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 c74d5969d57..00b20ab9cca 100644 --- a/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx +++ b/packages/eui/src/components/form/form_control_layout/form_control_layout.tsx @@ -43,6 +43,11 @@ export type EuiFormControlLayoutProps = CommonProps & append?: PrependAppendType; children?: ReactNode; icon?: EuiFormControlLayoutIconsProps['icon']; + /** + * Determines whether icons are absolutely or statically rendered. For single inputs, + * absolute rendering is typically preferred. + * @default absolute + */ iconsPosition?: EuiFormControlLayoutIconsProps['iconsPosition']; clear?: EuiFormControlLayoutIconsProps['clear']; /**