Skip to content
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

Merged
merged 12 commits into from
Apr 29, 2024

Conversation

tkajtoch
Copy link
Member

@tkajtoch tkajtoch commented Apr 16, 2024

Summary

This resolves #7612 by updating EuiFlexGroup and EuiFlexItem to support dynamic prop type resolution with generics. It also cleans up the code and adds ref forwarding to EuiFlexItem to keep these two consistent.

The generic argument falls back to 'div' to keep the API backward compatible in cases where a customer would import and use the EuiFlexGroupProps or EuiFlexItemProps type.

There are a couple of type casts added to ensure TypeScript and IDEs can resolve the full complex type and provide meaningful code suggestions.

QA

  • Ensure all tests are passing
  • Checkout the branch locally, import EuiFlexGroup/EuiFlexItem and confirm the types are correctly resolved, suggesting and type checking props of the passed component.

General checklist

  • Docs site QA
  • Code quality checklist
  • Release checklist
    • A changelog entry exists and is marked appropriately.

@tkajtoch tkajtoch self-assigned this Apr 16, 2024
@tkajtoch tkajtoch marked this pull request as ready for review April 16, 2024 14:14
@tkajtoch tkajtoch requested a review from a team as a code owner April 16, 2024 14:14
@tkajtoch tkajtoch changed the title feat: flex generics feat: EuiFlexGroup and EuiFlexItem component prop type improvements Apr 16, 2024
* 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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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:
The output might at times be a bit unexpected as it depends on the passed component where the props will be applied to.

<EuiFlexGroup
  direction="column"
  component={EuiButton}
>
  <EuiFlexItem component="span">Foo</EuiFlexItem>
  <EuiFlexItem component="span">Bar</EuiFlexItem>
</EuiFlexGroup>

This direction="column" will be applied to the outer wrapper but EuiButton places children in an inner wrapper with it's own flex behavior, meaning the direction change won't be applied or would need manual overwriting the inner styles from the outside.

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} />

Copy link
Member Author

Choose a reason for hiding this comment

The 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 component prop won't work as good, and your EuiButton example is a good example of it. We could consider passing EuiFlexGroup/EuiFlexItem props down to the child as an easy workaround 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you refer with "down to the child" to the inner wrapper within EuiButton?
If so, I'm not sure that's overall a scalable solution? To pass those props along might differ between components and would need manual adjustment for any component where the spread ...rest is on an unexpected level.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I meant having something like this:

const EuiFlexGroup = (props) => {
  [...]
  return <Component {...props} />
}

instead of just {...rest}, but that passes all props to the custom component even if they're unwanted, and would need to be limited to custom components only, since native HTML elements would convert props to DOM attributes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's up to the passed component how it handles props. We shouldn't do anything special for the flex components to handle cases such as EuiButton

