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

Support posing regular react components #276

Merged
merged 6 commits into from
Apr 12, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 32 additions & 5 deletions packages/popmotion-pose/docs/api/react/posed.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ description: Create a posed component
category: react
---

**Note:** React Pose **requires** React 16.3.0.

# `posed`

`posed` is used to create animated and interactive components that you can reuse throughout your React site.
Expand All @@ -26,6 +24,12 @@ import posed from 'react-pose'

### Create a posed component

`posed` can be used to create posed components in two ways:
1) **Recommended:** Create HTML & SVG elements (eg `posed.div()`)
2) **Advanced:** Convert existing components (eg `posed(Component)()`)

#### HTML & SVG elements

`pose` isn't called directly, instead we pass [posed props](/pose/api/props) to `posed.div`, `posed.button` etc. Every HTML and SVG element is supported:

```javascript
Expand All @@ -37,6 +41,32 @@ const DraggableCircle = posed.circle({
export default ({ radius }) => <DraggableCircle r={radius} />
```

#### Existing components

Existing components can be converted to posed components by calling `posed` directly:

```javascript
const PosedComponent = posed(MyComponent)(poseProps)
```

For performance and layout calculations, React Pose requires a reference to the underlying DOM element. So, the component to be posed **must pass hostRef to the host element's ref prop**.

```javascript
const MyComponent = ({ hostRef }) => <div ref={hostRef} />

const PosedComponent = posed(MyComponent)({
draggable: true
})

export default () => <PosedComponent pose={isOpen ? 'open' : 'closed'} />
```

For FLIP support in a `PoseGroup`, it **optionally** needs to pass on the `style` prop:

```javascript
const MyComponent = ({ hostRef, style }) => <div ref={hostRef} style={style} />
```

### Set a pose

Poses can be set via the `pose` property. This can either be a string, or an array of strings to reference multiple poses:
Expand Down Expand Up @@ -99,9 +129,6 @@ const sidebarProps = {
}

const Sidebar = styled(posed.nav(sidebarProps))`
width: 300px;
background: red;
`;
```

#### `className`
Expand Down
8 changes: 4 additions & 4 deletions packages/react-pose/dist/react-pose.js

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions packages/react-pose/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/react-pose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"webpack": "^3.11.0"
},
"dependencies": {
"popmotion-pose": "^1.3.0"
"popmotion-pose": "^1.3.0",
"react-lifecycles-compat": "^1.1.0"
},
"unpkg": "./dist/react-pose.js",
"prettier": {
Expand Down
86 changes: 81 additions & 5 deletions packages/react-pose/src/components/PoseElement.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { createContext, createElement } from 'react';
import reactLifecyclesCompat = require('react-lifecycles-compat');
import poseFactory, { Poser, PoserProps } from 'popmotion-pose';
import {
ChildRegistration,
Expand All @@ -11,6 +12,30 @@ import {

export const PoseParentContext = createContext({});

// Future enhancement: Memoize these functions for speed
function isReactComponent(component) {
let proto;

try {
// Must use getPrototypeOf to capture the case where Proxy components are
// used (they don't have a `.prototype`)
proto = component.prototype || Object.getPrototypeOf(component);
} catch {
// getPrototypeOf will throw in ES5, so we return early here
return false;
}

return 'isReactComponent' in proto;
}

function isStatelessFunctionalComponent(component) {
return typeof component === 'function' && !isReactComponent(component);
}

function isDOMPrimitiveComponent(component) {
return typeof component === 'string';
}

const calcPopFromFlowStyle = (el: HTMLElement): PopStyle => {
const { offsetTop, offsetLeft, offsetWidth, offsetHeight } = el;

Expand All @@ -32,10 +57,11 @@ const objectToMap = (obj: { [key: string]: any }): Map<string, any> =>
return map;
}, new Map());

export class PoseElement extends React.PureComponent<PoseElementProps> {
class PoseElement extends React.PureComponent<PoseElementProps> {
props: PoseElementProps;
poser: Poser;
ref: Element;
styleProps: { [key: string]: any };
children: Set<ChildRegistration> = new Set();
popStyle?: PopStyle;

Expand Down Expand Up @@ -109,7 +135,7 @@ export class PoseElement extends React.PureComponent<PoseElementProps> {
if (!this.popStyle) {
props.style = {
...props.style,
...calcPopFromFlowStyle(this.ref)
...calcPopFromFlowStyle(this.ref as HTMLElement)
};
this.popStyle = props.style;
} else {
Expand All @@ -128,8 +154,30 @@ export class PoseElement extends React.PureComponent<PoseElementProps> {

setRef = (ref: Element) => {
const { innerRef } = this.props;
if (innerRef) innerRef(ref);
this.ref = ref;

if (innerRef) {
// The parent component which set `innerRef` is interested in the
// immediately wrapped component, not necessarily the forwarded refs from
// children.
if (isDOMPrimitiveComponent(this.props.elementType)) {
// If the posed element is a DOM primitive, then we always pass the ref
// back
innerRef(ref);
} else if (!ref || isReactComponent(ref)) {
// Otherwise, we only pass the ref back if it's _not_ the forwarded
// ref (which would point to a DOM element)
innerRef(ref);
}
}

// In the `render()` function, we are setting both `ref` and `innerRef`.
// We're only interested in the thing that results in a DOM node here, so we
// only set the ref when we know what we got back was a DOM node.
// NOTE: We don't check for a functional component here since it's not
// possible to set a `ref` on one
if (!ref || (!isReactComponent(ref) && !isStatelessFunctionalComponent(ref))) {
this.ref = ref;
}
};

componentDidMount() {
Expand Down Expand Up @@ -181,6 +229,7 @@ export class PoseElement extends React.PureComponent<PoseElementProps> {
const { onUnmount } = this.props;
if (onUnmount) onUnmount(this.poser);
this.poser.destroy();
this.ref = undefined;
}

initPoser(poser: Poser) {
Expand Down Expand Up @@ -212,17 +261,44 @@ export class PoseElement extends React.PureComponent<PoseElementProps> {
const { elementType, children } = this.props;
const props = this.getSetProps();

const elementProps : {
ref?: (ref: Element) => void;
innerRef?: (ref: Element) => void;
hostRef?: (ref: Element) => void;
} = {
// Functional components throw a warning when passed a `ref` prop
ref: isStatelessFunctionalComponent(elementType) ? undefined : this.setRef,
};

if (!isDOMPrimitiveComponent(elementType)) {
// We need to get a ref to an underlying DOM element. The standard is to
// accept an `innerRef` prop (ala: Styled Components), so we pass that
// function along.
// Unfortunately when using Styled Components, it will intercept the
// `innerRef` prop, and not pass it down to a wrapped component (when
// doing styled(Component)``, hence we pass in another standard ref
// getting function `hostRef`, which the consuming code can opt into if
// they want.
// ( see https://github.com/facebook/react/issues/11401 )
elementProps.innerRef = this.setRef;
elementProps.hostRef = this.setRef;
}

return (
<PoseParentContext.Provider value={this.childrenHandlers}>
{createElement(
elementType,
{
...props,
ref: this.setRef
...elementProps,
},
children
)}
</PoseParentContext.Provider>
);
}
}

reactLifecyclesCompat(PoseElement);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Give backwards compatibility with existing react code bases.


export { PoseElement };
2 changes: 1 addition & 1 deletion packages/react-pose/src/components/PoseElement.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type PoseContextProps = {

export type PoseElementProps = {
children?: any;
elementType: string;
elementType: any;
poseProps: PoserProps;
pose?: CurrentPose;
initialPose?: CurrentPose;
Expand Down
7 changes: 6 additions & 1 deletion packages/react-pose/src/components/PoseGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { ReactElement } from 'react';
import reactLifecyclesCompat = require('react-lifecycles-compat');
import {
handleIncomingChildren,
makeChildList,
Expand All @@ -24,7 +25,7 @@ export type State = {
removeFromTree: (key: string) => void;
};

export class PoseGroup extends React.Component<Props, State> {
class PoseGroup extends React.Component<Props, State> {
static defaultProps = {
flipMove: true,
preEnterPose: 'exit',
Expand Down Expand Up @@ -80,3 +81,7 @@ export class PoseGroup extends React.Component<Props, State> {
return <Fragment>{children}</Fragment>;
}
}

reactLifecyclesCompat(PoseGroup);

export { PoseGroup };
6 changes: 4 additions & 2 deletions packages/react-pose/src/posed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type ComponentFactory = (
) => (props: PoseElementProps) => ReactElement<any>;

export type Posed = {
(): ComponentFactory;
[key: string]: ComponentFactory;
};

Expand Down Expand Up @@ -56,9 +57,10 @@ const getComponentFactory = (key: string) =>
: createComponentFactory(key);

const posed: Posed = new Proxy(
{},
function() {} as Posed,
{
get: (target, key: string) => getComponentFactory(key)
get: (target, key: string) => getComponentFactory(key),
apply: (target, thisArg, argumentsList) => getComponentFactory(argumentsList[0]),
}
);

Expand Down