@atlaskit/renderer
Version:
Renderer component
224 lines (222 loc) • 9 kB
JavaScript
/**
* @jsxRuntime classic
* @jsx jsx
* @jsxFrag
*/
import React from 'react';
import { abortAll } from '@atlaskit/react-ufo/interaction-metrics';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import VisuallyHidden from '@atlaskit/visually-hidden';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import AnalyticsContext from '../../analytics/analyticsContext';
import { copyTextToClipboard } from '../utils/clipboard';
import HeadingAnchor from './heading-anchor';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
import { css, jsx } from '@emotion/react';
var RENDERER_HEADING_WRAPPER = 'renderer-heading-wrapper';
var getCurrentUrlWithHash = function getCurrentUrlWithHash() {
var hash = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var url = new URL(window.location.href);
url.search = ''; // clear any query params so that the page will correctly scroll to the anchor
url.hash = encodeURIComponent(hash);
return url.href;
};
function hasRightAlignmentMark(marks) {
if (!marks || !marks.length) {
return false;
}
return marks.some(function (mark) {
return mark.type.name === 'alignment' && mark.attrs.align === 'end';
});
}
var wrapperStyles = css({
// Important: do NOT use flex here.
// With flex + baseline alignment, the anchor aligns to the *first line* of a multi-line heading,
// which visually places it at the top-right. We want the anchor to sit immediately after the
// last character of the heading (i.e. after the final wrapped line), so we use normal inline flow.
display: 'block'
});
function WrappedHeadingAnchor(_ref) {
var enableNestedHeaderLinks = _ref.enableNestedHeaderLinks,
level = _ref.level,
headingId = _ref.headingId,
hideFromScreenReader = _ref.hideFromScreenReader;
return jsx(AnalyticsContext.Consumer, null, function (_ref2) {
var fireAnalyticsEvent = _ref2.fireAnalyticsEvent;
return jsx(HeadingAnchor, {
enableNestedHeaderLinks: enableNestedHeaderLinks,
level: level
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onCopyText: function onCopyText() {
fireAnalyticsEvent({
action: ACTION.CLICKED,
actionSubject: ACTION_SUBJECT.BUTTON,
actionSubjectId: ACTION_SUBJECT_ID.HEADING_ANCHOR_LINK,
eventType: EVENT_TYPE.UI
});
return copyTextToClipboard(getCurrentUrlWithHash(headingId));
},
hideFromScreenReader: hideFromScreenReader,
headingId: headingId
});
});
}
/**
* Old heading structure (before a11y fix):
* - headning anchor is rendered INSIDE the heading element
* - A duplicate anchor is rendered in VisuallyHidden for screen readers
* - The visible button has hideFromScreenReader={true}
*
*/
function HeadingWithDuplicateAnchor(props) {
var headingId = props.headingId,
dataAttributes = props.dataAttributes,
allowHeadingAnchorLinks = props.allowHeadingAnchorLinks,
marks = props.marks,
invisible = props.invisible,
localId = props.localId,
asInline = props.asInline;
var HX = "h".concat(props.level);
var mouseEntered = React.useRef(false);
var showAnchorLink = !!props.showAnchorLink;
var isRightAligned = hasRightAlignmentMark(marks);
var enableNestedHeaderLinks = allowHeadingAnchorLinks && allowHeadingAnchorLinks.allowNestedHeaderLinks;
var headingIdToUse = invisible ? undefined : headingId;
var mouseEnterHandler = function mouseEnterHandler() {
if (showAnchorLink && !mouseEntered.current) {
// Abort TTVC calculation when the mouse hovers over heading. Hovering over
// heading render heading anchor and inline comment buttons. These user-induced
// DOM changes are valid reasons to abort the TTVC calculation.
abortAll('new_interaction');
mouseEntered.current = true;
}
};
return jsx(React.Fragment, null, jsx(HX, {
id: headingIdToUse,
"data-local-id": localId,
"data-renderer-start-pos": dataAttributes['data-renderer-start-pos'],
"data-as-inline": asInline,
onMouseEnter: mouseEnterHandler,
tabIndex: expValEquals('confluence_toc_nav_a11y', 'isEnabled', true) ? -1 : undefined
}, jsx(React.Fragment, null, showAnchorLink && headingId && isRightAligned && jsx(WrappedHeadingAnchor, {
level: props.level,
enableNestedHeaderLinks: enableNestedHeaderLinks,
headingId: headingId,
hideFromScreenReader: true
}), props.children, showAnchorLink && headingId && !isRightAligned && jsx(WrappedHeadingAnchor, {
level: props.level,
enableNestedHeaderLinks: enableNestedHeaderLinks,
headingId: headingId,
hideFromScreenReader: true
}))), jsx(VisuallyHidden, {
testId: "visually-hidden-heading-anchor"
}, showAnchorLink && headingId && jsx(WrappedHeadingAnchor, {
level: props.level,
enableNestedHeaderLinks: enableNestedHeaderLinks,
headingId: headingId
})));
}
/**
* New heading structure (a11y fix):
* - Heading anchor is rendered OUTSIDE the heading element in a .renderer-heading-wrapper div
* - Uses data-level attribute for CSS styling
* - Better accessibility: heading contains only text, button is a sibling
*/
function HeadingWithWrapper(props) {
var headingId = props.headingId,
dataAttributes = props.dataAttributes,
allowHeadingAnchorLinks = props.allowHeadingAnchorLinks,
marks = props.marks,
invisible = props.invisible,
localId = props.localId,
asInline = props.asInline;
var HX = "h".concat(props.level);
var mouseEntered = React.useRef(false);
var showAnchorLink = !!props.showAnchorLink;
var isRightAligned = hasRightAlignmentMark(marks);
var enableNestedHeaderLinks = allowHeadingAnchorLinks && allowHeadingAnchorLinks.allowNestedHeaderLinks;
var headingIdToUse = invisible ? undefined : headingId;
var mouseEnterHandler = function mouseEnterHandler() {
if (showAnchorLink && !mouseEntered.current) {
// Abort TTVC calculation when the mouse hovers over heading. Hovering over
// heading render heading anchor and inline comment buttons. These user-induced
// DOM changes are valid reasons to abort the TTVC calculation.
abortAll('new_interaction');
mouseEntered.current = true;
}
};
return jsx("div", {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop
className: RENDERER_HEADING_WRAPPER,
"data-testid": RENDERER_HEADING_WRAPPER,
"data-level": props.level,
css: wrapperStyles
}, showAnchorLink && headingId && isRightAligned && jsx(WrappedHeadingAnchor, {
level: props.level,
enableNestedHeaderLinks: enableNestedHeaderLinks,
headingId: headingId,
hideFromScreenReader: false
}), jsx(HX, {
id: headingIdToUse,
"data-local-id": localId,
"data-renderer-start-pos": dataAttributes['data-renderer-start-pos'],
"data-as-inline": asInline,
onMouseEnter: mouseEnterHandler,
tabIndex: expValEquals('confluence_toc_nav_a11y', 'isEnabled', true) ? -1 : undefined
}, props.children), showAnchorLink && headingId && !isRightAligned && jsx(WrappedHeadingAnchor, {
level: props.level,
enableNestedHeaderLinks: enableNestedHeaderLinks,
headingId: headingId,
hideFromScreenReader: false
}));
}
/**
* Gated Heading component:
* - When platform_editor_copy_link_a11y_inconsistency_fix experiment is enabled,
* returns HeadingWithWrapper (new a11y-improved structure)
* - Otherwise returns HeadingWithDuplicateAnchor (old structure)
*/
function Heading(_ref3) {
var allowHeadingAnchorLinks = _ref3.allowHeadingAnchorLinks,
children = _ref3.children,
dataAttributes = _ref3.dataAttributes,
headingId = _ref3.headingId,
invisible = _ref3.invisible,
level = _ref3.level,
localId = _ref3.localId,
marks = _ref3.marks,
nodeType = _ref3.nodeType,
showAnchorLink = _ref3.showAnchorLink,
serializer = _ref3.serializer,
asInline = _ref3.asInline;
if (expValEquals('platform_editor_copy_link_a11y_inconsistency_fix', 'isEnabled', true)) {
return jsx(HeadingWithWrapper, {
allowHeadingAnchorLinks: allowHeadingAnchorLinks,
dataAttributes: dataAttributes,
headingId: headingId,
invisible: invisible,
level: level,
localId: localId,
marks: marks,
nodeType: nodeType,
serializer: serializer,
showAnchorLink: showAnchorLink,
asInline: asInline
}, children);
}
return jsx(HeadingWithDuplicateAnchor, {
allowHeadingAnchorLinks: allowHeadingAnchorLinks,
dataAttributes: dataAttributes,
headingId: headingId,
invisible: invisible,
level: level,
localId: localId,
marks: marks,
nodeType: nodeType,
serializer: serializer,
showAnchorLink: showAnchorLink,
asInline: asInline
}, children);
}
export default Heading;