Skip to content

Commit

Permalink
MM-21880 Forward refs using OverlayTrigger wrapper (mattermost#4701)
Browse files Browse the repository at this point in the history
* MM-21880 Forward refs using OverlayTrigger wrapper

* Switch to React.createRef to switch typing issue

* Actually fix incorrect ref typing

* Fix unit tests
  • Loading branch information
hmhealey committed Jan 27, 2020
1 parent 84899bf commit 29fd74a
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
// See LICENSE.txt for license information.

import React from 'react';

import OverlayTrigger from 'components/overlay_trigger';
import {OverlayTrigger as BaseOverlayTrigger} from 'react-bootstrap';

import {mountWithIntl} from 'tests/helpers/intl-test-helper';

Expand All @@ -30,7 +29,7 @@ describe('components/ChannelHeaderMobile/ChannelInfoButton', () => {

const hide = jest.fn();

const ref = wrapper.find(OverlayTrigger);
const ref = wrapper.find(BaseOverlayTrigger);
ref.instance().hide = hide;
wrapper.instance().hide();

Expand All @@ -47,7 +46,7 @@ describe('components/ChannelHeaderMobile/ChannelInfoButton', () => {

const hide = jest.fn();

const ref = wrapper.find(OverlayTrigger);
const ref = wrapper.find(BaseOverlayTrigger);
ref.instance().hide = hide;
wrapper.instance().showEditChannelHeaderModal();

Expand Down
38 changes: 30 additions & 8 deletions components/overlay_trigger.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import React from 'react';
import {OverlayTrigger as BaseOverlayTrigger} from 'react-bootstrap';
import {FormattedMessage, IntlProvider} from 'react-intl';

import {mountWithIntl} from 'tests/helpers/intl-test-helper';

import OverlayTrigger from './overlay_trigger';

describe('OverlayTrigger', () => {
Expand All @@ -18,12 +20,14 @@ describe('OverlayTrigger', () => {
[testId]: 'Actual value',
},
};
const testOverlay = (
<FormattedMessage
id={testId}
defaultMessage='Default value'
/>
);
const baseProps = {
overlay: (
<FormattedMessage
id={testId}
defaultMessage='Default value'
/>
),
};

// Intercept console error messages since we intentionally cause some as part of these tests
let originalConsoleError: () => void;
Expand All @@ -40,7 +44,7 @@ describe('OverlayTrigger', () => {
test('base OverlayTrigger should fail to pass intl to overlay', () => {
const wrapper = mount(
<IntlProvider {...intlProviderProps}>
<BaseOverlayTrigger overlay={testOverlay}>
<BaseOverlayTrigger {...baseProps}>
<span/>
</BaseOverlayTrigger>
</IntlProvider>
Expand All @@ -57,7 +61,7 @@ describe('OverlayTrigger', () => {
test('custom OverlayTrigger should fail to pass intl to overlay', () => {
const wrapper = mount(
<IntlProvider {...intlProviderProps}>
<OverlayTrigger overlay={testOverlay}>
<OverlayTrigger {...baseProps}>
<span/>
</OverlayTrigger>
</IntlProvider>
Expand All @@ -68,4 +72,22 @@ describe('OverlayTrigger', () => {
expect(overlay.text()).toBe('Actual value');
expect(console.error).not.toHaveBeenCalled();
});

test('ref should properly be forwarded', () => {
const ref = React.createRef<BaseOverlayTrigger>();
const props = {
...baseProps,
ref,
};

const wrapper = mountWithIntl(
<IntlProvider {...intlProviderProps}>
<OverlayTrigger {...props}>
<span/>
</OverlayTrigger>
</IntlProvider>
);

expect(ref.current).toBe(wrapper.find(BaseOverlayTrigger).instance());
});
});
62 changes: 33 additions & 29 deletions components/overlay_trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,38 @@ import React from 'react';
import {OverlayTrigger as BaseOverlayTrigger, OverlayTriggerProps} from 'react-bootstrap';
import {IntlContext, IntlShape} from 'react-intl';

export {BaseOverlayTrigger};

type Props = OverlayTriggerProps;

export default class OverlayTrigger extends React.PureComponent<Props> {
static defaultProps = {
defaultOverlayShown: false,
trigger: ['hover', 'focus']
}

render() {
const {overlay, ...props} = this.props;

// The overlay is rendered outside of the regular React context, and our version react-bootstrap can't forward
// that context itself, so we have to manually forward the react-intl context to this component's child.
const OverlayWrapper = ({intl, ...otherProps}: {intl: IntlShape}) => (
<IntlContext.Provider value={intl}>
{React.cloneElement(overlay, otherProps)}
</IntlContext.Provider>
);

return (
<IntlContext.Consumer>
{(intl): React.ReactNode => (
<BaseOverlayTrigger
{...props}
overlay={<OverlayWrapper intl={intl}/>}
/>
)}
</IntlContext.Consumer>
);
}
}
const OverlayTrigger = React.forwardRef((props: Props, ref?: React.Ref<BaseOverlayTrigger>) => {
const {overlay, ...otherProps} = props;

// The overlay is rendered outside of the regular React context, and our version react-bootstrap can't forward
// that context itself, so we have to manually forward the react-intl context to this component's child.
const OverlayWrapper = ({intl, ...overlayProps}: {intl: IntlShape}) => (
<IntlContext.Provider value={intl}>
{React.cloneElement(overlay, overlayProps)}
</IntlContext.Provider>
);

return (
<IntlContext.Consumer>
{(intl): React.ReactNode => (
<BaseOverlayTrigger
{...otherProps}
ref={ref}
overlay={<OverlayWrapper intl={intl}/>}
/>
)}
</IntlContext.Consumer>
);
});

OverlayTrigger.defaultProps = {
defaultOverlayShown: false,
trigger: ['hover', 'focus']
};
OverlayTrigger.displayName = 'OverlayTrigger';

export default OverlayTrigger;
20 changes: 8 additions & 12 deletions components/profile_picture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

import React from 'react';

import OverlayTrigger from 'components/overlay_trigger';
import OverlayTrigger, {BaseOverlayTrigger} from 'components/overlay_trigger';
import ProfilePopover from 'components/profile_popover';
import StatusIcon from 'components/status_icon';
import Avatar from 'components/widgets/users/avatar';

interface MMOverlayTrigger extends OverlayTrigger {
hide?: () => void;
interface MMOverlayTrigger extends BaseOverlayTrigger {
hide: () => void;
}

type Props = {
Expand All @@ -27,8 +27,6 @@ type Props = {
}

export default class ProfilePicture extends React.PureComponent<Props> {
public overlay!: MMOverlayTrigger;

public static defaultProps = {
size: 'md',
isRHS: false,
Expand All @@ -37,16 +35,14 @@ export default class ProfilePicture extends React.PureComponent<Props> {
wrapperClass: '',
};

overlay = React.createRef<MMOverlayTrigger>();

public hideProfilePopover = () => {
if (this.overlay) {
this.overlay.hide!();
if (this.overlay.current) {
this.overlay.current.hide();
}
}

public setOverlayRef = (ref: OverlayTrigger) => {
this.overlay = ref;
}

public render() {
// profileSrc will, if possible, be the original user profile picture even if the icon
// for the post is overriden, so that the popup shows the user identity
Expand All @@ -59,7 +55,7 @@ export default class ProfilePicture extends React.PureComponent<Props> {
if (this.props.userId) {
return (
<OverlayTrigger
ref={this.setOverlayRef}
ref={this.overlay}
trigger='click'
placement='right'
rootClose={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ exports[`components/select_team/components/SelectTeamItem should match snapshot,
"_rendered": <div
className="signup-team-dir"
>
<OverlayTrigger
<ForwardRef
container={[Circular]}
defaultOverlayShown={false}
delayShow={1000}
Expand All @@ -238,7 +238,7 @@ exports[`components/select_team/components/SelectTeamItem should match snapshot,
<TeamInfoIcon
className="icon icon--info"
/>
</OverlayTrigger>
</ForwardRef>
<a
className=""
href="#"
Expand Down

0 comments on commit 29fd74a

Please sign in to comment.