@atlaskit/renderer 126.4.0 → 126.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -70,7 +70,7 @@ var DEGRADED_SEVERITY_THRESHOLD = exports.DEGRADED_SEVERITY_THRESHOLD = 3000;
70
70
  var TABLE_INFO_TIMEOUT = 10000;
71
71
  var RENDER_EVENT_SAMPLE_RATE = 0.2;
72
72
  var packageName = "@atlaskit/renderer";
73
- var packageVersion = "126.3.0";
73
+ var packageVersion = "126.5.0";
74
74
  var setAsQueryContainerStyles = (0, _react2.css)({
75
75
  containerName: 'ak-renderer-wrapper',
76
76
  containerType: 'inline-size'
@@ -8,6 +8,16 @@ export const headingAnchorLinkMessages = defineMessages({
8
8
  defaultMessage: 'Copy link to heading',
9
9
  description: 'Copy heading link to clipboard'
10
10
  },
11
+ copyLinkToClipboard: {
12
+ id: 'fabric.editor.headingLink.copyAnchorLinkTo',
13
+ defaultMessage: 'Copy link to',
14
+ description: 'Copy heading link to clipboard. Will be used as part of a 2-part a11y label ("Copy link to", "{heading text}")'
15
+ },
16
+ copyHeadingLinkLabelledBy: {
17
+ id: 'fabric.editor.headingLink.copyAnchorLinkLabelledBy',
18
+ defaultMessage: '{copyLink} {heading}',
19
+ description: 'The order in which to read the parts of the aria-labelledby for the copy heading link button depending on the grammar of the language. {copyLink} will be replaced with the "Copy link to" text and {heading} will be replaced with the actual heading text'
20
+ },
11
21
  copiedHeadingLinkToClipboard: {
12
22
  id: 'fabric.editor.headingLink.copied',
13
23
  defaultMessage: 'Copied!',
@@ -10,6 +10,7 @@ import React from 'react';
10
10
  import { css, jsx } from '@emotion/react';
11
11
  import { injectIntl } from 'react-intl-next';
12
12
  import LinkIcon from '@atlaskit/icon/core/link';
13
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
13
14
  import Tooltip from '@atlaskit/tooltip';
14
15
  import { headingAnchorLinkMessages } from '../../messages';
15
16
  export const HeadingAnchorWrapperClassName = 'heading-anchor-wrapper';
@@ -36,8 +37,8 @@ const copyAnchorButtonStyles = css({
36
37
  // Ignored via go/ees005
37
38
  // eslint-disable-next-line @repo/internal/react/no-class-components
38
39
  class HeadingAnchor extends React.PureComponent {
39
- constructor(...args) {
40
- super(...args);
40
+ constructor(props) {
41
+ super(props);
41
42
  _defineProperty(this, "state", {
42
43
  tooltipMessage: '',
43
44
  isClicked: false
@@ -64,35 +65,43 @@ class HeadingAnchor extends React.PureComponent {
64
65
  try {
65
66
  await this.props.onCopyText();
66
67
  this.setTooltipState(copiedHeadingLinkToClipboard, true);
67
- } catch (e) {
68
+ } catch {
68
69
  this.setTooltipState(failedToCopyHeadingLink);
69
70
  }
70
71
  });
71
72
  _defineProperty(this, "resetMessage", () => {
72
- this.setTooltipState(headingAnchorLinkMessages.copyHeadingLinkToClipboard);
73
+ const tooltip = expValEquals('platform_editor_copy_link_a11y_inconsistency_fix', 'isEnabled', true) ? headingAnchorLinkMessages.copyLinkToClipboard : headingAnchorLinkMessages.copyHeadingLinkToClipboard;
74
+ this.setTooltipState(tooltip);
73
75
  });
74
76
  _defineProperty(this, "renderAnchorButton", () => {
75
77
  const {
76
78
  hideFromScreenReader = false,
77
79
  headingId
78
80
  } = this.props;
81
+ const labelledBy = expValEquals('platform_editor_copy_link_a11y_inconsistency_fix', 'isEnabled', true) ? this.props.intl.formatMessage(headingAnchorLinkMessages.copyHeadingLinkLabelledBy, {
82
+ copyLink: this.copyLinkId,
83
+ heading: headingId
84
+ }) : headingId;
85
+ const tabIndex = expValEquals('platform_editor_copy_link_a11y_inconsistency_fix', 'isEnabled', true) ? 0 : hideFromScreenReader ? undefined : -1;
79
86
  return jsx("button", {
80
87
  "data-testid": "anchor-button",
88
+ id: this.copyLinkId,
81
89
  css: copyAnchorButtonStyles
82
90
  // eslint-disable-next-line @atlassian/a11y/mouse-events-have-key-events
83
91
  ,
84
92
  onMouseLeave: this.resetMessage,
85
93
  onClick: this.copyToClipboard,
86
94
  "aria-hidden": hideFromScreenReader,
87
- tabIndex: hideFromScreenReader ? undefined : -1,
95
+ tabIndex: tabIndex,
88
96
  "aria-label": hideFromScreenReader ? undefined : this.state.tooltipMessage,
89
- "aria-labelledby": hideFromScreenReader ? undefined : headingId,
97
+ "aria-labelledby": hideFromScreenReader ? undefined : labelledBy,
90
98
  type: "button"
91
99
  }, jsx(LinkIcon, {
92
100
  label: this.getCopyAriaLabel(),
93
101
  color: this.state.isClicked ? "var(--ds-icon-selected, #1868DB)" : "var(--ds-icon-subtle, #505258)"
94
102
  }));
95
103
  });
104
+ this.copyLinkId = expValEquals('platform_editor_copy_link_a11y_inconsistency_fix', 'isEnabled', true) ? crypto.randomUUID() : undefined;
96
105
  }
97
106
  componentDidMount() {
98
107
  this.resetMessage();
@@ -1,10 +1,19 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ * @jsxFrag
5
+ */
1
6
  import React from 'react';
2
- import VisuallyHidden from '@atlaskit/visually-hidden';
3
7
  import { abortAll } from '@atlaskit/react-ufo/interaction-metrics';
4
8
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
9
+ import VisuallyHidden from '@atlaskit/visually-hidden';
10
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
5
11
  import AnalyticsContext from '../../analytics/analyticsContext';
6
12
  import { copyTextToClipboard } from '../utils/clipboard';
7
13
  import HeadingAnchor from './heading-anchor';
14
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
15
+ import { css, jsx } from '@emotion/react';
16
+ const RENDERER_HEADING_WRAPPER = 'renderer-heading-wrapper';
8
17
  const getCurrentUrlWithHash = (hash = '') => {
9
18
  const url = new URL(window.location.href);
10
19
  url.search = ''; // clear any query params so that the page will correctly scroll to the anchor
@@ -17,15 +26,22 @@ function hasRightAlignmentMark(marks) {
17
26
  }
18
27
  return marks.some(mark => mark.type.name === 'alignment' && mark.attrs.align === 'end');
19
28
  }
29
+ const wrapperStyles = css({
30
+ // Important: do NOT use flex here.
31
+ // With flex + baseline alignment, the anchor aligns to the *first line* of a multi-line heading,
32
+ // which visually places it at the top-right. We want the anchor to sit immediately after the
33
+ // last character of the heading (i.e. after the final wrapped line), so we use normal inline flow.
34
+ display: 'block'
35
+ });
20
36
  function WrappedHeadingAnchor({
21
37
  enableNestedHeaderLinks,
22
38
  level,
23
39
  headingId,
24
40
  hideFromScreenReader
25
41
  }) {
26
- return /*#__PURE__*/React.createElement(AnalyticsContext.Consumer, null, ({
42
+ return jsx(AnalyticsContext.Consumer, null, ({
27
43
  fireAnalyticsEvent
28
- }) => /*#__PURE__*/React.createElement(HeadingAnchor, {
44
+ }) => jsx(HeadingAnchor, {
29
45
  enableNestedHeaderLinks: enableNestedHeaderLinks,
30
46
  level: level,
31
47
  onCopyText: () => {
@@ -41,7 +57,14 @@ function WrappedHeadingAnchor({
41
57
  headingId: headingId
42
58
  }));
43
59
  }
44
- function Heading(props) {
60
+ /**
61
+ * Old heading structure (before a11y fix):
62
+ * - headning anchor is rendered INSIDE the heading element
63
+ * - A duplicate anchor is rendered in VisuallyHidden for screen readers
64
+ * - The visible button has hideFromScreenReader={true}
65
+ *
66
+ */
67
+ function HeadingWithDuplicateAnchor(props) {
45
68
  const {
46
69
  headingId,
47
70
  dataAttributes,
@@ -66,28 +89,134 @@ function Heading(props) {
66
89
  mouseEntered.current = true;
67
90
  }
68
91
  };
69
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(HX, {
92
+ return jsx(React.Fragment, null, jsx(HX, {
70
93
  id: headingIdToUse,
71
94
  "data-local-id": localId,
72
95
  "data-renderer-start-pos": dataAttributes['data-renderer-start-pos'],
73
96
  "data-as-inline": asInline,
74
97
  onMouseEnter: mouseEnterHandler
75
- }, /*#__PURE__*/React.createElement(React.Fragment, null, showAnchorLink && headingId && isRightAligned && /*#__PURE__*/React.createElement(WrappedHeadingAnchor, {
98
+ }, jsx(React.Fragment, null, showAnchorLink && headingId && isRightAligned && jsx(WrappedHeadingAnchor, {
76
99
  level: props.level,
77
100
  enableNestedHeaderLinks: enableNestedHeaderLinks,
78
101
  headingId: headingId,
79
102
  hideFromScreenReader: true
80
- }), props.children, showAnchorLink && headingId && !isRightAligned && /*#__PURE__*/React.createElement(WrappedHeadingAnchor, {
103
+ }), props.children, showAnchorLink && headingId && !isRightAligned && jsx(WrappedHeadingAnchor, {
81
104
  level: props.level,
82
105
  enableNestedHeaderLinks: enableNestedHeaderLinks,
83
106
  headingId: headingId,
84
107
  hideFromScreenReader: true
85
- }))), /*#__PURE__*/React.createElement(VisuallyHidden, {
108
+ }))), jsx(VisuallyHidden, {
86
109
  testId: "visually-hidden-heading-anchor"
87
- }, showAnchorLink && headingId && /*#__PURE__*/React.createElement(WrappedHeadingAnchor, {
110
+ }, showAnchorLink && headingId && jsx(WrappedHeadingAnchor, {
88
111
  level: props.level,
89
112
  enableNestedHeaderLinks: enableNestedHeaderLinks,
90
113
  headingId: headingId
91
114
  })));
92
115
  }
116
+
117
+ /**
118
+ * New heading structure (a11y fix):
119
+ * - Heading anchor is rendered OUTSIDE the heading element in a .renderer-heading-wrapper div
120
+ * - Uses data-level attribute for CSS styling
121
+ * - Better accessibility: heading contains only text, button is a sibling
122
+ */
123
+ function HeadingWithWrapper(props) {
124
+ const {
125
+ headingId,
126
+ dataAttributes,
127
+ allowHeadingAnchorLinks,
128
+ marks,
129
+ invisible,
130
+ localId,
131
+ asInline
132
+ } = props;
133
+ const HX = `h${props.level}`;
134
+ const mouseEntered = React.useRef(false);
135
+ const showAnchorLink = !!props.showAnchorLink;
136
+ const isRightAligned = hasRightAlignmentMark(marks);
137
+ const enableNestedHeaderLinks = allowHeadingAnchorLinks && allowHeadingAnchorLinks.allowNestedHeaderLinks;
138
+ const headingIdToUse = invisible ? undefined : headingId;
139
+ const mouseEnterHandler = () => {
140
+ if (showAnchorLink && !mouseEntered.current) {
141
+ // Abort TTVC calculation when the mouse hovers over heading. Hovering over
142
+ // heading render heading anchor and inline comment buttons. These user-induced
143
+ // DOM changes are valid reasons to abort the TTVC calculation.
144
+ abortAll('new_interaction');
145
+ mouseEntered.current = true;
146
+ }
147
+ };
148
+ return jsx("div", {
149
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop
150
+ className: RENDERER_HEADING_WRAPPER,
151
+ "data-testid": RENDERER_HEADING_WRAPPER,
152
+ "data-level": props.level,
153
+ css: wrapperStyles
154
+ }, showAnchorLink && headingId && isRightAligned && jsx(WrappedHeadingAnchor, {
155
+ level: props.level,
156
+ enableNestedHeaderLinks: enableNestedHeaderLinks,
157
+ headingId: headingId,
158
+ hideFromScreenReader: false
159
+ }), jsx(HX, {
160
+ id: headingIdToUse,
161
+ "data-local-id": localId,
162
+ "data-renderer-start-pos": dataAttributes['data-renderer-start-pos'],
163
+ "data-as-inline": asInline,
164
+ onMouseEnter: mouseEnterHandler
165
+ }, props.children), showAnchorLink && headingId && !isRightAligned && jsx(WrappedHeadingAnchor, {
166
+ level: props.level,
167
+ enableNestedHeaderLinks: enableNestedHeaderLinks,
168
+ headingId: headingId,
169
+ hideFromScreenReader: false
170
+ }));
171
+ }
172
+
173
+ /**
174
+ * Gated Heading component:
175
+ * - When platform_editor_copy_link_a11y_inconsistency_fix experiment is enabled,
176
+ * returns HeadingWithWrapper (new a11y-improved structure)
177
+ * - Otherwise returns HeadingWithDuplicateAnchor (old structure)
178
+ */
179
+ function Heading({
180
+ allowHeadingAnchorLinks,
181
+ children,
182
+ dataAttributes,
183
+ headingId,
184
+ invisible,
185
+ level,
186
+ localId,
187
+ marks,
188
+ nodeType,
189
+ showAnchorLink,
190
+ serializer,
191
+ asInline
192
+ }) {
193
+ if (expValEquals('platform_editor_copy_link_a11y_inconsistency_fix', 'isEnabled', true)) {
194
+ return jsx(HeadingWithWrapper, {
195
+ allowHeadingAnchorLinks: allowHeadingAnchorLinks,
196
+ dataAttributes: dataAttributes,
197
+ headingId: headingId,
198
+ invisible: invisible,
199
+ level: level,
200
+ localId: localId,
201
+ marks: marks,
202
+ nodeType: nodeType,
203
+ serializer: serializer,
204
+ showAnchorLink: showAnchorLink,
205
+ asInline: asInline
206
+ }, children);
207
+ }
208
+ return jsx(HeadingWithDuplicateAnchor, {
209
+ allowHeadingAnchorLinks: allowHeadingAnchorLinks,
210
+ dataAttributes: dataAttributes,
211
+ headingId: headingId,
212
+ invisible: invisible,
213
+ level: level,
214
+ localId: localId,
215
+ marks: marks,
216
+ nodeType: nodeType,
217
+ serializer: serializer,
218
+ showAnchorLink: showAnchorLink,
219
+ asInline: asInline
220
+ }, children);
221
+ }
93
222
  export default Heading;