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

refactor(selectable): avoid prop conflicts #4291

Closed
wants to merge 1 commit into from

Conversation

smhutch
Copy link
Contributor

@smhutch smhutch commented Nov 20, 2020

Summary

I'm planning to open another PR to address #4262. While looking starting working on that, I noticed some code patterns that I see as problematic. The issue I see is that we have cases of props conflicting with each other (in this case top-level props conflicting with seachProps). The current code mostly fixes these issues by creating a ...cleanedSearchProps, which is all of searchProps minus some de-structured values. While this approach works, it's quite unstable. Another engineer could easily confuse these unused variables as "useless", and remove them. Similarly, we need to jump through some hoops to keep TypeScript happy about this approach (in this type casting).

The approach I have taken instead is to not spread ...searchProps over all others. Instead, I have continued to spread these, but added a couple of explicit overrides afterwards.

As this change doesn't explicitly fix an open issue, and is to some extent an opinionated change. I'm interested in hearing some thoughts on my approach from the Elastic team. Thanks!

Related to

#4132
#4262

Checklist

  • Check against all themes for compatibility in both light and dark modes
  • Checked in mobile
  • Checked in Chrome, Safari, Edge, and Firefox
  • Props have proper autodocs
  • Added documentation
  • Checked Code Sandbox works for the any docs examples
  • Added or updated jest tests
  • Checked for breaking changes and labeled appropriately
  • Checked for accessibility including keyboard-only and screenreader modes
  • A changelog entry exists and is marked appropriately

@kibanamachine
Copy link

Since this is a community submitted pull request, a Jenkins build has not been kicked off automatically. Can an Elastic organization member please verify the contents of this patch and then kick off a build manually?

) => {
if (props && props['aria-label']) {
return { 'aria-label': props['aria-label'] };
const getAriaProps = (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I renamed this function, because it's now setting multiple props, or returning undefined if no suitable aria props can be set.

type AriaLabelProps = {
'aria-label': string;
'aria-describedby': undefined;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When using aria-label, always override aria-describedby.

type AriaDescribedbyProps = {
'aria-label': undefined;
'aria-describedby': string;
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As above, do not set aria-label, if aria-described is used.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you changed this logic a little (which was, granted, somewhat convoluted)... Either way, the intended effect is that both aria-label and aria-describedby is allowed. We just need to move where the attribute is being set.

Copy link
Contributor Author

@smhutch smhutch Nov 24, 2020

Choose a reason for hiding this comment

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

@myasonik I'm still not sure where the logic has changed (but I agree, it's somewhat convoluted, and I could certainly be missing something).

I'm happy to make some more changes to accommodate the expected behaviour. Based on your last comment, I think the required logic is:

  • aria-label
    1. Uses searchProps['aria-label'] or listProps['aria-label'] if set
    2. Else, uses aria-label passed to EuiSelectable
    3. Uses a fallback of placeholderName
  • aria-describedby
    1. Uses searchProps['aria-describedby'] or listProps['aria-describedby'] if set
    2. Else, uses aria-describedby passed to EuiSelectable
    3. In either of the above cases, messageContentId is added to the end.
    4. No fallback is used.

Does this look correct? Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry about the long delay on getting back to you @smhutch - I was off for US Thanksgiving for a while but am back at it now!

I think what you outlined is generally correct but I'm going to restate it in a little more depth just to make sure we're on the same page.

First, let's define where we're going to be setting these props. There are 2 possible spots:
1. the input field (which has these values passed along through the ...rest param)
2. the list box (which sets these values using some ref hackiness)

The overarching rule is that each of these can be controlled individually with searchProps or listProps respectively but also need to respect broader values set on EuiSelectable itself.

So, for the input field:

  • aria-label
    1. Use searchProps['aria-label']
    2. Use aria-label passed into EuiSelectable
    3. Use placeholderName
  • aria-describedby
    1. Use searchProps['aria-describedby'] and messageContentId as a space-separated string
    2. Use aria-label passed into EuiSelectable and messageContentId as a space-separated string
    3. Use messageContentId

For the list box, it is the same as above but replace searchProps with listProps.

Writing this up, I also realized we have a gap by not covering aria-labelledby in conjunction with aria-label but I think we ignore that for now and tackle it in a future PR. (If you do want to tackle it, let me know and I can write up some guidelines but it'll double the length of the aria-label logic...)

Comment on lines +476 to +479
type AriaProps = Partial<{
'aria-label': string;
'aria-describedby': string;
}>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Previously this was:

| Partial<EuiSelectableSearchProps<T>>
| EuiSelectableOptionsListPropsWithDefaults

I changed to only require the keys that are needed within this function. Any object that might have these set may be used with this function.

const getAriaProps = (
props: AriaProps,
describedbyId?: string
): AriaPropsReturn => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added an explicit return type. Not 100% needed, but I think it's helpful.

: { 'aria-label': placeholderName })}
{...cleanedSearchProps}
{...searchProps}
onChange={this.onSearchChange}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Instead of spreading ...cleanedSeachProps, I took the approach of not spreading at the end. Any values that come after ...searchProps will override those which are spread.

In this case, onChange and ...ariaProps will override whatever is passed to ...searchProps

I have a feeling that there are more potential issues lurking here (i.e. should searchProps.placeholder override placeholder?)

singleSelection,
isLoading,
listProps,
listProps = {},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Setting this to an empty object by default, avoids the possibility of it being undefined. Which simplifies some other code (no need to check that it's not undefined, before accessing properties). 😄

@@ -333,9 +333,6 @@ export class EuiSelectable<T = {}> extends Component<
}
}
);
if (this.props.searchProps && this.props.searchProps.onSearch) {
this.props.searchProps.onSearch(searchValue);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here we were calling searchProps.onSearch, from a handler which was passed down as onChange. This code is still called, but is now handled within selectable_search.tsx


if (this.props.onSearch) {
this.props.onSearch(value);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Allows consumers to hook into onSearch calls.

incremental
defaultValue={defaultValue}
fullWidth
autoComplete="off"
aria-haspopup="listbox"
{...ariaPropsIfListIsPresent}
{...rest}
onSearch={this.onSearchChange}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

onSearch is moved to here, to prevent ...rest from overriding it.

@smhutch smhutch marked this pull request as ready for review November 20, 2020 13:35
@thompsongl thompsongl self-requested a review November 20, 2020 15:45
@thompsongl
Copy link
Contributor

Thanks for the PR, @smhutch!

Pinging @myasonik to check out the ARIA refactor

@smhutch
Copy link
Contributor Author

smhutch commented Jan 18, 2021

@thompsongl @myasonik

Just revisiting this. I won't have time to update this PR for at least a few more weeks, but I do intend to come back and make the required changes.

@thompsongl
Copy link
Contributor

👍 Thanks for the update, @smhutch

@github-actions
Copy link

👋 Hey there. This PR hasn't had any activity for 90 days. We'll automatically close it if that trend continues for another week. If you feel this issue is still valid and needs attention please let us know with a comment.

@smhutch
Copy link
Contributor Author

smhutch commented Apr 20, 2021

Hey @thompsongl, I started a new job, and don't have time to resolve this any more. Closing for now.

@smhutch smhutch closed this Apr 20, 2021
@smhutch smhutch deleted the selectable-cleanup branch April 20, 2021 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants