Skip to content

Commit

Permalink
chore(react-motions): improve error reporting on invalid elements (#2…
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter authored Nov 16, 2023
1 parent 0f56cc9 commit 3a5bf3f
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 3 deletions.
3 changes: 2 additions & 1 deletion packages/react-components/react-motions-preview/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"dependencies": {
"@fluentui/react-shared-contexts": "^9.12.0",
"@fluentui/react-utilities": "^9.15.2",
"@swc/helpers": "^0.5.1"
"@swc/helpers": "^0.5.1",
"react-is": "^17.0.2"
},
"peerDependencies": {
"@types/react": ">=16.8.0 <19.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useIsomorphicLayoutEffect, useMergedRefs } from '@fluentui/react-utilit
import * as React from 'react';

import { useIsReducedMotion } from '../hooks/useIsReducedMotion';
import { getChildElement } from '../utils/getChildElement';
import type { MotionAtom } from '../types';

export type AtomProps = {
Expand All @@ -20,7 +21,7 @@ export function createAtom(motion: MotionAtom) {
const Atom: React.FC<AtomProps> = props => {
const { children, iterations = 1, playState = 'running' } = props;

const child = React.Children.only(children) as React.ReactElement & { ref: React.Ref<HTMLElement> };
const child = getChildElement(children);

const animationRef = React.useRef<Animation | undefined>();
const elementRef = React.useRef<HTMLElement>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEventCallback, useIsomorphicLayoutEffect, useMergedRefs } from '@flu
import * as React from 'react';

import { useIsReducedMotion } from '../hooks/useIsReducedMotion';
import { getChildElement } from '../utils/getChildElement';
import type { MotionTransition } from '../types';

type TransitionProps = {
Expand All @@ -17,7 +18,7 @@ export function createTransition(transition: MotionTransition) {
const Transition: React.FC<TransitionProps> = props => {
const { appear, children, visible, unmountOnExit } = props;

const child = React.Children.only(children) as React.ReactElement & { ref: React.Ref<HTMLElement> };
const child = getChildElement(children);

const elementRef = React.useRef<HTMLElement>();
const ref = useMergedRefs(elementRef, child.ref);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react';
import { getChildElement } from './getChildElement';

describe('getChildElement', () => {
it('throws if multiple elements are passed', () => {
expect(() => {
getChildElement([<div key="1" />, <div key="2" />] as unknown as React.ReactElement);
}).toThrow('@fluentui/react-motions: Invalid child element');
});

it('throws if passed element does not support ref forwarding', () => {
const TestA = () => <div />;

// eslint-disable-next-line @typescript-eslint/naming-convention
function TestB() {
return <div />;
}

expect(() => {
getChildElement(<TestA />);
}).toThrow('@fluentui/react-motions: Invalid child element');
expect(() => {
getChildElement(<TestB />);
}).toThrow('@fluentui/react-motions: Invalid child element');
});

it('does not throw if passed element does supports ref forwarding', () => {
const Test = React.forwardRef(() => <div />);

expect(() => {
getChildElement(<Test />);
}).not.toThrow();
expect(() => {
getChildElement(<div />);
}).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import * as ReactIs from 'react-is';

export function getChildElement(children: React.ReactElement) {
try {
const child = React.Children.only(children) as React.ReactElement & { ref: React.Ref<HTMLElement> };

if (typeof child.type === 'string' || ReactIs.isForwardRef(child)) {
return child as React.ReactElement & { ref: React.Ref<HTMLElement> };
}

// We don't need to do anything here: we catch the exception from React to throw a more meaningful error
// eslint-disable-next-line no-empty
} catch {}

throw new Error(
[
'@fluentui/react-motions: Invalid child element.',
'\n',
'Motion factories require a single child element to be passed. ',
'That element element should support ref forwarding i.e. it should be either an intrinsic element (e.g. div) or a component that uses React.forwardRef().',
].join(''),
);
}

0 comments on commit 3a5bf3f

Please sign in to comment.