UNPKG

@atlaskit/renderer

Version:
224 lines (222 loc) • 9 kB
/** * @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;