[MM-62381] Move LinkTooltip from Popper to Floating-UI (#29727)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
a3b2ecec19
Коммит
db3b72e46c
@@ -39,23 +39,20 @@ describe('Link tooltips', () => {
|
|||||||
cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => {
|
cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => {
|
||||||
cy.visit(`/${team.name}/channels/${channel.name}`);
|
cy.visit(`/${team.name}/channels/${channel.name}`);
|
||||||
});
|
});
|
||||||
cy.postMessage('www.test.com');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MM-T3422 fade in and out with an animation', () => {
|
it('MM-T3422 fade in and out with an animation', () => {
|
||||||
cy.get('a[href*="www.test.com"] span').as('link');
|
const url = 'www.test.com';
|
||||||
cy.contains('This is a custom tooltip from the Demo Plugin').parents('.tooltip-container').as('tooltip-container');
|
cy.postMessage(url);
|
||||||
|
cy.uiWaitUntilMessagePostedIncludes(url);
|
||||||
|
|
||||||
// # Mouse over the link
|
// # Hover over the plugin link
|
||||||
cy.get('@link').trigger('mouseover');
|
cy.findByText(url).should('exist').focus();
|
||||||
|
|
||||||
// * Check tooltip has appeared
|
// * Check tooltip has appeared
|
||||||
cy.get('@tooltip-container').should('have.class', 'visible');
|
cy.findByText('This is a custom tooltip from the Demo Plugin').should('be.visible');
|
||||||
|
|
||||||
// # Mouse out the link
|
// # Close the tooltip
|
||||||
cy.get('@link').trigger('mouseout');
|
cy.get('body').type('{esc}');
|
||||||
|
|
||||||
// * Check tooltip has disappeared
|
|
||||||
cy.get('@tooltip-container').should('not.have.class', 'visible');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`components/link_tooltip/link_tooltip should match snapshot 1`] = `
|
|
||||||
<Fragment>
|
|
||||||
<div
|
|
||||||
className="tooltip-container"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"alignItems": "center",
|
|
||||||
"display": "flex",
|
|
||||||
"flexDirection": "column",
|
|
||||||
"left": -1000,
|
|
||||||
"position": "absolute",
|
|
||||||
"top": -1000,
|
|
||||||
"zIndex": 1070,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Connect(Pluggable)
|
|
||||||
href="www.test.com"
|
|
||||||
pluggableName="LinkTooltip"
|
|
||||||
show={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
data-channel-mention="somechannel"
|
|
||||||
data-hashtag="#somehashtag"
|
|
||||||
data-link="somelink"
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
|
||||||
test title
|
|
||||||
</span>
|
|
||||||
</Fragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`components/link_tooltip/link_tooltip should match snapshot with uncommon link structure 1`] = `
|
|
||||||
<Fragment>
|
|
||||||
<div
|
|
||||||
className="tooltip-container"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"alignItems": "center",
|
|
||||||
"display": "flex",
|
|
||||||
"flexDirection": "column",
|
|
||||||
"left": -1000,
|
|
||||||
"position": "absolute",
|
|
||||||
"top": -1000,
|
|
||||||
"zIndex": 1070,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Connect(Pluggable)
|
|
||||||
href="https://www.google.com"
|
|
||||||
pluggableName="LinkTooltip"
|
|
||||||
show={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onMouseOver={[Function]}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="codespan__pre-wrap"
|
|
||||||
>
|
|
||||||
<code>
|
|
||||||
foo
|
|
||||||
</code>
|
|
||||||
</span>
|
|
||||||
and
|
|
||||||
<span
|
|
||||||
className="codespan__pre-wrap"
|
|
||||||
>
|
|
||||||
<code>
|
|
||||||
bar
|
|
||||||
</code>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</Fragment>
|
|
||||||
`;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
@use 'utils/variables';
|
|
||||||
|
|
||||||
.tooltip-container {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity variables.$transition-quick ease-in;
|
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
&.visible {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {shallow} from 'enzyme';
|
|
||||||
import React from 'react';
|
|
||||||
import type {ReactPortal} from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
import LinkTooltip from 'components/link_tooltip/link_tooltip';
|
|
||||||
|
|
||||||
describe('components/link_tooltip/link_tooltip', () => {
|
|
||||||
test('should match snapshot', () => {
|
|
||||||
ReactDOM.createPortal = (node) => node as ReactPortal;
|
|
||||||
const wrapper = shallow<LinkTooltip>(
|
|
||||||
<LinkTooltip
|
|
||||||
href={'www.test.com'}
|
|
||||||
attributes={{
|
|
||||||
class: 'mention-highlight',
|
|
||||||
'data-hashtag': '#somehashtag',
|
|
||||||
'data-link': 'somelink',
|
|
||||||
'data-channel-mention': 'somechannel',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{'test title'}
|
|
||||||
</LinkTooltip>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
expect(wrapper.find('span').text()).toBe('test title');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should match snapshot with uncommon link structure', () => {
|
|
||||||
ReactDOM.createPortal = (node) => node as ReactPortal;
|
|
||||||
const wrapper = shallow<LinkTooltip>(
|
|
||||||
<LinkTooltip
|
|
||||||
href={'https://www.google.com'}
|
|
||||||
attributes={{}}
|
|
||||||
>
|
|
||||||
<span className='codespan__pre-wrap'>
|
|
||||||
<code>{'foo'}</code>
|
|
||||||
</span>
|
|
||||||
{' and '}
|
|
||||||
<span className='codespan__pre-wrap'>
|
|
||||||
<code>{'bar'}</code>
|
|
||||||
</span>
|
|
||||||
</LinkTooltip>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
expect(wrapper.find('span').at(0).text()).toBe('foo and bar');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import Popper from 'popper.js';
|
|
||||||
import React from 'react';
|
|
||||||
import type {RefObject, CSSProperties} from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
import Pluggable from 'plugins/pluggable';
|
|
||||||
import {Constants} from 'utils/constants';
|
|
||||||
|
|
||||||
import './link_tooltip.scss';
|
|
||||||
|
|
||||||
const tooltipContainerStyles: CSSProperties = {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
zIndex: 1070,
|
|
||||||
position: 'absolute',
|
|
||||||
top: -1000,
|
|
||||||
left: -1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
href: string;
|
|
||||||
attributes: {[attribute: string]: string};
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
show: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class LinkTooltip extends React.PureComponent<Props, State> {
|
|
||||||
private tooltipContainerRef: RefObject<HTMLDivElement>;
|
|
||||||
private hideTimeout: number;
|
|
||||||
private showTimeout: number;
|
|
||||||
private popper?: Popper;
|
|
||||||
|
|
||||||
public constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.tooltipContainerRef = React.createRef();
|
|
||||||
this.showTimeout = -1;
|
|
||||||
this.hideTimeout = -1;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
show: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public showTooltip = (e: React.MouseEvent<HTMLSpanElement>): void => {
|
|
||||||
//clear the hideTimeout in the case when the cursor is moved from a tooltipContainer child to the link
|
|
||||||
window.clearTimeout(this.hideTimeout);
|
|
||||||
|
|
||||||
if (!this.state.show) {
|
|
||||||
const target = e.currentTarget;
|
|
||||||
const tooltipContainer = this.tooltipContainerRef.current;
|
|
||||||
|
|
||||||
//clear the old this.showTimeout if there is any before overriding
|
|
||||||
window.clearTimeout(this.showTimeout);
|
|
||||||
|
|
||||||
this.showTimeout = window.setTimeout(() => {
|
|
||||||
this.setState({show: true});
|
|
||||||
|
|
||||||
if (!tooltipContainer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const addChildEventListeners = (node: Node) => {
|
|
||||||
node.addEventListener('mouseover', () => clearTimeout(this.hideTimeout));
|
|
||||||
(node as HTMLElement).addEventListener('mouseleave', (event) => {
|
|
||||||
if (event.relatedTarget !== null) {
|
|
||||||
this.hideTooltip();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
tooltipContainer.childNodes.forEach(addChildEventListeners);
|
|
||||||
|
|
||||||
this.popper = new Popper(target, tooltipContainer, {
|
|
||||||
placement: 'bottom',
|
|
||||||
modifiers: {
|
|
||||||
preventOverflow: {enabled: false},
|
|
||||||
hide: {enabled: false},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, Constants.OVERLAY_TIME_DELAY);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public hideTooltip = (): void => {
|
|
||||||
//clear the old this.hideTimeout if there is any before overriding
|
|
||||||
window.clearTimeout(this.hideTimeout);
|
|
||||||
|
|
||||||
this.hideTimeout = window.setTimeout(() => {
|
|
||||||
this.setState({show: false});
|
|
||||||
|
|
||||||
//prevent executing the showTimeout after the hideTooltip
|
|
||||||
clearTimeout(this.showTimeout);
|
|
||||||
}, Constants.OVERLAY_TIME_DELAY_SMALL);
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const {href, children, attributes} = this.props;
|
|
||||||
|
|
||||||
const dataAttributes = {
|
|
||||||
'data-hashtag': attributes['data-hashtag'],
|
|
||||||
'data-link': attributes['data-link'],
|
|
||||||
'data-channel-mention': attributes['data-channel-mention'],
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{ReactDOM.createPortal(
|
|
||||||
<div
|
|
||||||
style={tooltipContainerStyles}
|
|
||||||
ref={this.tooltipContainerRef}
|
|
||||||
className={classNames('tooltip-container', {visible: this.state.show})}
|
|
||||||
>
|
|
||||||
<Pluggable
|
|
||||||
href={href}
|
|
||||||
show={this.state.show}
|
|
||||||
pluggableName='LinkTooltip'
|
|
||||||
/>
|
|
||||||
</div>,
|
|
||||||
document.getElementById('root') as HTMLElement,
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
onMouseOver={this.showTooltip}
|
|
||||||
onMouseLeave={this.hideTooltip}
|
|
||||||
{...dataAttributes}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
112
webapp/channels/src/components/plugin_link_tooltip/index.tsx
Обычный файл
112
webapp/channels/src/components/plugin_link_tooltip/index.tsx
Обычный файл
@@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {
|
||||||
|
useFloating,
|
||||||
|
autoUpdate,
|
||||||
|
safePolygon,
|
||||||
|
useHover,
|
||||||
|
useDismiss,
|
||||||
|
useInteractions,
|
||||||
|
FloatingPortal,
|
||||||
|
autoPlacement,
|
||||||
|
inline,
|
||||||
|
useTransitionStyles,
|
||||||
|
FloatingOverlay,
|
||||||
|
FloatingFocusManager,
|
||||||
|
useFocus,
|
||||||
|
} from '@floating-ui/react';
|
||||||
|
import React, {useState} from 'react';
|
||||||
|
import type {AnchorHTMLAttributes, ReactElement} from 'react';
|
||||||
|
|
||||||
|
import Pluggable from 'plugins/pluggable';
|
||||||
|
import {RootHtmlPortalId, OverlaysTimings, OverlayTransitionStyles} from 'utils/constants';
|
||||||
|
|
||||||
|
import './plugin_link_tooltip.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nodeAttributes: AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||||
|
children: ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A key drawback of this component is that it gets attached to all links in the app if any installed plugin
|
||||||
|
* supports link previews. Ideally plugins should have provided a regex matcher upfront, allowing us to
|
||||||
|
* conditionally render the component only when needed.
|
||||||
|
*/
|
||||||
|
export default function PluginLinkTooltip(props: Props) {
|
||||||
|
const [isOpen, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const {refs: {setReference, setFloating}, floatingStyles, context: floatingContext} = useFloating({
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setOpen,
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
middleware: [
|
||||||
|
inline(),
|
||||||
|
autoPlacement({
|
||||||
|
allowedPlacements: ['top', 'bottom'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const {isMounted, styles: transitionStyles} = useTransitionStyles(floatingContext, TRANSITION_STYLE_PROPS);
|
||||||
|
|
||||||
|
const hoverInteractions = useHover(floatingContext, HOVER_PROPS);
|
||||||
|
const focusInteractions = useFocus(floatingContext);
|
||||||
|
const dismissInteraction = useDismiss(floatingContext);
|
||||||
|
|
||||||
|
const {getReferenceProps, getFloatingProps} = useInteractions([
|
||||||
|
hoverInteractions,
|
||||||
|
focusInteractions,
|
||||||
|
dismissInteraction,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
ref={setReference}
|
||||||
|
{...props.nodeAttributes}
|
||||||
|
{...getReferenceProps()}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{isMounted && (
|
||||||
|
<FloatingPortal id={RootHtmlPortalId}>
|
||||||
|
<FloatingOverlay className='plugin-link-tooltip-floating-overlay'>
|
||||||
|
<FloatingFocusManager context={floatingContext}>
|
||||||
|
<div
|
||||||
|
ref={setFloating}
|
||||||
|
style={{...floatingStyles, ...transitionStyles}}
|
||||||
|
{...getFloatingProps()}
|
||||||
|
>
|
||||||
|
<Pluggable
|
||||||
|
href={props.nodeAttributes.href}
|
||||||
|
show={true}
|
||||||
|
pluggableName='LinkTooltip'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FloatingFocusManager>
|
||||||
|
</FloatingOverlay>
|
||||||
|
</FloatingPortal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRANSITION_STYLE_PROPS = {
|
||||||
|
duration: {
|
||||||
|
open: OverlaysTimings.FADE_IN_DURATION,
|
||||||
|
close: OverlaysTimings.FADE_OUT_DURATION,
|
||||||
|
},
|
||||||
|
initial: OverlayTransitionStyles.START,
|
||||||
|
};
|
||||||
|
|
||||||
|
const HOVER_PROPS = {
|
||||||
|
restMs: OverlaysTimings.CURSOR_REST_TIME_BEFORE_OPEN,
|
||||||
|
move: false,
|
||||||
|
handleClose: safePolygon({
|
||||||
|
requireIntent: false,
|
||||||
|
blockPointerEvents: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
@use 'utils/variables';
|
||||||
|
|
||||||
|
.plugin-link-tooltip-floating-overlay {
|
||||||
|
z-index: variables.$z-index-popover;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is as per UX guidelines what the container of the
|
||||||
|
// plugin link tooltip should be, not included currently
|
||||||
|
// as it will be a breaking change for the plugin's implemented
|
||||||
|
//link tooltips with styling
|
||||||
|
.plugin-link-tooltip-container {
|
||||||
|
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--center-channel-bg);
|
||||||
|
box-shadow: var(--elevation-4);
|
||||||
|
|
||||||
|
.plugin-link-tooltip-arrow {
|
||||||
|
fill: var(--center-channel-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -144,26 +144,18 @@ Array [
|
|||||||
exports[`messageHtmlToComponent link with enabled a tooltip plugin 1`] = `
|
exports[`messageHtmlToComponent link with enabled a tooltip plugin 1`] = `
|
||||||
<p>
|
<p>
|
||||||
lorem ipsum
|
lorem ipsum
|
||||||
<a
|
<PluginLinkTooltip
|
||||||
className="theme markdown__link"
|
nodeAttributes={
|
||||||
href="http://www.dolor.com"
|
Object {
|
||||||
rel="noreferrer"
|
"class": "theme markdown__link",
|
||||||
target="_blank"
|
"href": "http://www.dolor.com",
|
||||||
>
|
"rel": "noreferrer",
|
||||||
<LinkTooltip
|
"target": "_blank",
|
||||||
attributes={
|
|
||||||
Object {
|
|
||||||
"class": "theme markdown__link",
|
|
||||||
"href": "http://www.dolor.com",
|
|
||||||
"rel": "noreferrer",
|
|
||||||
"target": "_blank",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
href="http://www.dolor.com"
|
}
|
||||||
>
|
>
|
||||||
www.dolor.com
|
www.dolor.com
|
||||||
</LinkTooltip>
|
</PluginLinkTooltip>
|
||||||
</a>
|
|
||||||
sit amet
|
sit amet
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -2009,8 +2009,6 @@ export const Constants = {
|
|||||||
COMMAND_SUGGESTION_CHANNEL: 'channel',
|
COMMAND_SUGGESTION_CHANNEL: 'channel',
|
||||||
COMMAND_SUGGESTION_USER: 'user',
|
COMMAND_SUGGESTION_USER: 'user',
|
||||||
},
|
},
|
||||||
OVERLAY_TIME_DELAY_SMALL: 100,
|
|
||||||
OVERLAY_TIME_DELAY: 400,
|
|
||||||
PERMALINK_FADEOUT: 5000,
|
PERMALINK_FADEOUT: 5000,
|
||||||
DEFAULT_MAX_USERS_PER_TEAM: 50,
|
DEFAULT_MAX_USERS_PER_TEAM: 50,
|
||||||
DEFAULT_MAX_CHANNELS_PER_TEAM: 2000,
|
DEFAULT_MAX_CHANNELS_PER_TEAM: 2000,
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import AtSumOfMembersMention from 'components/at_sum_members_mention';
|
|||||||
import CodeBlock from 'components/code_block/code_block';
|
import CodeBlock from 'components/code_block/code_block';
|
||||||
import LatexBlock from 'components/latex_block';
|
import LatexBlock from 'components/latex_block';
|
||||||
import LatexInline from 'components/latex_inline';
|
import LatexInline from 'components/latex_inline';
|
||||||
import LinkTooltip from 'components/link_tooltip/link_tooltip';
|
|
||||||
import MarkdownImage from 'components/markdown_image';
|
import MarkdownImage from 'components/markdown_image';
|
||||||
|
import PluginLinkTooltip from 'components/plugin_link_tooltip';
|
||||||
import PostEmoji from 'components/post_emoji';
|
import PostEmoji from 'components/post_emoji';
|
||||||
import PostEditedIndicator from 'components/post_view/post_edited_indicator';
|
import PostEditedIndicator from 'components/post_view/post_edited_indicator';
|
||||||
|
|
||||||
@@ -107,18 +107,14 @@ export function messageHtmlToComponent(html: string, options: Options = {}) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (options.hasPluginTooltips) {
|
if (options.hasPluginTooltips) {
|
||||||
const hrefAttrib = 'href';
|
|
||||||
processingInstructions.push({
|
processingInstructions.push({
|
||||||
replaceChildren: true,
|
replaceChildren: false,
|
||||||
shouldProcessNode: (node: any) => node.type === 'tag' && node.name === 'a' && node.attribs[hrefAttrib],
|
shouldProcessNode: (node: any) => node.type === 'tag' && node.name === 'a' && node.attribs.href,
|
||||||
processNode: (node: any, children: any) => {
|
processNode: (node: any, children: any) => {
|
||||||
return (
|
return (
|
||||||
<LinkTooltip
|
<PluginLinkTooltip nodeAttributes={node.attribs}>
|
||||||
href={node.attribs[hrefAttrib]}
|
|
||||||
attributes={node.attribs}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</LinkTooltip>
|
</PluginLinkTooltip>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user