Repository: microsoft/fluent-ui-react (ts)

Issue:
Animation component depends on theme styles
## Bug Report

`<Animation>` depends on styles in a theme. That means that for Animation component to work in your custom theme, you need to copy & paste the Animation styles from Teams theme 💣

```jsx
const spinner = {
  keyframe: {
    from: {
      transform: "rotate(0deg)"
    },
    to: {
      transform: "rotate(360deg)"
    }
  },
  duration: "5s",
  iterationCount: "infinite"
}

ReactDOM.render(
  <>
    <Provider theme={mergeThemes(themes.teams, { animations: { spinner } })}>
      <Animation name="spinner">
        <Box>spins</Box>
      </Animation>
    </Provider>
    <Provider theme={{ animations: { spinner } }}>
      <Animation name="spinner">
        <Box>should spin as well</Box>
      </Animation>
    </Provider>
  </>,
  document.getElementById("root")
)
```

### Version
0.43.0

### Testcase
https://codesandbox.io/s/animation-component-without-teams-5s1yj


Pull request:
chore(Animation): move to function component, styles in theme are not required
# BREAKING CHANGES

Utility styles from the `Animation` component where moved to the component itself. No upgrade path there as there should no be any custom styles for `Animation` component as custom animations are defined in the theme.

---

Fixes #2247.