@@ -246,7 +246,7 @@ export const FlexExample = {
),
},
{
title: 'Spans instead of divs',
title: 'Override output component type',
Copy link
Contributor

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?

// props are all passed to the component
<EuiFlexGroup
  component={EuiSomeComponent}
/>
// props need to be passed along to apply
const component = (props) => <EuiSomeComponent {...props} />
<EuiFlexGroup
  component={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. 🤔

Copy link
Member Author

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 both ComponentType and JSX elements, it would require a conditional cloneElement call, and I'm not sure if that's something we'd be 100% happy to introduce

Copy link
Contributor

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 the component would need to be cloned again 🙈

// current
<EuiFlexGroup
  component={EuiButton}
  ...buttonProps
/>

// variant
<EuiFlexGroup
  component={<EuiButton {...buttonProps} />}
  ...flexProps
/>

src/components/flex/flex_group.tsx Outdated Show resolved Hide resolved
@tkajtoch tkajtoch force-pushed the feat/flex-generics branch from cc50871 to 8607f2e Compare April 17, 2024 12:23
);
// Cast forwardRef return type to work with the generic TComponent type
// and not fallback to implicit any typing
export const EuiFlexGroup = forwardRef(EuiFlexGroupInternal) as <
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type cast (and the one below) is here because forwardRef doesn't support creating generic components. The issue is described here if you wanna learn more :)

@kibanamachine
Copy link

Preview staging links for this PR:

@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

cc @tkajtoch

Copy link
Contributor

@mgadewoll mgadewoll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢 🐈‍⬛ Looks good to me as implementation. Any other potential adjustments we could think about once the need arises 👍

@tkajtoch tkajtoch merged commit f8c9aec into elastic:main Apr 29, 2024
7 checks passed
cee-chen added a commit that referenced this pull request May 1, 2024
cee-chen added a commit to elastic/kibana that referenced this pull request May 3, 2024
`v94.1.0-backport.0` ⏩ `v94.2.1-backport.0`

_[Questions? Please see our Kibana upgrade
FAQ.](https://github.com/elastic/eui/blob/main/wiki/eui-team-processes/upgrading-kibana.md#faq-for-kibana-teams)_

---

##
[`v94.2.1-backport.0`](https://github.com/elastic/eui/releases/v94.2.1-backport.0)

**This is a backport release only intended for use by Kibana.**

- Reverted the `EuiFlexGroup`/`EuiFlexItem` `component` prop feature due
to Kibana typing issues

## [`v94.2.1`](https://github.com/elastic/eui/releases/v94.2.1)

**Bug fixes**

- Fixed an `EuiTabbedContent` edge case bug that occurred when updated
with a completely different set of `tabs`
([#7713](elastic/eui#7713))
- Fixed the `@storybook/test` dependency to be listed in
`devDependencies` and not `dependencies`
([#7719](elastic/eui#7719))

## [`v94.2.0`](https://github.com/elastic/eui/releases/v94.2.0)

- Updated `getDefaultEuiMarkdownPlugins()` to allow excluding the
following plugins in addition to `tooltip`:
([#7676](elastic/eui#7676))
  - `checkbox`
  - `linkValidator`
  - `lineBreaks`
  - `emoji`
- Updated `EuiSelectable`'s `isPreFiltered` prop to allow passing a
configuration object, which allows disabling search highlighting in
addition to search filtering
([#7683](elastic/eui#7683))
- 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`.
([#7688](elastic/eui#7688))
- Updated `EuiSearchBar` to allow the `@` special character in query
string searches ([#7702](elastic/eui#7702))
- Added a new, optional `optionMatcher` prop to `EuiSelectable` and
`EuiComboBox` allowing passing a custom option matcher function to these
components and controlling option filtering for given search string
([#7709](elastic/eui#7709))

**Bug fixes**

- Fixed an `EuiPageTemplate` bug where prop updates would not cascade
down to child sections
([#7648](elastic/eui#7648))
- To cascade props down to the sidebar, `EuiPageTemplate` now explicitly
requires using the `EuiPageTemplate.Sidebar` rather than
`EuiPageSidebar`
- Fixed `EuiFieldNumber`'s typing to accept an icon configuration shape
([#7666](elastic/eui#7666))
- Fixed `EuiFieldText` and `EuiFieldNumber` to render the correct
paddings for icon shapes set to `side: 'right'`
([#7666](elastic/eui#7666))
- Fixed `EuiFieldText` and `EuiFieldNumber` to fully ignore
`icon`/`prepend`/`append` when `controlOnly` is set to true
([#7666](elastic/eui#7666))
- Fixed `EuiColorPicker`'s input not setting the correct right padding
for the number of icons displayed
([#7666](elastic/eui#7666))
- Visual fixes for `EuiRange`s with `showInput`:
([#7678](elastic/eui#7678))
  - Longer `append`/`prepend` labels no longer cause a background bug
  - Inputs can no longer overwhelm the actual range in width
- Fixed a visual text alignment regression in `EuiTableRowCell`s with
the `row` header scope
([#7681](elastic/eui#7681))
- Fixed `toolTipProps` type on `EuiSuperUpdateButton` to use
`Partial<EuiToolTipProps>`
([#7692](elastic/eui#7692))
- Fixes missing prop type for `popperProps` on `EuiDatePicker`
([#7694](elastic/eui#7694))
- Fixed a focus bug with `EuiDataGrid`s with `leadingControlColumns`
when moving columns to the left/right
([#7701](elastic/eui#7701))
([#7698](elastic/eui#7698))
- Fixed `EuiSuperDatePicker` to validate date string with respect of
locale on `EuiAbsoluteTab`.
([#7705](elastic/eui#7705))
- Fixed a visual bug with `EuiSuperDatePicker`'s absolute tab on small
mobile screens ([#7708](elastic/eui#7708))
- Fixed i18n of empty and loading state messages for the
`FieldValueSelectionFilter` component
([#7718](elastic/eui#7718))

**Dependency updates**

- Updated `@hello-pangea/dnd` to v16.6.0
([#7599](elastic/eui#7599))
- Updated `remark-rehype` to v8.1.0
([#7601](elastic/eui#7601))

**Accessibility**

- Improved `EuiBasicTable` and `EuiInMemoryTable`'s selection checkboxes
to have unique aria-labels per row
([#7672](elastic/eui#7672))
- Added `aria-valuetext` attributes to `EuiRange`s with tick labels for
improved screen reader UX
([#7675](elastic/eui#7675))
- Updated `EuiAccordion` to keep focus on accordion trigger instead of
moving to content on click/keypress
([#7696](elastic/eui#7696))
- Added `aria-disabled` attribute to `EuiHorizontalSteps` when status is
"disabled" ([#7699](elastic/eui#7699))

---------

Co-authored-by: Tomasz Kajtoch <[email protected]>
mgadewoll pushed a commit to mgadewoll/eui that referenced this pull request May 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[EuiFlexGroup][EuiFlexItem] Add polimorphism support for ReactHTML elements
4 participants