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

Extract the search component into @automattic/search #46424

Merged
merged 23 commits into from
Oct 21, 2020

Conversation

sarayourfriend
Copy link
Contributor

@sarayourfriend sarayourfriend commented Oct 13, 2020

Rationale

We need to extract this component so that it can be used inside of the language-picker component. I've also moved it outside of @automattic/components for the same reasons laid out here: #46020 (comment)

Changes proposed in this Pull Request

  • Extract the search component from Calypso into a separate package
  • Removes analytics from the component. Consumers will need to implement their own using the appropriate hooks.

This is more or less a 1-to-1 reimplementation of the existing component. There are some differences, however:

  • Updated ref usage
  • Using WordPress components rather than Calypso components and icons
  • Using TypeScript and fixing some potential coding errors
  • Add a wrapping noop form to cause iOS to display a "Search" button rather than "return"

iOS displaying a Search button instead of Return on the soft keyboard

Testing instructions

  • Run the new storybook command yarn run search:storybook:start and ensure the examples work as expected and cover the different use cases

Note: I didn't port over the existing tests because they aren't really that useful of tests.

Questions

  1. The way this component is written it can go from controlled to uncontrolled (if you start to pass in value later in the component lifecycle. We could vastly simplify this component if we turned it into a fully uncontrolled component. For example, if we did this, we could remove the derived state logic and the "responsiveness" of the component to certain props changing (like do we really need to be able to handle changes in delaySearch? That seems like a waste and a supremely unlikely edge case usage). Shall we move forward with turning this component into an uncontrolled component (i.e., by not responding to changes in "static" props and not allowing value to be passed in?)

Fixes #

@matticbot
Copy link
Contributor

@sarayourfriend sarayourfriend force-pushed the try/extract-search branch 5 times, most recently from eab26ea to 28fe17d Compare October 15, 2020 00:19
@sarayourfriend sarayourfriend changed the title Try extracting search component Extract the search component into @automattic/search Oct 15, 2020
@sarayourfriend sarayourfriend marked this pull request as ready for review October 15, 2020 16:44
@sarayourfriend sarayourfriend requested a review from tyxla October 15, 2020 16:44
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Oct 15, 2020
tabIndex={ enableOpenIcon ? 0 : undefined }
onKeyDown={ enableOpenIcon ? this.openListener : undefined }
aria-controls={ 'search-component-' + this.instanceId }
aria-label={ __( 'Open Search' ) }
Copy link

Choose a reason for hiding this comment

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

ℹ️ String reuse speeds up translation and improves consistency. The following string might make a good alternative and has already been translated 31 times:
translate( 'Open Search', { context: 'button label'} ) ES Score: 14

@matticbot
Copy link
Contributor

This PR does not affect the size of JS and CSS bundles shipped to the user's browser.

Generated by performance advisor bot at iscalypsofastyet.com.

Copy link
Member

@tyxla tyxla left a comment

Choose a reason for hiding this comment

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

Nice work @saramarcondes.

I'm definitely interested to see a simplification of this component, but it's probably best to be done in another PR. Maybe we could write some tests and then do the refactor.

Yet, I wonder if refactoring it as uncontrolled makes sense - are we going to keep all the functionality that it already offers?

I've left some thoughts, but this generally looks like pretty solid work!

packages/search/src/search.stories.tsx Outdated Show resolved Hide resolved
packages/search/src/search.tsx Outdated Show resolved Hide resolved
packages/search/src/search.tsx Outdated Show resolved Hide resolved
packages/search/src/search.tsx Show resolved Hide resolved
packages/search/src/search.tsx Outdated Show resolved Hide resolved
packages/search/src/search.tsx Outdated Show resolved Hide resolved
};

