-
Notifications
You must be signed in to change notification settings - Fork 841
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: EuiFlexGroup
and EuiFlexItem
component prop type improvements
#7688
Changes from all commits
9d0fb07
ce274e2
1d94db9
63049cc
848708a
73cad77
a622d0a
67b28c4
e4cbb8e
b25a56c
8607f2e
bc12d64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I generally like the idea of allowing to pass React components with regards that it helps to make it more scoped to what's actually used and how this is readable. There might be limitations to this though: <EuiFlexGroup
direction="column"
component={EuiButton}
>
<EuiFlexItem component="span">Foo</EuiFlexItem>
<EuiFlexItem component="span">Bar</EuiFlexItem>
</EuiFlexGroup> This Of course, there is no preventing users from passing more than a single component when using a wrapping component. const someComponent = (props) => (
<>
<EuiSomeComponent {...props}>
...
</EuiSomeComponent>
<EuiSomeOtherComponent {...props}>
...
</EuiSomeOtherComponent>
</>
) This could lead to some weird creations 😅 💭 Just some further ideas: Maybe providing those layout functionality as shared hooks could be an idea too? 🤔 That would keep the components more clean in what they are/do but enables shared behavior anyway. It would need a bigger lift to add those though. const flexGroupStyles = useFlexGroup(options)
<EuiSomeComponent css={flexGroupStyles} /> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We recently explored shared hooks (or headless components), and I really like the general idea. However, this isn't a pattern we use right, and we'd need to be extra careful when introducing it to avoid duplicate functionality or—what's worse—having two almost exact things that do the same thing and confuse users. There will be components where the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you refer with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I meant having something like this:
instead of just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's up to the passed |
||
* 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 < | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type cast (and the one below) is here because |
||
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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be an idea to add additional examples for variants of passing a custom component?
As it would be important to pass the
props
along for the flex styles to be applied it might be good to document it? Not sure how descriptive we need to be. 🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be completely honest, I didn't want to spend too much time on these examples, considering we're moving to a new docs platform soon, and this isn't a major feature.
In my opinion, it should be pretty straightforward to the majority of React developers that wrapping a component in another component won't implicitly pass props down from
EuiFlexGroup
two levels deep. If we wanted to support passing props to bothComponentType
and JSX elements, it would require a conditionalcloneElement
call, and I'm not sure if that's something we'd be 100% happy to introduceThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair enough. I generally also think that should be clear to developers as is default JS/React but I don't yet know how descriptive we want to be 🙂
Regarding the JSX - I think that goes into the general direction around how should
component
be, like how much are concerns separated. The EuiFlexGroup would not need to be able to be anything else in that case. But yes, then thecomponent
would need to be cloned again 🙈