diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
index bb0d662c4f67e..6ab9ee79c7412 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
@@ -992,9 +992,11 @@ function returnsNonNode(
         }
       }
     },
+    // Skip traversing all nested functions and their return statements
     ArrowFunctionExpression: skipNestedFunctions(node),
     FunctionExpression: skipNestedFunctions(node),
     FunctionDeclaration: skipNestedFunctions(node),
+    ObjectMethod: node => node.skip(),
   });
 
   return !hasReturn || returnsNonNode;
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.expect.md
new file mode 100644
index 0000000000000..4c89f59919f0f
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.expect.md
@@ -0,0 +1,63 @@
+
+## Input
+
+```javascript
+// @compilationMode(infer)
+
+import {Stringify} from 'shared-runtime';
+
+function Test() {
+  const context = {
+    testFn() {
+      // if it is an arrow function its work
+      return () => 'test'; // it will break compile if returns an arrow fn
+    },
+  };
+
+  return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+  fn: Test,
+  params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
+
+import { Stringify } from "shared-runtime";
+
+function Test() {
+  const $ = _c(1);
+  let t0;
+  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+    const context = {
+      testFn() {
+        return _temp;
+      },
+    };
+
+    t0 = ;
+    $[0] = t0;
+  } else {
+    t0 = $[0];
+  }
+  return t0;
+}
+function _temp() {
+  return "test";
+}
+
+export const FIXTURE_ENTRYPOINT = {
+  fn: Test,
+  params: [{}],
+};
+
+```
+      
+### Eval output
+(kind: ok) 
{"value":{"testFn":{"kind":"Function","result":{"kind":"Function","result":"test"}}},"shouldInvokeFns":true}
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.jsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.jsx
new file mode 100644
index 0000000000000..c8e396b057f8f
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-nested-object-method.jsx
@@ -0,0 +1,19 @@
+// @compilationMode(infer)
+
+import {Stringify} from 'shared-runtime';
+
+function Test() {
+  const context = {
+    testFn() {
+      // if it is an arrow function its work
+      return () => 'test'; // it will break compile if returns an arrow fn
+    },
+  };
+
+  return ;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+  fn: Test,
+  params: [{}],
+};
diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js
index 8204643a3ec2f..688688bcd6c65 100644
--- a/fixtures/view-transition/src/components/Page.js
+++ b/fixtures/view-transition/src/components/Page.js
@@ -57,7 +57,7 @@ export default function Page({url, navigate}) {
         }}>
         {show ? 'A' : 'B'}
       