UNSAFE_componentWillReceiveProps( nextProps: Props ) {
if ( this.props.isOpen !== nextProps.isOpen ) {
Copy link
Member

Choose a reason for hiding this comment

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

This feels a bit flawed. The component state can be changed by user intent but could be overridden by another prop change, feels a bit off. I'd expect isOpen to be used only for the initial state. Is there a real use case where this behavior is necessary?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe there is a valid use case where the parent wants to close the search, no matter what the internal state is?

These cases would maybe be covered if the search component auto-closed when it loses focus -- means that another UI element is now active.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These cases would maybe be covered if the search component auto-closed when it loses focus -- means that another UI element is now active.

For this to work we'd need to always auto-focus. I'm not opposed to that given that a similar component in the new P2 follows this behavior but my understanding is that auto-focusing is undesirable for accessibility reasons. Maybe that doesn't apply for when you're "auto-focusing" on a new element after activating a button that makes the new element appear?

Copy link
Member

Choose a reason for hiding this comment

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

As far as I understand, auto-focusing is always intrusive from an accessibility point of view. I might be wrong, of course - @diegohaz would know best.

Copy link
Contributor

Choose a reason for hiding this comment

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

Auto-focus is okay if followed by a user action that makes sense. For example, clicking on a <label> should focus on the input; opening a dialog should move focus into it; deleting the currently focused row in a table could move focus somewhere else (like the next or previous rows).

In this component, I would expect that the input would be focused after clicking on the search icon.

isOpen: this.props.isOpen || false,
} );

if ( this.searchInput?.current ) {
Copy link
Member

Choose a reason for hiding this comment

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

this.searchInput always exists and always has the current property. It's the value of current that can be null. The optional chaining is not needed.

this.searchInput?.current?.blur();
this.openIcon?.current?.focus();
} else {
this.searchInput?.current?.focus();
Copy link
Member

Choose a reason for hiding this comment

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

Same here: only the ?. after current needs to be optional.

};

UNSAFE_componentWillReceiveProps( nextProps: Props ) {
if ( this.props.isOpen !== nextProps.isOpen ) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe there is a valid use case where the parent wants to close the search, no matter what the internal state is?

These cases would maybe be covered if the search component auto-closed when it loses focus -- means that another UI element is now active.

packages/search/src/search.tsx Outdated Show resolved Hide resolved
hasFocus: this.props.autoFocus,
};

UNSAFE_componentWillReceiveProps( nextProps: Props ) {
Copy link
Member

Choose a reason for hiding this comment

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

A new component shouldn't use the legacy method. The new getDerivedStateFromProps method is preferred. Or even better, do a functional component with hooks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This component cannot actually use getDerivedStateFromProps because it's being controlled externally and internally. It specifically needs to respond to changes in isOpen. If we turn this into an uncontrolled component then we can remove this logic and replace it with a defaultIsOpen as you suggested in another comment. Then consumers that need the component to respond to external isOpen may add a key prop that depends on their isOpen value to force the component to re-mount with the new default value.

@diegohaz
Copy link
Contributor

Maybe it's out of the scope of this PR since this is just a code extraction. But I wonder why the buttons are <div>s and not <button>s?

@sarayourfriend
Copy link
Contributor Author

But I wonder why the buttons are

s and not s?

I think its within scope for this PR. I'd like to extract and make significant improvements wherever possible! I'll update that, thanks for pointing it out. (Plus we can use WordPress Button... yet another win 😀)

Copy link
Collaborator

@wp-desktop wp-desktop left a comment

Choose a reason for hiding this comment

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

WordPress Desktop CI Failure for job "wp-desktop-mac".

@saramarcondes please inspect this job's build steps for breaking changes at this link. For temporal failures, you may try to "Rerun Workflow from Failed".

Please also ensure this branch is rebased off latest Calypso.

@wp-desktop wp-desktop dismissed their stale review October 19, 2020 16:52

wp-desktop ci passing, closing review

tabIndex={ enableOpenIcon ? 0 : undefined }
onKeyDown={ enableOpenIcon ? this.openListener : undefined }
aria-controls={ 'search-component-' + this.instanceId }
aria-label={ __( 'Open Search' ) }
Copy link

Choose a reason for hiding this comment

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

ℹ️ String reuse speeds up translation and improves consistency. The following string might make a good alternative and has already been translated 31 times:
translate( 'Open Search', { context: 'button label'} ) ES Score: 14

Copy link
Collaborator

@wp-desktop wp-desktop left a comment

Choose a reason for hiding this comment

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

WordPress Desktop CI Failure for job "wp-desktop-mac".

@saramarcondes please inspect this job's build steps for breaking changes at this link. For temporal failures, you may try to "Rerun Workflow from Failed".

Please also ensure this branch is rebased off latest Calypso.

@wp-desktop wp-desktop dismissed their stale review October 19, 2020 19:17

wp-desktop ci passing, closing review

Copy link
Collaborator

@wp-desktop wp-desktop left a comment

Choose a reason for hiding this comment

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

WordPress Desktop CI Failure for job "wp-desktop-mac".

@saramarcondes please inspect this job's build steps for breaking changes at this link. For temporal failures, you may try to "Rerun Workflow from Failed".

Please also ensure this branch is rebased off latest Calypso.

@wp-desktop wp-desktop dismissed their stale review October 19, 2020 20:58

wp-desktop ci passing, closing review

Copy link
Member

@tyxla tyxla left a comment

Choose a reason for hiding this comment

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

Looking really well and testing great!

I've left some nits, but nothing blocking IMHO.

I think it's already in a great state and we can address any potential improvement in subsequent PRs.

Great work @saramarcondes!

},
"devDependencies": {
"@storybook/addon-actions": "^5.3.18",
"enzyme": "^3.11.0"
Copy link
Member

Choose a reason for hiding this comment

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

Since we don't have any tests yet, it seems we can remove enzyme from the list of dependencies, no?

@@ -0,0 +1,52 @@
{
"name": "@automattic/search",
"version": "1.0.0-alpha.3",
Copy link
Member

Choose a reason for hiding this comment

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

Nit: we haven't released that yet, should it be 1.0.0-alpha or beta or even just 1.0.0?

}
}

.search__input-fade .search__text-overlay {
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it makes sense to have all the styles nested in .search, to ensure we're not bleeding any styles beyond the component.

Also, while on that topic, I wonder if it makes sense to change the wrapper .search to a more specific classname. .search seems a bit too common, and IMHO there's a big chance to run into conflicts.

packages/search/CHANGELOG.md Show resolved Hide resolved
@@ -0,0 +1,3 @@
## 1.0.0

- Add Search component
Copy link
Member

Choose a reason for hiding this comment

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

Should we mention that this based on the existing code in Calypso?

/**
* External dependencies
*/
import React, { KeyboardEvent, MouseEvent, FocusEvent, FormEvent, ChangeEvent } from 'react';
Copy link
Member

Choose a reason for hiding this comment

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

Opinionated nit: would be really great if we can sort the imports, both when they're multiple imports from the same module and multiple imports on multiple lines. Makes it more readable IMHO.

Copy link
Contributor Author

@sarayourfriend sarayourfriend Oct 20, 2020

Choose a reason for hiding this comment

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

Shall we enable an ESLint rule for sorted imports?

Copy link
Member

Choose a reason for hiding this comment

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

My personal opinion is YES, but maybe now it's not the time. With the new rule that we recently enabled to make all imports relative to the root of Calypso, any change to existing files will be a mess for contributors.

I say let's reconsider doing at some point in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

/cc @scinos

Copy link
Contributor

Choose a reason for hiding this comment

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

Plenty of options if we decide go that route https://www.npmjs.com/search?q=eslint%20sort%20import

Copy link
Collaborator

@wp-desktop wp-desktop left a comment

Choose a reason for hiding this comment

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

WordPress Desktop CI Failure for job "wp-desktop-mac".

@saramarcondes please inspect this job's build steps for breaking changes at this link. For temporal failures, you may try to "Rerun Workflow from Failed".

Please also ensure this branch is rebased off latest Calypso.

@wp-desktop wp-desktop dismissed their stale review October 21, 2020 17:43

wp-desktop ci passing, closing review

@sarayourfriend sarayourfriend merged commit 3bee2df into master Oct 21, 2020
@sarayourfriend sarayourfriend deleted the try/extract-search branch October 21, 2020 19:06
@matticbot matticbot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Oct 21, 2020
"scripts": {
"clean": "npx rimraf dist && tsc --build --clean",
"prepublish": "yarn run clean",
"prepare": "transpile && tsc && copy-assets"
Copy link
Member

@jsnajdr jsnajdr Oct 30, 2020

Choose a reason for hiding this comment

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

Hi @saramarcondes 👋 I noticed that even though the search package is written fully in TypeScript, the transpilation for the dist directories is done by Babel. The TypeScript compiler is used only to emit the type declarations.

Was this an intentional decision or is it some copy-paste accident? The compilation could be done 100% by the TypeScript compiler, with a pair of tsconfig.json and tsconfig-cjs.json config files. The @automattic/data-stores package does that, for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an accident of copy and paste from the way the components package is configured. Please feel free to open a PR to fix this if you can, I’m not really familiar with how it would work without Babel...

Side note: the language picker package probably has the same problem.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, I'll patch both packages in a PR 👍

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.

8 participants