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

Animate: refactor to TypeScript #49243

Merged
merged 5 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 0 additions & 60 deletions packages/components/src/animate/index.js

This file was deleted.

75 changes: 75 additions & 0 deletions packages/components/src/animate/index.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that there shouldn't be runtime changes in this file

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* Internal dependencies
*/
import type { AnimateProps, GetAnimateOptions } from './types';

/**
* @param type The animation type
* @return Default origin
*/
function getDefaultOrigin( type?: GetAnimateOptions[ 'type' ] ) {
return type === 'appear' ? 'top' : 'left';
}

/**
* @param options
*
* @return ClassName that applies the animations
*/
export function getAnimateClassName( options: GetAnimateOptions ) {
if ( options.type === 'loading' ) {
return classnames( 'components-animate__loading' );
}

const { type, origin = getDefaultOrigin( type ) } = options;

if ( type === 'appear' ) {
const [ yAxis, xAxis = 'center' ] = origin.split( ' ' );
return classnames( 'components-animate__appear', {
[ 'is-from-' + xAxis ]: xAxis !== 'center',
[ 'is-from-' + yAxis ]: yAxis !== 'middle',
} );
}

if ( type === 'slide-in' ) {
return classnames(
'components-animate__slide-in',
'is-from-' + origin
);
}

return undefined;
}

/**
* Simple interface to introduce animations to components.
*
* ```jsx
* import { Animate, Notice } from '@wordpress/components';
*
* const MyAnimatedNotice = () => (
* <Animate type="slide-in" options={ { origin: 'top' } }>
* { ( { className } ) => (
* <Notice className={ className } status="success">
* <p>Animation finished.</p>
* </Notice>
* ) }
* </Animate>
* );
* ```
*/
export function Animate( { type, options = {}, children }: AnimateProps ) {
Copy link
Member

Choose a reason for hiding this comment

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

It felt odd that we're allowing Animate to not have any type, but I guess that this is by design. After all, if we want to not animate, we would just omit this component from the tree.

Anyway, this was like that before, and I've confirmed that the distributive conditional type support that use case correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It also felt odd to me, but then I saw that the Default Storybook example explicitly mentions that the lack of type prop means no animation 🤷

return children( {
className: getAnimateClassName( {
type,
...options,
} as GetAnimateOptions ),
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 typecast is not ideal, but there wasn't an easy way around it. Given that the Animate component doesn't seem to be currently used in Gutenberg, I thought I wouldn't spend too long on it.

Copy link
Member

Choose a reason for hiding this comment

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

Fair enough 👍

} );
}

export default Animate;
53 changes: 0 additions & 53 deletions packages/components/src/animate/stories/index.js

This file was deleted.

102 changes: 102 additions & 0 deletions packages/components/src/animate/stories/index.tsx
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 rewrote this file to be more verbose, but in a way that allows us to get rid of the Appear helper and to use the Story.args syntax which works well with Storybook controls.

In short, I opted for more verbosity in exchange of more straightforward code.

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* External dependencies
*/
import type { ComponentMeta, ComponentStory } from '@storybook/react';

/**
* Internal dependencies
*/
import { Animate } from '..';
import Notice from '../../notice';

const meta: ComponentMeta< typeof Animate > = {
title: 'Components/Animate',
component: Animate,
parameters: {
controls: { expanded: true },
docs: { source: { state: 'open' } },
},
};
export default meta;

const Template: ComponentStory< typeof Animate > = ( props ) => (
<Animate { ...props } />
);

export const Default: ComponentStory< typeof Animate > = Template.bind( {} );
Default.args = {
children: ( { className } ) => (
<Notice className={ className } status="success">
<p>{ `No default animation. Use one of type = "appear", "slide-in", or "loading".` }</p>
</Notice>
),
};

export const AppearTopLeft: ComponentStory< typeof Animate > = Template.bind(
{}
);
AppearTopLeft.args = {
type: 'appear',
options: { origin: 'top left' },
children: ( { className } ) => (
<Notice className={ className } status="success">
<p>Appear animation. Origin: top left.</p>
</Notice>
),
};
export const AppearTopRight: ComponentStory< typeof Animate > = Template.bind(
{}
);
AppearTopRight.args = {
type: 'appear',
options: { origin: 'top right' },
children: ( { className } ) => (
<Notice className={ className } status="success">
<p>Appear animation. Origin: top right.</p>
</Notice>
),
};
export const AppearBottomLeft: ComponentStory< typeof Animate > = Template.bind(
{}
);
AppearBottomLeft.args = {
type: 'appear',
options: { origin: 'bottom left' },
children: ( { className } ) => (
<Notice className={ className } status="success">
<p>Appear animation. Origin: bottom left.</p>
</Notice>
),
};
export const AppearBottomRight: ComponentStory< typeof Animate > =
Template.bind( {} );
AppearBottomRight.args = {
type: 'appear',
options: { origin: 'bottom right' },
children: ( { className } ) => (
<Notice className={ className } status="success">
<p>Appear animation. Origin: bottom right.</p>
</Notice>
),
};

export const Loading: ComponentStory< typeof Animate > = Template.bind( {} );
Loading.args = {
type: 'loading',
children: ( { className } ) => (
<Notice className={ className } status="success">
<p>Loading animation.</p>
</Notice>
),
};

export const SlideIn: ComponentStory< typeof Animate > = Template.bind( {} );
SlideIn.args = {
type: 'slide-in',
options: { origin: 'left' },
children: ( { className } ) => (
<Notice className={ className } status="success">
<p>Slide-in animation.</p>
</Notice>
),
};
32 changes: 32 additions & 0 deletions packages/components/src/animate/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export type AppearOptions = {
type: 'appear';
origin?:
| 'top'
| 'top left'
| 'top right'
| 'middle'
| 'middle left'
| 'middle right'
| 'bottom'
| 'bottom left'
| 'bottom right';
};
type SlideInOptions = { type: 'slide-in'; origin?: 'left' | 'right' };
type LoadingOptions = { type: 'loading'; origin?: never };
type NoAnimationOptions = { type?: never; origin?: never };

export type GetAnimateOptions =
| AppearOptions
| SlideInOptions
| LoadingOptions
| NoAnimationOptions;

// Create a new type that and distributes the `Pick` operator separately to
// every individual type of a union, thus preserving that same union.
type DistributiveTypeAndOptions< T extends { type?: any } > = T extends any
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@chad1008 does this look familiar?

? Pick< T, 'type' > & { options?: Omit< T, 'type' > }
: never;

export type AnimateProps = DistributiveTypeAndOptions< GetAnimateOptions > & {
children: ( props: { className?: string } ) => JSX.Element;
};