Also affect affects performance improvements as styles now be recomputed only on demand.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c90135ed1..f5debb59f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 ### BREAKING CHANGES
 - Restricted prop set in the `Checkbox`, `Icon`, `Label`, `Slider`, `Status`, `Text` @layershifter ([#2307](https://github.com/microsoft/fluent-ui-react/pull/2307))
 - Styles caching when no inline overrides are defined is enabled by default; use the `performance` prop on the `Provider` to opt out of this if needed @mnajdova ([#2309](https://github.com/microsoft/fluent-ui-react/pull/2309))
+- Styles for the `Animation` component are removed from Teams theme @layershifter ([#2258](https://github.com/microsoft/fluent-ui-react/pull/2258))
 
 ### Fixes
 - Remove dependency on Lodash in TypeScript typings @layershifter ([#2323](https://github.com/microsoft/fluent-ui-react/pull/2323))
@@ -33,6 +34,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 ### Performance
 - Add styles caching when there aren't inline overrides defined @mnajdova ([#2309](https://github.com/microsoft/fluent-ui-react/pull/2309))
+- Styles for `Animation` component are computed again only on prop changes @layershifter ([#2258](https://github.com/microsoft/fluent-ui-react/pull/2258))
 
 <!--------------------------------[ v0.44.0 ]------------------------------- -->
 ## [v0.44.0](https://github.com/microsoft/fluent-ui-react/tree/v0.44.0) (2020-02-05)
diff --git a/packages/react/src/components/Animation/Animation.tsx b/packages/react/src/components/Animation/Animation.tsx
index cc586f87d..b16895978 100644
--- a/packages/react/src/components/Animation/Animation.tsx
+++ b/packages/react/src/components/Animation/Animation.tsx
@@ -1,17 +1,20 @@
-import * as PropTypes from 'prop-types'
-import * as React from 'react'
+import {
+  ComponentAnimationProp,
+  getUnhandledProps,
+  unstable_createAnimationStyles as createAnimationStyles,
+  unstable_getStyles as getStyles,
+  useTelemetry,
+} from '@fluentui/react-bindings'
 import cx from 'classnames'
 import * as _ from 'lodash'
+import * as PropTypes from 'prop-types'
+import * as React from 'react'
+// @ts-ignore
+import { ThemeContext } from 'react-fela'
 import { Transition } from 'react-transition-group'
 
-import {
-  UIComponent,
-  childrenExist,
-  StyledComponentProps,
-  commonPropTypes,
-  ShorthandFactory,
-} from '../../utils'
-import { ComponentEventHandler } from '../../types'
+import { childrenExist, StyledComponentProps, commonPropTypes } from '../../utils'
+import { ComponentEventHandler, ProviderContextPrepared } from '../../types'
 
 export type AnimationChildrenProp = (props: { classes: string }) => React.ReactNode
 
@@ -139,92 +142,156 @@ export interface AnimationProps extends StyledComponentProps {
 /**
  * An Animation provides animation effects to rendered elements.
  */
-class Animation extends UIComponent<AnimationProps, any> {
-  static create: ShorthandFactory<AnimationProps>
-
-  static className = 'ui-animation'
-
-  static displayName = 'Animation'
-
-  static propTypes = {
-    ...commonPropTypes.createCommon({
-      accessibility: false,
-      as: false,
-      content: false,
-      children: false,
-    }),
-    children: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
-    name: PropTypes.string,
-    delay: PropTypes.string,
-    direction: PropTypes.string,
-    duration: PropTypes.string,
-    fillMode: PropTypes.string,
-    iterationCount: PropTypes.string,
-    keyframeParams: PropTypes.object,
-    playState: PropTypes.string,
-    timingFunction: PropTypes.string,
-    visible: PropTypes.bool,
-    appear: PropTypes.bool,
-    mountOnEnter: PropTypes.bool,
-    unmountOnExit: PropTypes.bool,
-    timeout: PropTypes.oneOfType([
-      PropTypes.number,
-      PropTypes.shape({
-        appear: PropTypes.number,
-        enter: PropTypes.number,
-        exit: PropTypes.number,
-      }),
-    ]),
-    onEnter: PropTypes.func,
-    onEntering: PropTypes.func,
-    onEntered: PropTypes.func,
-    onExit: PropTypes.func,
-    onExiting: PropTypes.func,
-    onExited: PropTypes.func,
-  }
-
-  static defaultProps = {
-    timeout: 0,
-  }
-
-  handleAnimationEvent = (
+const Animation: React.FC<AnimationProps> & {
+  className: string
+  handledProps: (keyof AnimationProps)[]
+} = props => {
+  const context: ProviderContextPrepared = React.useContext(ThemeContext)
+  const { setStart, setEnd } = useTelemetry(Animation.displayName, context.telemetry)
+  setStart()
+
+  const {
+    appear,
+    children,
+    className,
+    delay,
+    direction,
+    duration,
+    fillMode,
+    iterationCount,
+    keyframeParams,
+    mountOnEnter,
+    name,
+    playState,
+    timeout,
+    timingFunction,
+    visible,
+    unmountOnExit,
+  } = props
+
+  const handleAnimationEvent = (
     event: 'onEnter' | 'onEntering' | 'onEntered' | 'onExit' | 'onExiting' | 'onExited',
   ) => () => {
-    _.invoke(this.props, event, null, this.props)
+    _.invoke(props, event, null, props)
   }
 
-  renderComponent({ ElementType, classes, unhandledProps }) {
-    const { children, mountOnEnter, unmountOnExit, timeout, appear, visible } = this.props
-
-    const isChildrenFunction = typeof children === 'function'
-
-    const child =
-      childrenExist(children) &&
-      !isChildrenFunction &&
-      (React.Children.only(children) as React.ReactElement<any>)
-
-    return (
-      <Transition
-        in={visible}
-        appear={appear}
-        mountOnEnter={mountOnEnter}
-        unmountOnExit={unmountOnExit}
-        timeout={timeout}
-        onEnter={this.handleAnimationEvent('onEnter')}
-        onEntering={this.handleAnimationEvent('onEntering')}
-        onEntered={this.handleAnimationEvent('onEntered')}
-        onExit={this.handleAnimationEvent('onExit')}
-        onExiting={this.handleAnimationEvent('onExiting')}
-        onExited={this.handleAnimationEvent('onExited')}
-        {...unhandledProps}
-        className={!isChildrenFunction ? cx(classes.root, (child as any).props.className) : ''}
-      >
-        {isChildrenFunction
-          ? () => (children as AnimationChildrenProp)({ classes: classes.root })
-          : child}
-      </Transition>
-    )
-  }
+  const { classes } = React.useMemo(() => {
+    const animation: ComponentAnimationProp = {
+      name,
+      keyframeParams,
+      duration,
+      delay,
+      iterationCount,
+      direction,
+      fillMode,
+      playState,
+      timingFunction,
+    }
+
+    return getStyles({
+      className: Animation.className,
+      displayName: Animation.displayName,
+      props: {
+        className,
+        styles: createAnimationStyles(animation, context.theme),
+      },
+
+      disableAnimations: context.disableAnimations,
+      renderer: context.renderer,
+      rtl: context.rtl,
+      performance: {},
+      saveDebug: _.noop,
+      theme: context.theme,
+    })
+  }, [
+    className,
+    context,
+    name,
+    delay,
+    direction,
+    duration,
+    fillMode,
+    iterationCount,
+    keyframeParams,
+    playState,
+    timingFunction,
+  ])
+  const unhandledProps = getUnhandledProps(Animation.handledProps, props)
+
+  const isChildrenFunction = typeof children === 'function'
+  const child =
+    childrenExist(children) &&
+    !isChildrenFunction &&
+    (React.Children.only(children) as React.ReactElement)
+
+  const element = (
+    <Transition
+      in={visible}
+      appear={appear}
+      mountOnEnter={mountOnEnter}
+      unmountOnExit={unmountOnExit}
+      timeout={timeout}
+      onEnter={handleAnimationEvent('onEnter')}
+      onEntering={handleAnimationEvent('onEntering')}
+      onEntered={handleAnimationEvent('onEntered')}
+      onExit={handleAnimationEvent('onExit')}
+      onExiting={handleAnimationEvent('onExiting')}
+      onExited={handleAnimationEvent('onExited')}
+      {...unhandledProps}
+      className={!isChildrenFunction ? cx(classes.root, (child as any).props.className) : ''}
+    >
+      {isChildrenFunction
+        ? () => (children as AnimationChildrenProp)({ classes: classes.root })
+        : child}
+    </Transition>
+  )
+  setEnd()
+
+  return element
+}
+
+Animation.className = 'ui-animation'
+Animation.displayName = 'Animation'
+
+Animation.defaultProps = {
+  timeout: 0,
+}
+Animation.propTypes = {
+  ...commonPropTypes.createCommon({
+    accessibility: false,
+    as: false,
+    content: false,
+    children: false,
+  }),
+  children: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
+  name: PropTypes.string,
+  delay: PropTypes.string,
+  direction: PropTypes.string,
+  duration: PropTypes.string,
+  fillMode: PropTypes.string,
+  iterationCount: PropTypes.string,
+  keyframeParams: PropTypes.object,
+  playState: PropTypes.string,
+  timingFunction: PropTypes.string,
+  visible: PropTypes.bool,
+  appear: PropTypes.bool,
+  mountOnEnter: PropTypes.bool,
+  unmountOnExit: PropTypes.bool,
+  timeout: PropTypes.oneOfType([
+    PropTypes.number,
+    PropTypes.shape({
+      appear: PropTypes.number,
+      enter: PropTypes.number,
+      exit: PropTypes.number,
+    }),
+  ]),
+  onEnter: PropTypes.func,
+  onEntering: PropTypes.func,
+  onEntered: PropTypes.func,
+  onExit: PropTypes.func,
+  onExiting: PropTypes.func,
+  onExited: PropTypes.func,
 }
+Animation.handledProps = Object.keys(Animation.propTypes) as any
 
 export default Animation
diff --git a/packages/react/src/themes/teams/componentStyles.ts b/packages/react/src/themes/teams/componentStyles.ts
index 2da1609a2..007b33b12 100644
--- a/packages/react/src/themes/teams/componentStyles.ts
+++ b/packages/react/src/themes/teams/componentStyles.ts
@@ -103,8 +103,6 @@ export { default as Tree } from './components/Tree/treeStyles'
 export { default as TreeItem } from './components/Tree/treeItemStyles'
 export { default as TreeTitle } from './components/Tree/treeTitleStyles'
 
-export { default as Animation } from './components/Animation/animationStyles'
-
 export { default as Video } from './components/Video/videoStyles'
 
 export { default as Tooltip } from './components/Tooltip/tooltipStyles'
diff --git a/packages/react/src/themes/teams/componentVariables.ts b/packages/react/src/themes/teams/componentVariables.ts
index 025fd37ba..e43df336c 100644
--- a/packages/react/src/themes/teams/componentVariables.ts
+++ b/packages/react/src/themes/teams/componentVariables.ts
@@ -85,8 +85,6 @@ export { default as ToolbarMenuRadioGroup } from './components/Toolbar/toolbarMe
 
 export { default as TreeTitle } from './components/Tree/treeTitleVariables'
 
-export { default as Animation } from './components/Animation/animationVariables'
-
 export { default as Video } from './components/Video/videoVariables'
 
 export { default as Tooltip } from './components/Tooltip/tooltipVariables'
diff --git a/packages/react/src/themes/teams/components/Animation/animationStyles.ts b/packages/react/src/themes/teams/components/Animation/animationStyles.ts
deleted file mode 100644
index aeacc8904..000000000
--- a/packages/react/src/themes/teams/components/Animation/animationStyles.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import {
-  ComponentAnimationProp,
-  unstable_createAnimationStyles as createAnimationStyles,
-} from '@fluentui/react-bindings'
-
-export default {
-  root: ({ props: p, theme }) => {
-    const animation: ComponentAnimationProp = {
-      name: p.name,
-      keyframeParams: p.keyframeParams,
-      duration: p.duration,
-      delay: p.delay,
-      iterationCount: p.iterationCount,
-      direction: p.direction,
-      fillMode: p.fillMode,
-      playState: p.playState,
-      timingFunction: p.timingFunction,
-    }
-
-    return createAnimationStyles(animation, theme)
-  },
-}
diff --git a/packages/react/src/themes/teams/components/Animation/animationVariables.ts b/packages/react/src/themes/teams/components/Animation/animationVariables.ts
deleted file mode 100644
index ead516c97..000000000
--- a/packages/react/src/themes/teams/components/Animation/animationVariables.ts
+++ /dev/null
@@ -1,1 +0,0 @@
-export default () => {}


Test patch:
diff --git a/packages/react/test/specs/components/Animation/Animation-test.tsx b/packages/react/test/specs/components/Animation/Animation-test.tsx
index 628a7b55b..e95f8955f 100644
--- a/packages/react/test/specs/components/Animation/Animation-test.tsx
+++ b/packages/react/test/specs/components/Animation/Animation-test.tsx
@@ -5,6 +5,7 @@ import Animation from 'src/components/Animation/Animation'
 
 describe('Animation', () => {
   isConformant(Animation, {
+    constructorName: 'Animation',
     hasAccessibilityProp: false,
     requiredProps: { children: <div /> },
     handlesAsProp: false,
diff --git a/packages/react/test/specs/utils/felaRenderer-test.tsx b/packages/react/test/specs/utils/felaRenderer-test.tsx
index dce6e3062..524ccafcc 100644
--- a/packages/react/test/specs/utils/felaRenderer-test.tsx
+++ b/packages/react/test/specs/utils/felaRenderer-test.tsx
@@ -6,31 +6,6 @@ import Animation from 'src/components/Animation/Animation'
 import Provider from 'src/components/Provider/Provider'
 import Text from 'src/components/Text/Text'
 import { felaRenderer } from 'src/utils'
-import {
-  ComponentAnimationProp,
-  unstable_createAnimationStyles as createAnimationStyles,
-} from '@fluentui/react-bindings'
-
-// Animation component depends on theme styles 💣
-// Issue: https://github.com/microsoft/fluent-ui-react/issues/2247
-// This adds required styles when needed.
-const AnimationComponentStyles = {
-  root: ({ props: p, theme }) => {
-    const animation: ComponentAnimationProp = {
-      name: p.name,
-      keyframeParams: p.keyframeParams,
-      duration: p.duration,
-      delay: p.delay,
-      iterationCount: p.iterationCount,
-      direction: p.direction,
-      fillMode: p.fillMode,
-      playState: p.playState,
-      timingFunction: p.timingFunction,
-    }
-
-    return createAnimationStyles(animation, theme)
-  },
-}
 
 describe('felaRenderer', () => {
   test('basic styles are rendered', () => {
@@ -75,7 +50,6 @@ describe('felaRenderer', () => {
     const snapshot = createSnapshot(
       <Provider
         theme={{
-          componentStyles: { Animation: AnimationComponentStyles },
           animations: { colorChanger },
         }}
       >
@@ -106,7 +80,6 @@ describe('felaRenderer', () => {
     const snapshot = createSnapshot(
       <Provider
         theme={{
-          componentStyles: { Animation: AnimationComponentStyles },
           animations: { colorChanger },
         }}
       >
@@ -138,7 +111,6 @@ describe('felaRenderer', () => {
       <Provider
         disableAnimations
         theme={{
-          componentStyles: { Animation: AnimationComponentStyles },
           animations: { spinner },
         }}
       >