Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
[MM-9622] Allow plugins to create link tooltips (#2386)
Browse files Browse the repository at this point in the history
  • Loading branch information
kosgrz authored and jwilander committed Mar 5, 2019
1 parent f950f70 commit 850b003
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 0 deletions.
27 changes: 27 additions & 0 deletions components/link_tooltip/__snapshots__/link_tooltip.test.jsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`components/link_tooltip/link_tooltip should match snapshot 1`] = `
<Fragment>
<div
style={
Object {
"alignItems": "center",
"display": "flex",
"flexDirection": "column",
"zIndex": "10",
}
}
>
<Connect(Pluggable)
href="www.test.com"
pluggableName="LinkTooltip"
/>
</div>
<span
onMouseLeave={[Function]}
onMouseOver={[Function]}
>
test title
</span>
</Fragment>
`;
79 changes: 79 additions & 0 deletions components/link_tooltip/link_tooltip.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import PropTypes from 'prop-types';
import React from 'react';
import Popper from 'popper.js';
import ReactDOM from 'react-dom';

import {Constants} from 'utils/constants';
import Pluggable from 'plugins/pluggable';

const tooltipContainerStyles = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
zIndex: '10',
};

export default class LinkTooltip extends React.PureComponent {
static propTypes = {
href: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};

constructor(props) {
super(props);

this.tooltipContainerRef = React.createRef();
}

showTooltip = (e) => {
const target = $(e.target);
const tooltipContainer = $(this.tooltipContainerRef.current);

this.timeout = setTimeout(() => {
tooltipContainer.show();

this.popper = new Popper(target, tooltipContainer, {
placement: 'bottom',
modifiers: {
preventOverflow: {enabled: false},
hide: {enabled: false},
},
});
}, Constants.OVERLAY_TIME_DELAY);
};

hideTooltip = () => {
clearTimeout(this.timeout);
$(this.tooltipContainerRef.current).hide();
};

render() {
const {href, title} = this.props;
return (
<React.Fragment>
{ReactDOM.createPortal(
<div
style={tooltipContainerStyles}
ref={this.tooltipContainerRef}
>
<Pluggable
href={href}
pluggableName='LinkTooltip'
/>
</div>,
document.getElementById('root')
)}
<span
onMouseOver={this.showTooltip}
onMouseLeave={this.hideTooltip}
>
{title}
</span>
</React.Fragment>
);
}
}

23 changes: 23 additions & 0 deletions components/link_tooltip/link_tooltip.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';
import ReactDOM from 'react-dom';
import {shallow} from 'enzyme';

import LinkTooltip from 'components/link_tooltip/link_tooltip';

describe('components/link_tooltip/link_tooltip', () => {
test('should match snapshot', () => {
ReactDOM.createPortal = (node) => node;
const wrapper = shallow(
<LinkTooltip
href={'www.test.com'}
title={'test title'}
/>
);

expect(wrapper).toMatchSnapshot();
expect(wrapper.find('span').text()).toBe('test title');
});
});
6 changes: 6 additions & 0 deletions components/markdown/markdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export default class Markdown extends React.PureComponent {
* prop for passed down to image component for dimensions
*/
imagesMetadata: PropTypes.object,

/**
* Whether or not to place the LinkTooltip component inside links
*/
hasPluginTooltips: PropTypes.bool,
};

static defaultProps = {
Expand Down Expand Up @@ -103,6 +108,7 @@ export default class Markdown extends React.PureComponent {
return messageHtmlToComponent(htmlFormattedText, this.props.isRHS, {
imageProps: this.props.imageProps,
imagesMetadata: this.props.imagesMetadata,
hasPluginTooltips: this.props.hasPluginTooltips,
});
}
}
1 change: 1 addition & 0 deletions components/post_markdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function mapStateToProps(state, ownProps) {
return {
channel: getChannel(state, ownProps.channelId),
pluginHooks: state.plugins.components.MessageWillFormat,
hasPluginTooltips: Boolean(state.plugins.components.LinkTooltip),
};
}

Expand Down
6 changes: 6 additions & 0 deletions components/post_markdown/post_markdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export default class PostMarkdown extends React.PureComponent {
options: PropTypes.object,

pluginHooks: PropTypes.arrayOf(PropTypes.object),

/**
* Whether or not to place the LinkTooltip component inside links
*/
hasPluginTooltips: PropTypes.bool,
};

