Skip to content

Commit

Permalink
[EuiFormControlLayout] Convert from class to function component (#7803)
Browse files Browse the repository at this point in the history
Co-authored-by: Lene Gadewoll <[email protected]>
  • Loading branch information
cee-chen and mgadewoll authored Jun 4, 2024
1 parent bffd3f1 commit 1780a0d
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 188 deletions.
41 changes: 21 additions & 20 deletions packages/eui/src-docs/src/views/form_controls/prepend_append.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { Fragment, useState } from 'react';
import React, { useState } from 'react';

import {
EuiFlexGroup,
EuiButtonEmpty,
EuiButtonIcon,
EuiFieldText,
Expand All @@ -19,24 +20,24 @@ export default () => {
const [isReadOnly, setReadOnly] = useState(false);

return (
<Fragment>
<EuiSwitch
label="compressed"
checked={isCompressed}
onChange={(e) => setCompressed(e.target.checked)}
/>
&emsp;
<EuiSwitch
label="disabled"
checked={isDisabled}
onChange={(e) => setDisabled(e.target.checked)}
/>
&emsp;
<EuiSwitch
label="readOnly"
checked={isReadOnly}
onChange={(e) => setReadOnly(e.target.checked)}
/>
<>
<EuiFlexGroup responsive={false}>
<EuiSwitch
label="compressed"
checked={isCompressed}
onChange={(e) => setCompressed(e.target.checked)}
/>
<EuiSwitch
label="disabled"
checked={isDisabled}
onChange={(e) => setDisabled(e.target.checked)}
/>
<EuiSwitch
label="readOnly"
checked={isReadOnly}
onChange={(e) => setReadOnly(e.target.checked)}
/>
</EuiFlexGroup>
<EuiSpacer />
<EuiFieldText
placeholder="String & text in a tooltip"
Expand Down Expand Up @@ -180,6 +181,6 @@ export default () => {
readOnly={isReadOnly}
aria-label="Use aria labels when no actual label is in use"
/>
</Fragment>
</>
);
};
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 All @@ -42,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'];
/**
Expand All @@ -66,174 +72,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 = 'absolute',
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 1780a0d

Please sign in to comment.