Skip to content

Fix passing ref to functional components (#153) #156

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

Closed
wants to merge 2 commits into from
Closed
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
22 changes: 14 additions & 8 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { HTMLAttributes } from 'react';
import React, { HTMLAttributes, RefAttributes, ReactInstance } from 'react';
import ReactDOM from 'react-dom';
import contains from 'rc-util/lib/Dom/contains';
import findDOMNode from 'rc-util/lib/Dom/findDOMNode';
Expand Down Expand Up @@ -732,9 +732,8 @@ export function generateTrigger(
const { popupVisible } = this.state;
const { children, forceRender, alignPoint, className } = this.props;
const child = React.Children.only(children) as React.ReactElement;
const newChildProps: HTMLAttributes<HTMLElement> & { key: string } = {
key: 'trigger',
};
const newChildProps: HTMLAttributes<HTMLElement> &
RefAttributes<ReactInstance> & { key: string } = { key: 'trigger' };

if (this.isContextMenuToShow()) {
newChildProps.onContextMenu = this.onContextMenu;
Expand Down Expand Up @@ -779,10 +778,17 @@ export function generateTrigger(
if (childrenClassName) {
newChildProps.className = childrenClassName;
}
const trigger = React.cloneElement(child, {
...newChildProps,
ref: composeRef(this.triggerRef, (child as any).ref),
});

// Prevent adding ref to Functional child components
if (
!child.type ||
typeof child.type !== 'function' ||
(child.type.prototype && child.type.prototype.isReactComponent)
) {
newChildProps.ref = composeRef(this.triggerRef, (child as any).ref);
}

const trigger = React.cloneElement(child, newChildProps);

let portal: React.ReactElement;
// prevent unmounting after it's rendered
Expand Down
75 changes: 66 additions & 9 deletions tests/basic.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -538,22 +538,19 @@ describe('Trigger.Basic', () => {
});

it('support function component', () => {
const NoRef = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => null);
return (
<div className="target" {...props}>
click
</div>
);
});
const FuncComp = props => (
<div className="target" {...props}>
click
</div>
);

const wrapper = mount(
<Trigger
action={['click']}
popupAlign={placementAlignMap.left}
popup={<strong className="x-content">tooltip2</strong>}
>
<NoRef />
<FuncComp />
</Trigger>,
);

Expand All @@ -564,6 +561,66 @@ describe('Trigger.Basic', () => {
expect(wrapper.isHidden()).toBeTruthy();
});

describe('passes ref to children where applicable', () => {
function getRefResult(component) {
const triggerMock = jest.fn();
const wrapper = mount(
<Trigger
action={['click']}
popupAlign={placementAlignMap.left}
popup={<strong className="x-content">tooltip2</strong>}
ref={triggerMock}
>
{component}
</Trigger>,
);

wrapper.trigger();

return triggerMock.mock.calls[0][0].triggerRef.current;
}

it('does not pass ref to function component', () => {
const NoRef = props => (
<div id="target" {...props}>
click
</div>
);

const refVal = getRefResult(<NoRef />);
expect(refVal).toBeNull();
});

it('does pass ref to forwardRef function component', () => {
const WithRef = React.forwardRef((props, ref) => (
<div id="target" {...props} ref={ref}>
click
</div>
));

const refVal = getRefResult(<WithRef />);
expect(refVal.id).toBe('target');
});

it('does pass ref to class component', () => {
class ClassComp extends React.Component {
id = 'target';

render() {
return <div>click</div>;
}
}

const refVal = getRefResult(<ClassComp />);
expect(refVal.id).toBe('target');
});

it('does pass ref to element', () => {
const refVal = getRefResult(<div id="target">click</div>);
expect(refVal.id).toBe('target');
});
});

it('Popup with mouseDown prevent', () => {
const wrapper = mount(
<Trigger
Expand Down