Skip to content

Conversation

@louwie17
Copy link
Contributor

@louwie17 louwie17 commented Oct 4, 2022

All Submissions:

Changes proposed in this Pull Request:

This adds an AsyncSelectControl component that is a wrapper around the SelectControl component.
It adds a onSearch prop that expects a promise with the search results.

If a user decides to pass in the pageSize and total, if the total is less then the pageSize it will do the filtering on the client side and not hit onSearch for every key stroke.

Partially addresses #34331

How to test the changes in this Pull Request:

  1. Load this branch and run pnpm --filter=@woocommerce/storybook storybook
  2. Check out the Async and Async with client side filtering under the experimental select control.
  3. The latter should not show the spinner after the initial items have been retrieved.

Other information:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes, as applicable?
  • Have you successfully run tests with your changes locally?
  • Have you created a changelog file for each project being changed, ie pnpm --filter=<project> run changelog add?

FOR PR REVIEWER ONLY:

  • I have reviewed that everything is sanitized/escaped appropriately for any SQL or XSS injection possibilities. I made sure Linting is not ignored or disabled.

@louwie17 louwie17 requested a review from a team October 4, 2022 19:52
@github-actions github-actions bot added the package: @woocommerce/components issues related to @woocommerce/components label Oct 4, 2022
@github-actions
Copy link
Contributor

github-actions bot commented Oct 4, 2022

Test Results Summary

Commit SHA: e7a3277

Test 🧪Passed ✅Failed 🚨Broken 🚧Skipped ⏭️Unknown ❔Total 📊Duration ⏱️
API Tests15300201550m 37s
E2E Tests187002018915m 19s

To view the full API test report, click here.
To view the full E2E test report, click here.
To view all test reports, visit the WooCommerce Test Reports Dashboard.