-      
+      
         
           {show ? (
             
@@ -92,7 +92,7 @@ export default function Page({url, navigate}) {
               
!!
             
           
-          {show ? 
 : 
 
}
+          {show ? 
 : null}
         
 
       
      
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index 8e98b763b7afb..e57f54e6687e6 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -203,7 +203,10 @@ import {
   OffscreenDetached,
   OffscreenPassiveEffectsConnected,
 } from './ReactFiberActivityComponent';
-import {getViewTransitionName} from './ReactFiberViewTransitionComponent';
+import {
+  getViewTransitionName,
+  getViewTransitionClassName,
+} from './ReactFiberViewTransitionComponent';
 import {
   TransitionRoot,
   TransitionTracingMarker,
@@ -504,7 +507,7 @@ function commitBeforeMutationEffectsOnFiber(
             // We should just stash the parent ViewTransitionComponent and continue
             // walking the tree until we find HostComponent but to do that we need
             // to use a stack which requires refactoring this phase.
-            commitBeforeUpdateViewTransition(current);
+            commitBeforeUpdateViewTransition(current, finishedWork);
           }
         }
         break;
@@ -663,24 +666,31 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
               'Found a pair with an auto name. This is a bug in React.',
             );
           }
-          // We found a new appearing view transition with the same name as this deletion.
-          // We'll transition between them.
-          viewTransitionHostInstanceIdx = 0;
-          const inViewport = applyViewTransitionToHostInstances(
-            child.child,
-            props.name,
+          const name = props.name;
+          const className: ?string = getViewTransitionClassName(
             props.className,
-            null,
-            false,
+            props.share,
           );
-          if (!inViewport) {
-            // This boundary is exiting within the viewport but is going to leave the viewport.
-            // Instead, we treat this as an exit of the previous entry by reverting the new name.
-            // Ideally we could undo the old transition but it's now too late. It's also on its
-            // on snapshot. We have know was for it to paint onto the original group.
-            // TODO: This will lead to things unexpectedly having exit animations that normally
-            // wouldn't happen. Consider if we should just let this fly off the screen instead.
-            restoreViewTransitionOnHostInstances(child.child, false);
+          if (className !== 'none') {
+            // We found a new appearing view transition with the same name as this deletion.
+            // We'll transition between them.
+            viewTransitionHostInstanceIdx = 0;
+            const inViewport = applyViewTransitionToHostInstances(
+              child.child,
+              name,
+              className,
+              null,
+              false,
+            );
+            if (!inViewport) {
+              // This boundary is exiting within the viewport but is going to leave the viewport.
+              // Instead, we treat this as an exit of the previous entry by reverting the new name.
+              // Ideally we could undo the old transition but it's now too late. It's also on its
+              // on snapshot. We have know was for it to paint onto the original group.
+              // TODO: This will lead to things unexpectedly having exit animations that normally
+              // wouldn't happen. Consider if we should just let this fly off the screen instead.
+              restoreViewTransitionOnHostInstances(child.child, false);
+            }
           }
         }
       }
@@ -691,29 +701,37 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
 
 function commitEnterViewTransitions(placement: Fiber): void {
   if (placement.tag === ViewTransitionComponent) {
+    const state: ViewTransitionState = placement.stateNode;
     const props: ViewTransitionProps = placement.memoizedProps;
-    const name = getViewTransitionName(props, placement.stateNode);
-    viewTransitionHostInstanceIdx = 0;
-    const inViewport = applyViewTransitionToHostInstances(
-      placement.child,
-      name,
+    const name = getViewTransitionName(props, state);
+    const className: ?string = getViewTransitionClassName(
       props.className,
-      null,
-      false,
+      state.paired ? props.share : props.enter,
     );
-    if (!inViewport) {
-      // TODO: If this was part of a pair we will still run the onShare callback.
-      // Revert the transition names. This boundary is not in the viewport
-      // so we won't bother animating it.
-      restoreViewTransitionOnHostInstances(placement.child, false);
-      // TODO: Should we still visit the children in case a named one was in the viewport?
-    } else {
-      commitAppearingPairViewTransitions(placement);
+    if (className !== 'none') {
+      viewTransitionHostInstanceIdx = 0;
+      const inViewport = applyViewTransitionToHostInstances(
+        placement.child,
+        name,
+        className,
+        null,
+        false,
+      );
+      if (!inViewport) {
+        // TODO: If this was part of a pair we will still run the onShare callback.
+        // Revert the transition names. This boundary is not in the viewport
+        // so we won't bother animating it.
+        restoreViewTransitionOnHostInstances(placement.child, false);
+        // TODO: Should we still visit the children in case a named one was in the viewport?
+      } else {
+        commitAppearingPairViewTransitions(placement);
 
-      const state: ViewTransitionState = placement.stateNode;
-      if (!state.paired) {
-        scheduleViewTransitionEvent(placement, props.onEnter);
+        if (!state.paired) {
+          scheduleViewTransitionEvent(placement, props.onEnter);
+        }
       }
+    } else {
+      commitAppearingPairViewTransitions(placement);
     }
   } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
     let child = placement.child;
@@ -752,28 +770,34 @@ function commitDeletedPairViewTransitions(
         if (name != null && name !== 'auto') {
           const pair = appearingViewTransitions.get(name);
           if (pair !== undefined) {
-            // We found a new appearing view transition with the same name as this deletion.
-            viewTransitionHostInstanceIdx = 0;
-            const inViewport = applyViewTransitionToHostInstances(
-              child.child,
-              name,
+            const className: ?string = getViewTransitionClassName(
               props.className,
-              null,
-              false,
+              props.share,
             );
-            if (!inViewport) {
-              // This boundary is not in the viewport so we won't treat it as a matched pair.
-              // Revert the transition names. This avoids it flying onto the screen which can
-              // be disruptive and doesn't really preserve any continuity anyway.
-              restoreViewTransitionOnHostInstances(child.child, false);
-            } else {
-              // We'll transition between them.
-              const oldinstance: ViewTransitionState = child.stateNode;
-              const newInstance: ViewTransitionState = pair;
-              newInstance.paired = oldinstance;
-              // Note: If the other side ends up outside the viewport, we'll still run this.
-              // Therefore it's possible for onShare to be called with only an old snapshot.
-              scheduleViewTransitionEvent(child, props.onShare);
+            if (className !== 'none') {
+              // We found a new appearing view transition with the same name as this deletion.
+              viewTransitionHostInstanceIdx = 0;
+              const inViewport = applyViewTransitionToHostInstances(
+                child.child,
+                name,
+                className,
+                null,
+                false,
+              );
+              if (!inViewport) {
+                // This boundary is not in the viewport so we won't treat it as a matched pair.
+                // Revert the transition names. This avoids it flying onto the screen which can
+                // be disruptive and doesn't really preserve any continuity anyway.
+                restoreViewTransitionOnHostInstances(child.child, false);
+              } else {
+                // We'll transition between them.
+                const oldinstance: ViewTransitionState = child.stateNode;
+                const newInstance: ViewTransitionState = pair;
+                newInstance.paired = oldinstance;
+                // Note: If the other side ends up outside the viewport, we'll still run this.
+                // Therefore it's possible for onShare to be called with only an old snapshot.
+                scheduleViewTransitionEvent(child, props.onShare);
+              }
             }
             // Delete the entry so that we know when we've found all of them
             // and can stop searching (size reaches zero).
@@ -797,22 +821,29 @@ function commitExitViewTransitions(
   if (deletion.tag === ViewTransitionComponent) {
     const props: ViewTransitionProps = deletion.memoizedProps;
     const name = getViewTransitionName(props, deletion.stateNode);
-    viewTransitionHostInstanceIdx = 0;
-    const inViewport = applyViewTransitionToHostInstances(
-      deletion.child,
-      name,
+    const pair =
+      appearingViewTransitions !== null
+        ? appearingViewTransitions.get(name)
+        : undefined;
+    const className: ?string = getViewTransitionClassName(
       props.className,
-      null,
-      false,
+      pair !== undefined ? props.share : props.exit,
     );
-    if (!inViewport) {
-      // Revert the transition names. This boundary is not in the viewport
-      // so we won't bother animating it.
-      restoreViewTransitionOnHostInstances(deletion.child, false);
-      // TODO: Should we still visit the children in case a named one was in the viewport?
-    } else if (appearingViewTransitions !== null) {
-      const pair = appearingViewTransitions.get(name);
-      if (pair !== undefined) {
+    if (className !== 'none') {
+      viewTransitionHostInstanceIdx = 0;
+      const inViewport = applyViewTransitionToHostInstances(
+        deletion.child,
+        name,
+        className,
+        null,
+        false,
+      );
+      if (!inViewport) {
+        // Revert the transition names. This boundary is not in the viewport
+        // so we won't bother animating it.
+        restoreViewTransitionOnHostInstances(deletion.child, false);
+        // TODO: Should we still visit the children in case a named one was in the viewport?
+      } else if (pair !== undefined) {
         // We found a new appearing view transition with the same name as this deletion.
         // We'll transition between them instead of running the normal exit.
         const oldinstance: ViewTransitionState = deletion.stateNode;
@@ -820,6 +851,7 @@ function commitExitViewTransitions(
         newInstance.paired = oldinstance;
         // Delete the entry so that we know when we've found all of them
         // and can stop searching (size reaches zero).
+        // $FlowFixMe[incompatible-use]: Refined by the pair.
         appearingViewTransitions.delete(name);
         // Note: If the other side ends up outside the viewport, we'll still run this.
         // Therefore it's possible for onShare to be called with only an old snapshot.
@@ -827,10 +859,10 @@ function commitExitViewTransitions(
       } else {
         scheduleViewTransitionEvent(deletion, props.onExit);
       }
+    }
+    if (appearingViewTransitions !== null) {
       // Look for more pairs deeper in the tree.
       commitDeletedPairViewTransitions(deletion, appearingViewTransitions);
-    } else {
-      scheduleViewTransitionEvent(deletion, props.onExit);
     }
   } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
     let child = deletion.child;
@@ -845,7 +877,10 @@ function commitExitViewTransitions(
   }
 }
 
-function commitBeforeUpdateViewTransition(current: Fiber): void {
+function commitBeforeUpdateViewTransition(
+  current: Fiber,
+  finishedWork: Fiber,
+): void {
   // The way we deal with multiple HostInstances as children of a View Transition in an
   // update can get tricky. The important bit is that if you swap out n HostInstances
   // from n HostInstances then they match up in order. Similarly, if you don't swap
@@ -862,13 +897,32 @@ function commitBeforeUpdateViewTransition(current: Fiber): void {
   // be unexpected but it is in line with the semantics that the ViewTransition is its
   // own layer that cross-fades its content when it updates. If you want to reorder then
   // each child needs its own ViewTransition.
-  const props: ViewTransitionProps = current.memoizedProps;
-  const name = getViewTransitionName(props, current.stateNode);
+  const oldProps: ViewTransitionProps = current.memoizedProps;
+  const oldName = getViewTransitionName(oldProps, current.stateNode);
+  const newProps: ViewTransitionProps = finishedWork.memoizedProps;
+  // This className applies only if there are fewer child DOM nodes than
+  // before or if this update should've been cancelled but we ended up with
+  // a parent animating so we need to animate the child too.
+  // For example, if update="foo" layout="none" and it turns out this was
+  // a layout only change, then the "foo" class will be applied even though
+  // it was not actually an update. Which is a bug.
+  let className: ?string = getViewTransitionClassName(
+    newProps.className,
+    newProps.update,
+  );
+  if (className === 'none') {
+    className = getViewTransitionClassName(newProps.className, newProps.layout);
+    if (className === 'none') {
+      // If both update and layout are both "none" then we don't have to
+      // apply a name. Since we won't animate this boundary.
+      return;
+    }
+  }
   viewTransitionHostInstanceIdx = 0;
   applyViewTransitionToHostInstances(
     current.child,
-    name,
-    props.className,
+    oldName,
+    className,
     (current.memoizedState = []),
     true,
   );
@@ -882,14 +936,20 @@ function commitNestedViewTransitions(changedParent: Fiber): void {
       // was an update through this component then the inner one wins.
       const props: ViewTransitionProps = child.memoizedProps;
       const name = getViewTransitionName(props, child.stateNode);
-      viewTransitionHostInstanceIdx = 0;
-      applyViewTransitionToHostInstances(
-        child.child,
-        name,
+      const className: ?string = getViewTransitionClassName(
         props.className,
-        (child.memoizedState = []),
-        false,
+        props.layout,
       );
+      if (className !== 'none') {
+        viewTransitionHostInstanceIdx = 0;
+        applyViewTransitionToHostInstances(
+          child.child,
+          name,
+          className,
+          (child.memoizedState = []),
+          false,
+        );
+      }
     } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
       commitNestedViewTransitions(child);
     }
@@ -979,10 +1039,58 @@ function restoreNestedViewTransitions(changedParent: Fiber): void {
   }
 }
 
+function cancelViewTransitionHostInstances(
+  currentViewTransition: Fiber,
+  child: null | Fiber,
+  stopAtNestedViewTransitions: boolean,
+): void {
+  if (!supportsMutation) {
+    return;
+  }
+  while (child !== null) {
+    if (child.tag === HostComponent) {
+      const instance: Instance = child.stateNode;
+      const oldName = getViewTransitionName(
+        currentViewTransition.memoizedProps,
+        currentViewTransition.stateNode,
+      );
+      if (viewTransitionCancelableChildren === null) {
+        viewTransitionCancelableChildren = [];
+      }
+      viewTransitionCancelableChildren.push(
+        instance,
+        oldName,
+        child.memoizedProps,
+      );
+      viewTransitionHostInstanceIdx++;
+    } else if (
+      child.tag === OffscreenComponent &&
+      child.memoizedState !== null
+    ) {
+      // Skip any hidden subtrees. They were or are effectively not there.
+    } else if (
+      child.tag === ViewTransitionComponent &&
+      stopAtNestedViewTransitions
+    ) {
+      // Skip any nested view transitions for updates since in that case the
+      // inner most one is the one that handles the update.
+    } else {
+      cancelViewTransitionHostInstances(
+        currentViewTransition,
+        child.child,
+        stopAtNestedViewTransitions,
+      );
+    }
+    child = child.sibling;
+  }
+}
+
 function measureViewTransitionHostInstances(
   currentViewTransition: Fiber,
   parentViewTransition: Fiber,
   child: null | Fiber,
+  name: string,
+  className: ?string,
   previousMeasurements: null | Array,
   stopAtNestedViewTransitions: boolean,
 ): boolean {
@@ -1028,20 +1136,15 @@ function measureViewTransitionHostInstances(
         parentViewTransition.flags |= AffectedParentLayout;
       }
       if ((parentViewTransition.flags & Update) !== NoFlags) {
-        const props: ViewTransitionProps = parentViewTransition.memoizedProps;
         // We might update this node so we need to apply its new name for the new state.
-        const newName = getViewTransitionName(
-          props,
-          parentViewTransition.stateNode,
-        );
         applyViewTransitionName(
           instance,
           viewTransitionHostInstanceIdx === 0
-            ? newName
+            ? name
             : // If we have multiple Host Instances below, we add a suffix to the name to give
               // each one a unique name.
-              newName + '_' + viewTransitionHostInstanceIdx,
-          props.className,
+              name + '_' + viewTransitionHostInstanceIdx,
+          className,
         );
       }
       if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {
@@ -1083,6 +1186,8 @@ function measureViewTransitionHostInstances(
           currentViewTransition,
           parentViewTransition,
           child.child,
+          name,
+          className,
           previousMeasurements,
           stopAtNestedViewTransitions,
         )
@@ -1099,6 +1204,42 @@ function measureUpdateViewTransition(
   current: Fiber,
   finishedWork: Fiber,
 ): boolean {
+  const props: ViewTransitionProps = finishedWork.memoizedProps;
+  const updateClassName: ?string = getViewTransitionClassName(
+    props.className,
+    props.update,
+  );
+  const layoutClassName: ?string = getViewTransitionClassName(
+    props.className,
+    props.update,
+  );
+  let className: ?string;
+  if (updateClassName === 'none') {
+    if (layoutClassName === 'none') {
+      // If both update and layout class name were none, then we didn't apply any
+      // names in the before update phase so we shouldn't now neither.
+      return false;
+    }
+    // We don't care if this is mutated or children layout changed, but we still
+    // measure each instance to see if it moved and therefore should apply layout.
+    finishedWork.flags &= ~Update;
+    className = layoutClassName;
+  } else if ((finishedWork.flags & Update) !== NoFlags) {
+    // It was updated and we have an appropriate class name to apply.
+    className = updateClassName;
+  } else {
+    if (layoutClassName === 'none') {
+      // If we did not update, then all changes are considered a layout. We'll
+      // attempt to cancel.
+      viewTransitionHostInstanceIdx = 0;
+      cancelViewTransitionHostInstances(current, finishedWork.child, true);
+      return false;
+    }
+    // We didn't update but we might still apply layout so we measure each
+    // instance to see if it moved or resized.
+    className = layoutClassName;
+  }
+  const name = getViewTransitionName(props, finishedWork.stateNode);
   // If nothing changed due to a mutation, or children changing size
   // and the measurements end up unchanged, we should restore it to not animate.
   viewTransitionHostInstanceIdx = 0;
@@ -1107,6 +1248,8 @@ function measureUpdateViewTransition(
     current,
     finishedWork,
     finishedWork.child,
+    name,
+    className,
     previousMeasurements,
     true,
   );
@@ -1127,16 +1270,27 @@ function measureNestedViewTransitions(changedParent: Fiber): void {
     if (child.tag === ViewTransitionComponent) {
       const current = child.alternate;
       if (current !== null) {
+        const props: ViewTransitionProps = child.memoizedProps;
+        const name = getViewTransitionName(props, child.stateNode);
+        const className: ?string = getViewTransitionClassName(
+          props.className,
+          props.layout,
+        );
         viewTransitionHostInstanceIdx = 0;
-        measureViewTransitionHostInstances(
+        const inViewport = measureViewTransitionHostInstances(
           current,
           child,
           child.child,
+          name,
+          className,
           child.memoizedState,
           false,
         );
-        const props: ViewTransitionProps = child.memoizedProps;
-        scheduleViewTransitionEvent(child, props.onLayout);
+        if ((child.flags & Update) === NoFlags || !inViewport) {
+          // Nothing changed.
+        } else {
+          scheduleViewTransitionEvent(child, props.onLayout);
+        }
       }
     } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
       measureNestedViewTransitions(child);
@@ -3010,11 +3164,6 @@ function recursivelyTraverseAfterMutationEffects(
     // its size and position. We need to measure this and if not, restore it to
     // not animate.
     measureNestedViewTransitions(parentFiber);
-    if ((parentFiber.flags & AffectedParentLayout) !== NoFlags) {
-      // This boundary changed size in a way that may have caused its parent to
-      // relayout. We need to bubble this information up to the parent.
-      viewTransitionContextChanged = true;
-    }
   }
 }
 
diff --git a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
index bfbc1b0b2e337..65e3ffc459148 100644
--- a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
+++ b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
@@ -19,8 +19,13 @@ import {getTreeId} from './ReactFiberTreeContext';
 
 export type ViewTransitionProps = {
   name?: string,
-  className?: string,
   children?: ReactNodeList,
+  className?: 'none' | string,
+  enter?: 'none' | string,
+  exit?: 'none' | string,
+  layout?: 'none' | string,
+  share?: 'none' | string,
+  update?: 'none' | string,
   onEnter?: (instance: ViewTransitionInstance) => void,
   onExit?: (instance: ViewTransitionInstance) => void,
   onLayout?: (instance: ViewTransitionInstance) => void,
@@ -76,3 +81,19 @@ export function getViewTransitionName(
   // We should have assigned a name by now.
   return (instance.autoName: any);
 }
+
+export function getViewTransitionClassName(
+  className: ?string,
+  eventClassName: ?string,
+): ?string {
+  if (eventClassName == null) {
+    return className;
+  }
+  if (eventClassName === 'none') {
+    return eventClassName;
+  }
+  if (className != null) {
+    return className + ' ' + eventClassName;
+  }
+  return eventClassName;
+}