Skip to content

Commit

Permalink
feat: EuiFlexGroup and EuiFlexItem component prop type improvemen…
Browse files Browse the repository at this point in the history
…ts (#7688)
  • Loading branch information
tkajtoch authored Apr 29, 2024
1 parent 1e83176 commit f8c9aec
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 124 deletions.
1 change: 1 addition & 0 deletions changelogs/upcoming/7688.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Updated `EuiFlexGroup` and `EuiFlexItem` prop types to support passing any valid React component type to the `component` prop and ensure proper type checking of the extra props forwarded to the `component`.
9 changes: 6 additions & 3 deletions src-docs/src/views/flex/flex_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export const FlexExample = {
),
},
{
title: 'Spans instead of divs',
title: 'Override output component type',
source: [
{
type: GuideSectionTypes.JS,
Expand All @@ -255,9 +255,12 @@ export const FlexExample = {
],
text: (
<p>
Specify <EuiCode>component=&ldquo;span&rdquo;</EuiCode> on{' '}
Pass the <EuiCode>component</EuiCode> property to{' '}
<strong>EuiFlexGroup</strong> and/or <strong>EuiFlexItem</strong> to
change from the default <EuiCode>div</EuiCode>.
change the rendered component type from the default{' '}
<EuiCode>div</EuiCode>. It can be any valid React component type like
a tag name string such as <EuiCode>div</EuiCode>
or <EuiCode>span</EuiCode> or a React component.
</p>
),
snippet: componentSpanSnippet,
Expand Down
30 changes: 11 additions & 19 deletions src/components/flex/flex_group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@
* Side Public License, v 1.
*/

import React from 'react';
import React, { JSX } from 'react';
import { requiredProps } from '../../test';
import {
shouldRenderCustomStyles,
testOnReactVersion,
} from '../../test/internal';
import { shouldRenderCustomStyles } from '../../test/internal';
import { render } from '../../test/rtl';

import {
Expand All @@ -20,7 +17,6 @@ import {
ALIGN_ITEMS,
JUSTIFY_CONTENTS,
DIRECTIONS,
COMPONENT_TYPES,
} from './flex_group';

describe('EuiFlexGroup', () => {
Expand Down Expand Up @@ -98,25 +94,21 @@ describe('EuiFlexGroup', () => {
});

describe('component', () => {
COMPONENT_TYPES.forEach((value) => {
['div', 'span'].forEach((value) => {
test(`${value} is rendered`, () => {
const { container } = render(<EuiFlexGroup component={value} />);
const { container } = render(
<EuiFlexGroup component={value as keyof JSX.IntrinsicElements} />
);

expect(container.firstChild!.nodeName).toEqual(value.toUpperCase());
});
});

// React 18 throws a false error on test unmount for components w/ ref callbacks
// that throw in a `useEffect`. Note: This only affects the test env, not prod
// @see https://github.com/facebook/react/issues/25675#issuecomment-1363957941
// TODO: Remove `testOnReactVersion` once the above bug is fixed
testOnReactVersion(['16', '17'])(
`invalid component types throw an error`,
() => {
// @ts-expect-error intentionally passing an invalid value
expect(() => render(<EuiFlexGroup component="h2" />)).toThrow();
}
);
test('custom component is rendered', () => {
const component = () => <span>Custom component test</span>;
const { getByText } = render(<EuiFlexGroup component={component} />);
expect(getByText('Custom component test')).toBeInTheDocument();
});
});
});
});
152 changes: 80 additions & 72 deletions src/components/flex/flex_group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
* Side Public License, v 1.
*/

import React, { HTMLAttributes, Ref, forwardRef, useEffect } from 'react';
import React, {
ComponentPropsWithoutRef,
ComponentType,
ElementType,
ForwardedRef,
forwardRef,
FunctionComponent,
Ref,
} from 'react';
import classNames from 'classnames';
import { CommonProps } from '../common';

Expand Down Expand Up @@ -43,80 +51,80 @@ export const DIRECTIONS = [
] as const;
type FlexGroupDirection = (typeof DIRECTIONS)[number];

export const COMPONENT_TYPES = ['div', 'span'] as const;
type FlexGroupComponentType = (typeof COMPONENT_TYPES)[number];
type ComponentPropType = ElementType<CommonProps>;

export interface EuiFlexGroupProps
extends CommonProps,
HTMLAttributes<HTMLDivElement | HTMLSpanElement> {
alignItems?: FlexGroupAlignItems;
component?: FlexGroupComponentType;
direction?: FlexGroupDirection;
gutterSize?: EuiFlexGroupGutterSize;
justifyContent?: FlexGroupJustifyContent;
responsive?: boolean;
wrap?: boolean;
}
export type EuiFlexGroupProps<TComponent extends ComponentPropType = 'div'> =
ComponentPropsWithoutRef<TComponent> & {
alignItems?: FlexGroupAlignItems;
/**
* Customize the component type that is rendered.
*
* It can be any valid React component type like a tag name string
* such as `'div'` or `'span'`, a React component (a function, a class,
* or an exotic component like `memo()`).
*
* `<EuiFlexGroup>` accepts and forwards all extra props to the custom
* component.
*
* @example
* // Renders a <button> element
* <EuiFlexGroup component="button">
* Submit form
* </EuiFlexGroup>
* @default "div"
*/
component?: TComponent;
direction?: FlexGroupDirection;
gutterSize?: EuiFlexGroupGutterSize;
justifyContent?: FlexGroupJustifyContent;
responsive?: boolean;
wrap?: boolean;
};

export const EuiFlexGroup = forwardRef<
HTMLDivElement | HTMLSpanElement,
EuiFlexGroupProps
>(
(
{
children,
className,
gutterSize = 'l',
alignItems = 'stretch',
responsive = true,
justifyContent = 'flexStart',
direction = 'row',
wrap = false,
component = 'div',
...rest
},
ref: Ref<HTMLDivElement> | Ref<HTMLSpanElement>
) => {
const styles = useEuiMemoizedStyles(euiFlexGroupStyles);
const cssStyles = [
styles.euiFlexGroup,
responsive && !direction.includes('column') && styles.responsive,
wrap && styles.wrap,
styles.gutterSizes[gutterSize],
styles.justifyContent[justifyContent],
styles.alignItems[alignItems],
styles.direction[direction],
];
const EuiFlexGroupInternal = <TComponent extends ComponentPropType>(
{
className,
component = 'div' as TComponent,
gutterSize = 'l',
alignItems = 'stretch',
responsive = true,
justifyContent = 'flexStart',
direction = 'row',
wrap = false,
...rest
}: EuiFlexGroupProps<TComponent>,
ref: ForwardedRef<TComponent>
) => {
const styles = useEuiMemoizedStyles(euiFlexGroupStyles);
const cssStyles = [
styles.euiFlexGroup,
responsive && !direction.includes('column') && styles.responsive,
wrap && styles.wrap,
styles.gutterSizes[gutterSize],
styles.justifyContent[justifyContent],
styles.alignItems[alignItems],
styles.direction[direction],
];

const classes = classNames('euiFlexGroup', className);

const classes = classNames('euiFlexGroup', className);
// Cast the resolved component prop type to ComponentType to help TS
// process multiple infers and the overall type complexity.
// This might not be needed in TypeScript 5
const Component = component as ComponentType<CommonProps & typeof rest>;

useEffect(() => {
if (!COMPONENT_TYPES.includes(component)) {
throw new Error(
`${component} is not a valid element type. Use \`div\` or \`span\`.`
);
}
}, [component]);
return <Component {...rest} ref={ref} className={classes} css={cssStyles} />;
};

return component === 'span' ? (
<span
css={cssStyles}
className={classes}
ref={ref as Ref<HTMLSpanElement>}
{...rest}
>
{children}
</span>
) : (
<div
css={cssStyles}
className={classes}
ref={ref as Ref<HTMLDivElement>}
{...rest}
>
{children}
</div>
);
// Cast forwardRef return type to work with the generic TComponent type
// and not fallback to implicit any typing
export const EuiFlexGroup = forwardRef(EuiFlexGroupInternal) as <
TComponent extends ComponentPropType
>(
props: EuiFlexGroupProps<TComponent> & {
ref?: Ref<typeof EuiFlexGroupInternal>;
}
);
EuiFlexGroup.displayName = 'EuiFlexGroup';
) => ReturnType<typeof EuiFlexGroupInternal>;

// Cast is required here because of the cast above
(EuiFlexGroup as FunctionComponent).displayName = 'EuiFlexGroup';
41 changes: 32 additions & 9 deletions src/components/flex/flex_item.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
* Side Public License, v 1.
*/

import React from 'react';
import React, { JSX } from 'react';
import {
requiredProps,
startThrowingReactWarnings,
stopThrowingReactWarnings,
} from '../../test';
import { shouldRenderCustomStyles } from '../../test/internal';
import {
shouldRenderCustomStyles,
testOnReactVersion,
} from '../../test/internal';
import { render } from '../../test/rtl';

import { EuiFlexItem } from './flex_item';
Expand All @@ -29,10 +32,23 @@ describe('EuiFlexItem', () => {
expect(container.firstChild).toMatchSnapshot();
});

it('renders as the passed component element', () => {
const { container } = render(<EuiFlexItem component="span" />);
describe('component', () => {
['div', 'span'].forEach((value) => {
test(`${value} is rendered`, () => {
const { container } = render(
<EuiFlexItem component={value as keyof JSX.IntrinsicElements} />
);

expect(container.firstChild?.nodeName).toEqual(value.toUpperCase());
});
});

expect(container.firstChild?.nodeName).toEqual('SPAN');
test('custom component is rendered', () => {
const component = () => <span>Custom component test</span>;
const { getByText } = render(<EuiFlexItem component={component} />);

expect(getByText('Custom component test')).toBeInTheDocument();
});
});

describe('grow', () => {
Expand Down Expand Up @@ -78,9 +94,16 @@ describe('EuiFlexItem', () => {
});
});

it('throws an error for invalid values', () => {
// @ts-expect-error testing invalid type
expect(() => render(<EuiFlexItem grow={11} />)).toThrowError();
});
// React 18 throws a false error on test unmount for components w/ ref callbacks
// that throw in a `useEffect`. Note: This only affects the test env, not prod
// @see https://github.com/facebook/react/issues/25675#issuecomment-1363957941
// TODO: Remove `testOnReactVersion` once the above bug is fixed
testOnReactVersion(['16', '17'])(
`invalid component types throw an error`,
() => {
// @ts-expect-error intentionally passing an invalid value
expect(() => render(<EuiFlexItem grow={11} />)).toThrow();
}
);
});
});
Loading

0 comments on commit f8c9aec

Please sign in to comment.