static defaultProps = {
Expand Down Expand Up @@ -71,6 +76,7 @@ export default class PostMarkdown extends React.PureComponent {
proxyImages={proxyImages}
options={this.props.options}
channelNamesMap={channelNamesMap}
hasPluginTooltips={this.props.hasPluginTooltips}
imagesMetadata={this.props.post && this.props.post.metadata && this.props.post.metadata.images}
/>
);
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"pdfjs-dist": "2.0.489",
"perfect-scrollbar": "0.8.1",
"popmotion": "8.6.3",
"popper.js": "1.14.7",
"prop-types": "15.7.2",
"react": "16.8.3",
"react-addons-pure-render-mixin": "15.6.2",
Expand Down
6 changes: 6 additions & 0 deletions plugins/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ export default class PluginRegistry {
return dispatchPluginComponentAction('PostMessageAttachment', this.id, component);
}

// Register a component to show as a tooltip when a user hovers on a link in a post.
// Accepts a React component. Returns a unique identifier.
registerLinkTooltipComponent(component) {
return dispatchPluginComponentAction('LinkTooltip', this.id, component);
}

// Add a button to the channel header. If there are more than one buttons registered by any
// plugin, a dropdown menu is created to contain all the plugin buttons.
// Accepts the following:
Expand Down
41 changes: 41 additions & 0 deletions utils/__snapshots__/message_html_to_component.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,47 @@ Array [
]
`;

exports[`messageHtmlToComponent link with enabled a tooltip plugin 1`] = `
Array [
<p>
lorem ipsum
<a
className="theme markdown__link"
href="http:https://www.dolor.com"
rel="noreferrer"
target="_blank"
>
<LinkTooltip
href="http:https://www.dolor.com"
title="www.dolor.com"
/>
</a>
sit amet
</p>,
"
",
]
`;

exports[`messageHtmlToComponent link without enabled tooltip plugins 1`] = `
Array [
<p>
lorem ipsum
<a
className="theme markdown__link"
href="http:https://www.dolor.com"
rel="noreferrer"
target="_blank"
>
www.dolor.com
</a>
sit amet
</p>,
"
",
]
`;

exports[`messageHtmlToComponent plain text 1`] = `
Array [
<p>
Expand Down
17 changes: 17 additions & 0 deletions utils/message_html_to_component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import AtMention from 'components/at_mention';
import LatexBlock from 'components/latex_block';
import SizeAwareImage from 'components/size_aware_image';
import PostEmoji from 'components/post_emoji';
import LinkTooltip from '../components/link_tooltip/link_tooltip';

/*
* Converts HTML to React components using html-to-react.
Expand All @@ -18,6 +19,7 @@ import PostEmoji from 'components/post_emoji';
* - imageProps - If specified, any extra props that should be passed into the image component.
* - latex - If specified, latex is replaced with the LatexBlock component. Defaults to true.
* - imagesMetadata - the dimensions of the image as retrieved from post.metadata.images.
* - hasPluginTooltips - If specified, the LinkTooltip component is placed inside links. Defaults to false.
*/
export function messageHtmlToComponent(html, isRHS, options = {}) {
if (!html) {
Expand All @@ -32,6 +34,21 @@ export function messageHtmlToComponent(html, isRHS, options = {}) {
}

const processingInstructions = [];
if (options.hasPluginTooltips) {
const hrefAttrib = 'href';
processingInstructions.push({
replaceChildren: true,
shouldProcessNode: (node) => node.type === 'tag' && node.name === 'a' && node.attribs[hrefAttrib],
processNode: (node, children) => {
return (
<LinkTooltip
href={node.attribs[hrefAttrib]}
title={children[0]}
/>
);
},
});
}
if (!('mentions' in options) || options.mentions) {
const mentionAttrib = 'data-mention';
processingInstructions.push({
Expand Down
14 changes: 14 additions & 0 deletions utils/message_html_to_component.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,18 @@ That was some latex!`;

expect(messageHtmlToComponent(html)).toMatchSnapshot();
});

test('link without enabled tooltip plugins', () => {
const input = 'lorem ipsum www.dolor.com sit amet';
const html = TextFormatting.formatText(input);

expect(messageHtmlToComponent(html)).toMatchSnapshot();
});

test('link with enabled a tooltip plugin', () => {
const input = 'lorem ipsum www.dolor.com sit amet';
const html = TextFormatting.formatText(input);

expect(messageHtmlToComponent(html, false, {hasPluginTooltips: true})).toMatchSnapshot();
});
});

0 comments on commit 850b003

Please sign in to comment.