@louwie17 louwie17 force-pushed the add/34331_add_async_select_control branch from a5ed9a7 to 00277fd Compare October 5, 2022 21:06
@louwie17 louwie17 force-pushed the add/34331_add_async_select_control branch from 00277fd to e7a3277 Compare October 6, 2022 15:55
onInputValueChange: ( { inputValue: value, ...changes } ) => {
if ( value !== undefined ) {
setInputValue( value );
onInputChange( value, changes );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@joshuatf note that I did end up updating this logic and exposing the changes as a second param.

I realized we did need this to be triggered all the time, especially in a async multi select, where a user uses there keyboard to select an item. The input value gets cleared afterwards, so if the user hits a down arrow after, the results of the original search value are still being shown, which is wrong.

Copy link
Contributor

@mattsherman mattsherman left a comment

Choose a reason for hiding this comment

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

Overall, looks like a great start on this!

I hit some confusing behavior in Storybook. And, I think there are some issues with the debounce handling.

Bigger picture, I'm finding it a little confusing how the consumer handles "client side" filtering vs fetching... I feel like things could be made more clear and explicit for the consumer, instead of depending on having total and pageSize set appropriately. In fact, I naively feel like the consumer should just be able to set onSearch (or, getFilteredItems... see below) and it can handle whether it is doing things "client side" or going off to the network.

Even bigger picture... I wonder if the separate async wrapper is really needed. Why not just bake this into the base select control and have items and getFilteredItems support passing in a Promise? Or, maybe even just have getFilteredItems... do we really need both items and getFilteredItems? Does the select control need to ever know if it is dealing with "all" the items, or does it really just need to show and allow selection of the filtered items?

};

return (
<>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the Fragment wrapper?

This is unnecessary and makes the story less useful than it could be otherwise. Ideally, stories should be set up to support Storybook controls (https://storybook.js.org/docs/react/essentials/controls).

If you need a wrapper, you can use Storybook decorators.

See the DateTimePickerControl stories for an example of how to structure things to handle controls and decorators.

Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice on this story to use a decorator to show the sample items that will be fetched. That would make it easier to use the story.

.toLowerCase()
.includes( value?.toLowerCase() || '' )
)
.sort( () => 0.5 - Math.random() );
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the random sort order? It makes it harder to understand what is happening in the story and harder to understand if things are working correctly.

const [ isFetching, setIsFetching ] = useState( false );
};

export const AsyncWithClientSideFiltering: React.FC = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really know what's going on here... whether it is a bug in the story, or the component, but I'm getting really confusing behavior when I type things in...

Type r, and only Pear, and Grape appear (why not Orange too?). Continue typing, so ra, and now only Orange appears (why no Grape)?!

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand where the client side filtering comes into play here. I keep on looking at this story and the only the setting of the pageSize and total props differ.

Ah...

If a user decides to pass in the pageSize and total, if the total is less then the pageSize it will do the filtering on the client side and not hit onSearch for every key stroke.

Okay. I think I get it.

Would be nice to use Storybook actions (see DataTimePickerControl example mentioned earlier) to log when onSearch is called. That would help to illustrate what is going on here.

onInputChange?: ( value: string | undefined ) => void;
onInputChange?: (
value: string | undefined,
changes: Partial< Omit< UseComboboxState< ItemType >, 'inputValue' > >
Copy link
Contributor

Choose a reason for hiding this comment

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

It is unclear to me what changes means here... is it all of the combobox state. Why does a consumer care about this?

Copy link
Contributor

Choose a reason for hiding this comment

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

In fact, bigger picture, why does the consumer need onInputChange at all? Shouldn't this implementation be hidden from the consumer?

SelectControlProps< ItemType >,
'onInputChange' | 'items' | 'children'
> & {
onSearch: ( value: string | undefined ) => Promise< ItemType[] >;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why | undefined? Why would we ever call onSearch with value === undefined?

items?: ItemType[];
triggerInitialFetch?: boolean;
pageSize?: number;
total?: number;
Copy link
Contributor

Choose a reason for hiding this comment

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

A slightly more descriptive prop name would be nice. What is this?

return (
<Menu isOpen={ isOpen } getMenuProps={ getMenuProps }>
{ isFetching ? (
<Spinner />
Copy link
Contributor

Choose a reason for hiding this comment

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

Spinner feels a bit odd in a dropdown like this. Maybe if it had some text with it as well it would be better.

}
}, [ remainingSelectControlProps.disabled ] );

const searchDebounced = useMemo(
Copy link
Contributor

Choose a reason for hiding this comment

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

useMemo and useCallback do not guarantee that the functions won't be recreated, even if the dependencies do not change (I hit this in #34963). I think we might want to go with a similar solution as in #34963.

Copy link
Contributor

Choose a reason for hiding this comment

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

Might want to consider useDebounce (see #34963)

Copy link
Contributor

Choose a reason for hiding this comment

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

Otherwise, I think that this will suffer similar issues to what I found in DateTimePickerControl. But, since you use debounce instead of useDebounce, this might actually be "more" okay, as your debounce won't get canceled.

{ ...remainingSelectControlProps }
getFilteredItems={ getFilteredItems }
items={ fetchedItems }
onInputChange={ onInputChange }
Copy link
Contributor

Choose a reason for hiding this comment

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

Why isn't getFilteredItems enough for the consumer? Why is onInputChange needed as well?

@mattsherman
Copy link
Contributor

@louwie17 It would also be useful to add a Form story with this control on it, to allow for easy testing of how it looks and works in a Form. I found some bugs with DateTimePickerControl when I popped it on a Form. It exposed some different ways that consumers could use the control.

@mattsherman
Copy link
Contributor

@louwie17 it would be great to have some unit tests as well for this component.

@louwie17 louwie17 mentioned this pull request Oct 11, 2022
8 tasks
@louwie17
Copy link
Contributor Author

@mattsherman thanks for the detailed review on this, also much appreciate your view and input. I like your suggestion of having getFilteredItems support promises as a return statement as well, this might actually help alleviate most of the work for this.
I will try to work on this a bit more, if it does take longer then expected I might push this off for now, as I am able to make things work fine without this control for now.

@mattsherman
Copy link
Contributor

@louwie17 Sounds good. If we don't need this control now, I'd definitely be supportive of putting it on the back-burner for now.

@louwie17
Copy link
Contributor Author

@louwie17 Sounds good. If we don't need this control now, I'd definitely be supportive of putting it on the back-burner for now.

Yeah in that case I will close this for now and create a separate issue for this. As there is some general support that could be added to the select control.
For example, I am not a huge fan of putting the spinner inside the search results, but like to put a spinner or (busy) icon on the right, like this Github search example: https://ericgio.github.io/react-bootstrap-typeahead/#asynchronous-searching

@louwie17
Copy link
Contributor Author

Closing this in favour of a follow up as part of this issue: #35076

@louwie17 louwie17 closed this Oct 13, 2022
@kalessil kalessil deleted the add/34331_add_async_select_control branch September 25, 2024 06:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: @woocommerce/components issues related to @woocommerce/components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants