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

Spinner: Convert component to TypeScript #41540

Merged
merged 13 commits into from
Jun 17, 2022

Conversation

opr
Copy link
Contributor

@opr opr commented Jun 3, 2022

What?

Converts the Spinner component to TypeScript

Part of #35744

Why?

Brings us closer to completely typing the components package!

How?

I converted the relevant files to TS. I changed the story so there is now a default one, and one to allow editing of the height and width in a Custom Size story.

Testing Instructions

  1. Run npm run dev and ensure no typing errors.
  2. Run npm run storybook:dev and visit the Spinner component.
  3. Ensure you can change the height/width props in the Custom Size story and that the Spinner displays correctly at different sizes without the stroke width changing.

Screenshots or screencast

cc @ciampo

@opr opr requested a review from ajitbohra as a code owner June 3, 2022 14:28
@ciampo ciampo requested review from mirka, ciampo and chad1008 June 3, 2022 18:10
@ciampo ciampo added [Type] Enhancement A suggestion for improvement. [Package] Components /packages/components [Feature] Component System WordPress component system labels Jun 3, 2022
Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

Hey @opr , thank you for your effort — it's really appreciated!

Could you also add an entry to the CHANGELOG ? Thank you!

Comment on lines 10 to 15
import type { SpinnerProps } from './types';

/**
* @typedef OwnProps
*
* @property {string} [className] Class name
*/
/** @typedef {import('react').ComponentPropsWithoutRef<'svg'> & OwnProps} Props */

