Skip to content

Commit

Permalink
[tech debt] Convert EuiFormControlLayout from a class to a function c…
Browse files Browse the repository at this point in the history
…omponent

- which will allow us to more easily use hooks etc for styles
  • Loading branch information
cee-chen committed May 30, 2024
1 parent 654e470 commit 1ef42a0
Showing 1 changed file with 129 additions and 168 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -66,174 +67,134 @@ export type EuiFormControlLayoutProps = CommonProps &
inputId?: string;
};

export class EuiFormControlLayout extends Component<EuiFormControlLayoutProps> {
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 (
<div className={classes} {...rest}>
{prependNodes}
<div
className="euiFormControlLayout__childrenWrapper"
style={iconAffordanceStyles}
>
{this.renderLeftIcons()}
{children}
{this.renderRightIcons()}
</div>
{appendNodes}
</div>
);
}

renderLeftIcons = () => {
const { icon, iconsPosition, compressed } = this.props;

const leftCustomIcon =
icon && (icon as IconShape)?.side !== 'right' ? icon : undefined;

return leftCustomIcon ? (
<EuiFormControlLayoutIcons
side="left"
icon={leftCustomIcon}
iconsPosition={iconsPosition}
compressed={compressed}
/>
) : 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 ? (
<EuiFormControlLayoutIcons
side="right"
icon={rightCustomIcon}
iconsPosition={iconsPosition}
compressed={compressed}
clear={clear}
isLoading={isLoading}
isInvalid={isInvalid}
isDropdown={hasDropdownIcon}
isDropdown: hasDropdownIcon,
});
}, [iconsPosition, icon, clear, isInvalid, isLoading, hasDropdownIcon]);

return (
<div className={classes} {...rest}>
<EuiFormControlLayoutSideNodes
side="prepend"
nodes={prepend}
inputId={inputId}
/>
) : 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 (
<EuiFormLabel
htmlFor={inputId}
className={`euiFormControlLayout__${side}`}
<div
className="euiFormControlLayout__childrenWrapper"
style={iconAffordanceStyles}
>
{string}
{hasLeftIcon && (
<EuiFormControlLayoutIcons
side="left"
icon={icon}
iconsPosition={iconsPosition}
compressed={compressed}
/>
)}

{children}

{hasRightIcons && (
<EuiFormControlLayoutIcons
side="right"
icon={hasRightIcon ? icon : undefined}
iconsPosition={iconsPosition}
compressed={compressed}
clear={clear}
isLoading={isLoading}
isInvalid={isInvalid}
isDropdown={hasDropdownIcon}
/>
)}
</div>
<EuiFormControlLayoutSideNodes
side="append"
nodes={append}
inputId={inputId}
/>
</div>
);
};

/**
* 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) => (
<EuiFormLabel htmlFor={inputId} className={className}>
{label}
</EuiFormLabel>
);
}

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,
})
)}
</>
);
};

0 comments on commit 1ef42a0

Please sign in to comment.