/**
* @param {Props} props
* @return {JSX.Element} Element
*/
export default function Spinner( { className, ...props } ) {
export default function Spinner( {
className,
...props
}: SpinnerProps ): JSX.Element {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we use the WordPressComponentProps utility type instead, we can avoid specifying the className prop (and therefore, adding the types.ts file altogether):

diff --git a/packages/components/src/spinner/index.tsx b/packages/components/src/spinner/index.tsx
index f1493a6c1c..ebe47628a2 100644
--- a/packages/components/src/spinner/index.tsx
+++ b/packages/components/src/spinner/index.tsx
@@ -7,12 +7,12 @@ import classNames from 'classnames';
  * Internal dependencies
  */
 import { StyledSpinner, SpinnerTrack, SpinnerIndicator } from './styles';
-import type { SpinnerProps } from './types';
+import type { WordPressComponentProps } from '../ui/context';
 
 export default function Spinner( {
 	className,
 	...props
-}: SpinnerProps ): JSX.Element {
+}: WordPressComponentProps< {}, 'svg', false > ) {
 	return (
 		<StyledSpinner
 			className={ classNames( 'components-spinner', className ) }
diff --git a/packages/components/src/spinner/types.ts b/packages/components/src/spinner/types.ts
deleted file mode 100644
index 596dafdb7a..0000000000
--- a/packages/components/src/spinner/types.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export interface SpinnerProps extends React.ComponentPropsWithoutRef< 'svg' > {
-	className: string;
-}

The WordPressComponentProps< {}, 'svg', false > ) expression means:

  • use the WordPressComponentProps type
  • don't add any extra props (hence the {})
  • add all props and attributes of the svg element (including className)
  • mark the component as non-polymorphic by not accepting the as prop (hence the false), since it should always be rendered as an SVG

We also don't need to explicitly add JSX.Element as a return 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.

Thanks for taking the time to explain this @ciampo this information is really valuable. I'll take this into account on future PRs.

We also don't need to explicitly add JSX.Element as a return value.

Good to know, thank you :)

Comment on lines 20 to 22
const meta: Meta<
typeof Spinner & { size: 'default' | 'medium' | 'large' }
> = {
Copy link
Contributor

@ciampo ciampo Jun 5, 2022

Choose a reason for hiding this comment

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

Let's make a few changes to how this story is written:

  • we need to add a snippet of code to the meta objects to setup controls and show the docs tab by default
  • while we're at it, we could actually remove the extra size control (as it's not a prop on the Spinner component), and instead just create a separate story that illustrates how the Spinner component can be resized to any size, while the stroke width stays unaffected
Sample diff of the changes
diff --git a/packages/components/src/spinner/stories/index.tsx b/packages/components/src/spinner/stories/index.tsx
index e4f8d9c026..2ffbf680d8 100644
--- a/packages/components/src/spinner/stories/index.tsx
+++ b/packages/components/src/spinner/stories/index.tsx
@@ -1,51 +1,34 @@
 /**
  * External dependencies
  */
-import { css } from '@emotion/react';
 import type { Meta, Story } from '@storybook/react';
 
 /**
  * Internal dependencies
  */
-import { useCx } from '../../utils';
 import { space } from '../../ui/utils/space';
 import Spinner from '../';
 
-const sizes = {
-	default: undefined,
-	medium: space( 20 ),
-	large: space( 40 ),
-};
-
-const meta: Meta<
-	typeof Spinner & { size: 'default' | 'medium' | 'large' }
-> = {
-	title: 'Components/Spinner',
+const meta: Meta< typeof Spinner > = {
 	component: Spinner,
-	argTypes: {
-		size: {
-			options: Object.keys( sizes ),
-			mapping: sizes,
-			control: { type: 'select' },
-		},
+	title: 'Components/Spinner',
+	parameters: {
+		controls: { expanded: true },
+		docs: { source: { state: 'open' } },
 	},
 };
 export default meta;
 
-const Template: Story<
-	typeof Spinner & { size: 'default' | 'medium' | 'large' }
-> = ( { size } ) => {
-	const cx = useCx();
-	const spinnerClassname = cx( css`
-		width: ${ size };
-		height: ${ size };
-	` );
-	return <Spinner className={ spinnerClassname } />;
-};
+const Template: Story< typeof Spinner > = ( args ) => <Spinner { ...args } />;
+
+export const Default: Story< typeof Spinner > = Template.bind( {} );
+Default.args = {};
 
-export const Default: Story<
-	typeof Spinner & { size: 'default' | 'medium' | 'large' }
-> = Template.bind( {} );
-Default.args = {
-	size: 'default',
+// The width of the Spinner's border is not affected by the overall component's dimensions.
+export const CustomSize: Story< typeof Spinner > = Template.bind( {} );
+CustomSize.args = {
+	style: {
+		width: space( 20 ),
+		height: space( 20 ),
+	},
 };

Copy link
Contributor Author

@opr opr Jun 6, 2022

Choose a reason for hiding this comment

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

This seems much better thanks for the tip! Btw I used ComponentMeta and ComponentStory as they seem more correct since we're documenting React components, other components' stories use these types too.

@opr opr requested a review from ciampo June 6, 2022 13:12
Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

Apart from a small comment above, code changes LGTM!

Just wanted to double-check with @mirka about the props inherited from WordPressComponentProps not showing up in Storybook — is that expected?

Screenshot 2022-06-09 at 11 35 05

Copy link
Contributor

@chad1008 chad1008 left a comment

Choose a reason for hiding this comment

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

I'm still new to Typescript so I'll defer to @ciampo's approval there, but this looks good to me from a Storybook perspective! I'm also curious about the WordPressComponentProps not loading in the docs like @ciampo pointed out, but I think we can explore that further in another PR.

@mirka
Copy link
Member

mirka commented Jun 9, 2022

Just wanted to double-check with @mirka about the props inherited from WordPressComponentProps not showing up in Storybook — is that expected?

It's expected that the inherited props (i.e. second arg of WordPressComponentProps<>) is not shown, as that would be too much. However, the code in this PR also prevents Spinner's own props from showing — which is currently unnoticeable because Spinner doesn't have its own props (yet) 😄

To get the docgen to work, we need a named export:

// This named export is necessary for the docgen to work
export function Spinner() { /* ... */ }

export default Spinner;

Then, doing that shows me this in the props table:

ref prop

Apparently this is suppressed by taking a forwardedRef arg like this. We might as well forward this ref, I guess?

@ciampo
Copy link
Contributor

ciampo commented Jun 10, 2022

Good catch, @mirka !

To get the docgen to work, we need a named export:

I wasn't expecting any props to show in the table and therefore I didn't look for the named export 😅

Apparently this is suppressed by taking a forwardedRef arg like this. We might as well forward this ref, I guess?

Yes, agreed. Let's wrap the component in a forwardRef call

@mirka
Copy link
Member

mirka commented Jun 10, 2022

Sorry, changed my mind. Given that adding ref forwarding could be a breaking change, and we don't have a solid use case for forwarding the ref, I think it may be better to omit the ref for now. (As was done for RadioControl.) At the very least, it's a runtime change that shouldn't be part of this TS migration PR. What do you think?

@ciampo
Copy link
Contributor

ciampo commented Jun 10, 2022

What do you think?

I'm not sure about how this can introduce a breaking change in the case of the Spinner component — as of today, the component wasn't using ref from its own props, and I don't think that we fall in the scenarios highlighted here.

I think we may be able to introduce forwardRef safely

@mirka
Copy link
Member

mirka commented Jun 10, 2022

Ok, yeah... I think I'm convinced that it's safe, and we might as well do it here instead of omitting.

@opr
Copy link
Contributor Author

opr commented Jun 10, 2022

@ciampo and @mirka - I added fda74b1 - I copied other components that were using contextConnect as this seems the way the other do it, is this what you had in mind?

I also note that the ref prop is missing from Storybook now :)

Let me know if I should have done it differently or if this is fine.

Thanks! 😁

@opr opr requested a review from ciampo June 10, 2022 21:53
@ciampo
Copy link
Contributor

ciampo commented Jun 13, 2022

Thank you for the update, @opr !

I had a look at your latest changes:

  • a requirement of the migration is that we add, next to the default export, a named export of the component for the version that is wrapped in the ref forwarding. For that component, we need to use the
  • in your current approach, your exporting the non-forwarded component (Spinner) and you're calling the connected component ConnectedSpinner. Instead we should:
    • Name the "raw" component UnconnectedSpinner
    • Name the connected component Spinner
    • Name-export the connected component (Spinner), instead of the "raw" component
  • Finally, we have two choices when it comes to adding ref forwarding
    • Use React's forwardRef
    • Use the utility function contextConnect, which hooks the component into the library's context system
    • If we wanted to use the context system, we should also call the useContextSystem hook in the component's code (as explained in the contributing guidelines highlighted in the previous point)
    • Since the component doesn't currently use the context system, my preference would be to use the simpler forwardRef function from React
Here's how I would apply the feedback to this PR
diff --git a/packages/components/src/spinner/index.tsx b/packages/components/src/spinner/index.tsx
index c36778161f..60a1d787cd 100644
--- a/packages/components/src/spinner/index.tsx
+++ b/packages/components/src/spinner/index.tsx
@@ -1,17 +1,23 @@
 /**
  * External dependencies
  */
+import type { ForwardedRef } from 'react';
 import classNames from 'classnames';
 
+/**
+ * WordPress dependencies
+ */
+import { forwardRef } from '@wordpress/element';
+
 /**
  * Internal dependencies
  */
 import { StyledSpinner, SpinnerTrack, SpinnerIndicator } from './styles';
-import { WordPressComponentProps, contextConnect } from '../ui/context';
+import type { WordPressComponentProps } from '../ui/context';
 
-export function Spinner(
+function UnforwardedSpinner(
 	{ className, ...props }: WordPressComponentProps< {}, 'svg', false >,
-	forwardedRef: React.ForwardedRef< any >
+	forwardedRef: ForwardedRef< any >
 ) {
 	return (
 		<StyledSpinner
@@ -40,6 +46,18 @@ export function Spinner(
 	);
 }
 
-const ConnectedSpinner = contextConnect( Spinner, 'Spinner' );
+/**
+ * `Spinner` is a component used to notify users that their action is being processed.
+ *
+ * @example
+ * ```js
+ *   import { Spinner } from '@wordpress/components';
+ *
+ *   function Example() {
+ *     return <Spinner />;
+ *   }
+ * ```
+ */
+export const Spinner = forwardRef( UnforwardedSpinner );
 
-export default ConnectedSpinner;
+export default Spinner;

Finally, I'm sorry if this aspect of the TypeScript migration was not super clear, but we really appreciate your help on highlighting any friction and allow us to improve the process — in fact we're working on enhancing the guidelines in #41669!

@opr
Copy link
Contributor Author

opr commented Jun 16, 2022

@ciampo thanks for your feedback! I have applied the changes you suggested. Your comment was super helpful.

@opr opr force-pushed the update/spinner-typescript branch from 76a4502 to df3885f Compare June 16, 2022 11:09
@ciampo
Copy link
Contributor

ciampo commented Jun 17, 2022

Thank you again, @opr !

Hope to collaborate with you more often in the future :)

@ciampo ciampo merged commit c965181 into WordPress:trunk Jun 17, 2022
@github-actions github-actions bot added this to the Gutenberg 13.6 milestone Jun 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Component System WordPress component system [Package] Components /packages/components [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants