@atlaskit/renderer 137.1.6 → 137.1.7

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.
@@ -13,8 +13,7 @@ import { akEditorLineHeight, akEditorSwoopCubicBezier, akLayoutGutterOffset } fr
13
13
  import ChevronRightIcon from '@atlaskit/icon/core/chevron-right';
14
14
  import Tooltip from '@atlaskit/tooltip/Tooltip';
15
15
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
16
- import { fg } from '@atlaskit/platform-feature-flags/fg';
17
- import { getExpandSearchText } from './utils/expand-search-text';
16
+ import { ExpandBodyProvider, useExpandBody } from './expand-body';
18
17
  import _uniqueId from 'lodash/uniqueId';
19
18
  import { injectIntl } from 'react-intl';
20
19
  import { MODE, PLATFORM } from '../analytics/events';
@@ -139,12 +138,18 @@ const contentContainerStylesNotExpanded = css({
139
138
  // hidden="until-found" attribute on the outer container to hide content.
140
139
  // We remove height:0/overflow:hidden/clip from the inner wrapper so the
141
140
  // browser can actually search through the content.
141
+ //
142
+ // `user-select: none` must NOT be set here, unlike the variant above. WebKit scrolls to a
143
+ // find-in-page match by selecting it, so unselectable text is matched but never revealed: the
144
+ // reader is told there is a hit, the `hidden` attribute stays put, and the expand never opens.
145
+ // Chrome does not need a selection, so it hid the problem. Nothing is lost by leaving it out —
146
+ // content-visibility:hidden already makes a skipped subtree unselectable. It is only needed in the
147
+ // other variant, where the content is merely clipped and would otherwise be selectable.
142
148
  const contentContainerStylesNotExpandedBrowserFind = css({
143
149
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors
144
150
  '.expand-content-wrapper, .nestedExpand-content-wrapper': {
145
151
  width: '100%',
146
- display: 'block',
147
- userSelect: 'none'
152
+ display: 'block'
148
153
  }
149
154
  });
150
155
  const clearNextSiblingMarginTopStyle = css({
@@ -215,15 +220,15 @@ function Expand({
215
220
  rendererContentMode,
216
221
  node
217
222
  }) {
223
+ const ancestorBody = useExpandBody();
218
224
  const [expanded, setExpanded] = useState(false);
219
225
  const [focused, setFocused] = useState(false);
220
226
  /**
221
- * PGXT-9021: latches on the first open so the body stays mounted once rendered, including after
222
- * the expand is collapsed again. Remounting it would re-run every macro and Forge data fetch
223
- * inside on each reopen, which is both slower and visibly reloads content the reader has already
224
- * seen. The saving this gate exists for is at initial load, where the body has never been opened,
225
- * so nothing is given up by keeping it after that. Never set on the server, so the server and the
226
- * client's first render agree and hydration cannot desync.
227
+ * PGXT-9021: once opened, the body stays rendered even if the reader closes the expand again.
228
+ * Throwing it away would re-run every macro and Forge fetch inside it on the next open, which is
229
+ * slower and visibly reloads content the reader has already seen. We only wanted to save work on
230
+ * the first page load, and by now that has happened. Starts false on both the server and the
231
+ * client, so hydration cannot disagree.
227
232
  */
228
233
  const [hasBeenExpanded, setHasBeenExpanded] = useState(false);
229
234
  const isMobile = false;
@@ -241,15 +246,21 @@ function Expand({
241
246
  setExpanded(true);
242
247
  setHasBeenExpanded(true);
243
248
  }, []);
249
+ // The browser found the text inside this expand. Opening this one is not enough: if it sits
250
+ // inside other expands, those have to open too or the reader still cannot see it. Each expand
251
+ // asks the one above it, so a single match opens the whole chain and nothing else.
252
+ const openWithAncestors = useCallback(() => {
253
+ openBody();
254
+ ancestorBody === null || ancestorBody === void 0 ? void 0 : ancestorBody.openWithAncestors();
255
+ }, [ancestorBody, openBody]);
244
256
 
245
- // Feature-detect hidden="until-found" support via the beforematch event.
246
- // Chrome 102+ and Firefox 130+ support it; Safari does not yet.
247
- // In unsupported browsers, setting hidden="until-found" is treated as boolean hidden
248
- // (display:none), which would break the expand entirely.
249
- // Initialised as false and set in useEffect to avoid SSR/client hydration mismatch
250
- // useMemo would return true on the client's first render in supported browsers,
251
- // differing from the server snapshot which always produces false.
252
- const [supportsHiddenUntilFound, setSupportsHiddenUntilFound] = useState(false);
257
+ // Feature-detect hidden="until-found" support via the beforematch event. Chrome 102+,
258
+ // Firefox 139+ and Safari 26.2+ all have it; in a browser without it, hidden="until-found" is
259
+ // treated as plain boolean hidden (display:none), which would break the expand entirely.
260
+ // Starts as `undefined`, meaning "we do not know yet", and is only answered in an effect. We
261
+ // cannot check during render: the server has no browser to check, so it would say no there and
262
+ // yes on the client, and hydration would disagree.
263
+ const [supportsHiddenUntilFound, setSupportsHiddenUntilFound] = useState(undefined);
253
264
  useEffect(() => {
254
265
  const doc = getDocument();
255
266
  setSupportsHiddenUntilFound(doc !== null && doc !== void 0 && doc.body ? 'onbeforematch' in doc.body : false);
@@ -263,9 +274,9 @@ function Expand({
263
274
  // because visibility:hidden blocks browser find. On expanded, we restore visibility to visible.
264
275
  //
265
276
  // Only applied when the browser supports hidden="until-found" (detected via onbeforematch).
266
- // In unsupported browsers (Safari), we skip this entirely and fall back to the normal
267
- // CSS hiding (visibility:hidden + height:0), which doesn't support find-in-page but
268
- // still works correctly for expand/collapse.
277
+ // Without it we skip this entirely and fall back to the normal CSS hiding
278
+ // (visibility:hidden + height:0), which doesn't support find-in-page but still works
279
+ // correctly for expand/collapse.
269
280
  useEffect(() => {
270
281
  const contentContainer = contentContainerRef.current;
271
282
  const contentWrapper = contentWrapperRef.current;
@@ -274,6 +285,13 @@ function Expand({
274
285
  }
275
286
  if (supportsHiddenUntilFound && isExperimentEnabled('platform_editor_close_expand_find') && !expanded) {
276
287
  contentWrapper.setAttribute('hidden', 'until-found');
288
+ // Products ship a CSS reset with `[hidden] { display: none }` — Confluence does. That is an
289
+ // author rule, so it beats the UA stylesheet's
290
+ // `[hidden="until-found"] { content-visibility: hidden }` and takes the content out of the
291
+ // page altogether, where find cannot reach it. Setting display next to the attribute keeps
292
+ // the two together, rather than relying on a class selector elsewhere out-weighing a reset
293
+ // we do not own.
294
+ contentWrapper.style.display = 'block';
277
295
  // Override the CSS visibility:hidden from contentContainerStyles — hidden="until-found"
278
296
  // now handles hiding via content-visibility:hidden, which allows browser find to index
279
297
  // the content. We use 'visible' (not '') because '' only clears the inline style but
@@ -282,6 +300,7 @@ function Expand({
282
300
  contentWrapper.style.visibility = 'visible';
283
301
  } else {
284
302
  contentWrapper.removeAttribute('hidden');
303
+ contentWrapper.style.display = '';
285
304
  contentContainer === null || contentContainer === void 0 ? void 0 : contentContainer.style.removeProperty('visibility');
286
305
  contentWrapper.style.visibility = '';
287
306
  }
@@ -293,20 +312,26 @@ function Expand({
293
312
  const contentWrapper = contentWrapperRef.current;
294
313
  const unbindWrapperBeforeMatch = contentWrapper && supportsHiddenUntilFound ? bind(contentWrapper, {
295
314
  type: 'beforematch',
296
- listener: openBody
315
+ listener: openWithAncestors
297
316
  }) : undefined;
298
317
  return () => {
299
318
  unbindWrapperBeforeMatch === null || unbindWrapperBeforeMatch === void 0 ? void 0 : unbindWrapperBeforeMatch();
300
319
  };
301
- }, [expanded, openBody, supportsHiddenUntilFound]);
320
+ }, [expanded, openWithAncestors, supportsHiddenUntilFound]);
302
321
 
303
- // Until the expand is first opened, stand in a plain-text mirror of the body so
304
- // hidden="until-found" keeps the content findable by browser find without mounting the real
305
- // subtree. The beforematch listener above expands before the browser scrolls to its match.
306
- // Computed lazily: a nested expand is not mounted until its ancestor opens, so its mirror is
307
- // never built on initial load.
308
- const searchText = useMemo(() => node && fg('hot-121622_lazy_load_expand_content') ? getExpandSearchText(node) : undefined, [node]);
309
- const expandContent = searchText && !expanded && !hasBeenExpanded ? jsx("span", null, searchText) : children;
322
+ // While this is false, the blocks of the body show their text instead of rendering. It is true
323
+ // when:
324
+ // - the expand is open, or has been opened before, so the reader wants to see the content;
325
+ // - no node was given, so this expand never opted into lazy loading;
326
+ // - the experiment is off;
327
+ // - the browser has no hidden="until-found", so the text would not be findable and standing it in
328
+ // would gain nothing. While we still do not know, we assume the browser has it — assuming the
329
+ // opposite would render the whole body on the first paint and lose the saving entirely.
330
+ const revealed = expanded || hasBeenExpanded || !node || supportsHiddenUntilFound === false || !isExperimentEnabled('platform_editor_defer_collapsed_expand_body');
331
+ const expandBody = useMemo(() => ({
332
+ revealed,
333
+ openWithAncestors
334
+ }), [revealed, openWithAncestors]);
310
335
  return jsx(Container, {
311
336
  "data-testid": `expand-container-${nodeType}-${id}`,
312
337
  "data-node-type": nodeType,
@@ -371,7 +396,9 @@ function Expand({
371
396
  ref: contentWrapperRef
372
397
  }, jsx(WidthProvider, null, jsx("div", {
373
398
  css: clearNextSiblingMarginTopStyle
374
- }), expandContent))));
399
+ }), jsx(ExpandBodyProvider, {
400
+ value: expandBody
401
+ }, children)))));
375
402
  }
376
403
 
377
404
  // eslint-disable-next-line @typescript-eslint/ban-types
@@ -60,7 +60,7 @@ export const DEGRADED_SEVERITY_THRESHOLD = 3000;
60
60
  const TABLE_INFO_TIMEOUT = 10000;
61
61
  const RENDER_EVENT_SAMPLE_RATE = 0.1;
62
62
  const packageName = "@atlaskit/renderer";
63
- const packageVersion = "137.1.5";
63
+ const packageVersion = "137.1.6";
64
64
  const setAsQueryContainerStyles = css({
65
65
  containerName: 'ak-renderer-wrapper',
66
66
  containerType: 'inline-size'
@@ -0,0 +1,107 @@
1
+ import React, { createContext, isValidElement, useContext } from 'react';
2
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
3
+ import { BLOCK_SEPARATOR, getBlockSearchText, holdsRevealableContent, isExpandNode } from './utils/expand-search-text';
4
+ const ExpandBodyContext = /*#__PURE__*/createContext(null);
5
+ export const ExpandBodyProvider = ExpandBodyContext.Provider;
6
+ export const useExpandBody = () => useContext(ExpandBodyContext);
7
+ /**
8
+ * One or more neighbouring blocks of an expand's body. Shows their text until the expand is
9
+ * opened, then renders them. Falls back to rendering if there is no text, or if there is no
10
+ * expand above it.
11
+ *
12
+ * The text is rendered as-is, with no element around it. Nothing reads it but browser find,
13
+ * which only needs the characters to be in the DOM.
14
+ */
15
+ export const ExpandBodyBlock = ({
16
+ children,
17
+ searchText
18
+ }) => {
19
+ const body = useExpandBody();
20
+ if (searchText === undefined || body === null || body.revealed) {
21
+ return /*#__PURE__*/React.createElement(React.Fragment, null, children);
22
+ }
23
+ return searchText;
24
+ };
25
+
26
+ /**
27
+ * Called for every node the serializer renders. If the node is a block of an expand's body,
28
+ * wraps it so it can show its text instead of rendering while that expand is collapsed.
29
+ * Everything else is returned untouched.
30
+ *
31
+ * A block holding an expand of its own is also returned untouched, so a table with a nested
32
+ * expand in it, or an extension holding stashed ADF, renders even while collapsed. That
33
+ * expand needs an element of its own, because the element browser find reveals is the only
34
+ * way we learn the match was inside it rather than higher up. We give up the saving on those
35
+ * blocks so the reader searches once instead of once per level.
36
+ *
37
+ * `ancestors` is the node's ancestor chain, nearest last.
38
+ */
39
+ export const withExpandBodyBlock = (node, ancestors, index, serialized) => {
40
+ const parent = ancestors[ancestors.length - 1];
41
+ if (!parent || !isExpandNode(parent)) {
42
+ return serialized;
43
+ }
44
+ if (!isExperimentEnabled('platform_editor_defer_collapsed_expand_body') || holdsRevealableContent(node)) {
45
+ return serialized;
46
+ }
47
+ const searchText = getBlockSearchText(node);
48
+ if (searchText === undefined) {
49
+ return serialized;
50
+ }
51
+ return /*#__PURE__*/React.createElement(ExpandBodyBlock, {
52
+ key: `expand-body-block-${index}`,
53
+ searchText: searchText
54
+ }, serialized);
55
+ };
56
+
57
+ /** Whatever the serializer produced for one child: an element, a string, an array of either. */
58
+
59
+ const searchTextOf = child => /*#__PURE__*/isValidElement(child) && child.type === ExpandBodyBlock ? child.props.searchText : undefined;
60
+
61
+ /**
62
+ * Joins neighbouring blocks that are showing text into one, so a run of them is a single string in
63
+ * the DOM rather than one per block. A body of twenty paragraphs becomes one text node instead of
64
+ * twenty.
65
+ *
66
+ * A block that is being rendered — a nested expand, say — ends the run, because the text either
67
+ * side of it has to stay either side of it.
68
+ */
69
+ export const mergeExpandBodyText = children => {
70
+ // Called for every fragment in the document, and only an expand's body has anything to merge, so
71
+ // leave the array alone unless two neighbours are actually showing text.
72
+ const hasRun = children.some((child, index) => index > 0 && searchTextOf(child) !== undefined && searchTextOf(children[index - 1]) !== undefined);
73
+ if (!hasRun) {
74
+ return children;
75
+ }
76
+ const merged = [];
77
+ let run = [];
78
+ const endRun = () => {
79
+ if (run.length === 0) {
80
+ return;
81
+ }
82
+ if (run.length === 1) {
83
+ merged.push(run[0]);
84
+ } else {
85
+ var _key;
86
+ const text = run.map(searchTextOf).filter(Boolean).join(BLOCK_SEPARATOR);
87
+ // The blocks themselves, not the wrappers around them: nesting the wrappers would show
88
+ // each block's text again inside the joined run.
89
+ const blocks = run.map(child => child.props.children);
90
+ merged.push( /*#__PURE__*/React.createElement(ExpandBodyBlock, {
91
+ key: (_key = run[0].key) !== null && _key !== void 0 ? _key : undefined,
92
+ searchText: text
93
+ }, blocks));
94
+ }
95
+ run = [];
96
+ };
97
+ children.forEach(child => {
98
+ if (searchTextOf(child) === undefined) {
99
+ endRun();
100
+ merged.push(child);
101
+ return;
102
+ }
103
+ run.push(child);
104
+ });
105
+ endRun();
106
+ return merged;
107
+ };
@@ -6,20 +6,25 @@ import { findChildrenByMark } from '@atlaskit/editor-prosemirror/utils';
6
6
  */
7
7
 
8
8
  /** Separates text from adjacent blocks so phrases cannot fuse across block boundaries. */
9
- const BLOCK_SEPARATOR = ' ';
9
+ export const BLOCK_SEPARATOR = ' ';
10
10
  const asText = value => typeof value === 'string' ? value : '';
11
+ const nestedContentOf = attrs => {
12
+ var _attrs$parameters;
13
+ return attrs === null || attrs === void 0 ? void 0 : (_attrs$parameters = attrs.parameters) === null || _attrs$parameters === void 0 ? void 0 : _attrs$parameters.nestedContent;
14
+ };
15
+ export const isExpandNode = node => node.type.name === 'expand' || node.type.name === 'nestedExpand';
11
16
 
12
17
  /**
13
- * Extracts text from raw ADF JSON.
18
+ * Reads the text out of raw ADF JSON.
14
19
  *
15
- * Needed because some extensions stash a whole ADF subtree in `attrs.parameters.nestedContent`
16
- * instead of in their node content Confluence does this when it coerces nesting the schema
17
- * cannot represent (an expand more than two collapsible layers deep, say). Such a node is a
18
- * ProseMirror leaf, so the model walk cannot see the subtree even though it renders on the page
19
- * via a nested renderer. Recurses, because nestedContent routinely holds further nested content.
20
+ * Some extensions keep a whole ADF subtree in `attrs.parameters.nestedContent` rather than in their
21
+ * node content. Confluence does this when the authored nesting is deeper than the schema allows —
22
+ * an expand more than two collapsible layers deep, for example. To ProseMirror the extension is a
23
+ * leaf, so walking the node tree never sees that subtree, even though a nested renderer puts it on
24
+ * the page. Recurses, because stashed content often stashes more content of its own.
20
25
  */
21
26
  const adfEntityText = entity => {
22
- var _entity$attrs, _entity$attrs$paramet, _entity$content;
27
+ var _entity$content;
23
28
  if (!entity) {
24
29
  return '';
25
30
  }
@@ -27,57 +32,88 @@ const adfEntityText = entity => {
27
32
  if (entity.type === 'text') {
28
33
  parts.push(asText(entity.text));
29
34
  }
30
- const nestedContent = (_entity$attrs = entity.attrs) === null || _entity$attrs === void 0 ? void 0 : (_entity$attrs$paramet = _entity$attrs.parameters) === null || _entity$attrs$paramet === void 0 ? void 0 : _entity$attrs$paramet.nestedContent;
35
+ const nestedContent = nestedContentOf(entity.attrs);
31
36
  if (nestedContent) {
32
37
  // Prefer the real subtree over `attrs.text`, which is only a placeholder label for it.
33
38
  parts.push(adfEntityText(nestedContent));
34
39
  } else {
35
- var _entity$attrs2;
36
- parts.push(asText((_entity$attrs2 = entity.attrs) === null || _entity$attrs2 === void 0 ? void 0 : _entity$attrs2.text));
40
+ var _entity$attrs;
41
+ parts.push(asText((_entity$attrs = entity.attrs) === null || _entity$attrs === void 0 ? void 0 : _entity$attrs.text));
37
42
  }
38
43
  (_entity$content = entity.content) === null || _entity$content === void 0 ? void 0 : _entity$content.forEach(child => parts.push(adfEntityText(child)));
39
44
  return parts.filter(Boolean).join(BLOCK_SEPARATOR);
40
45
  };
41
46
 
42
47
  /**
43
- * `textBetween` drops every non-text leaf unless the node spec declares `leafText`, and no ADF
44
- * node does. Leaves carry their visible text in attrs, so recover it here.
48
+ * `textBetween` skips every leaf node unless its spec declares `leafText`, and no ADF node
49
+ * does so mentions, emoji, dates and statuses would all be lost. They keep their visible
50
+ * text in attrs, so read it from there.
45
51
  */
46
52
  const leafText = leaf => {
47
- var _leaf$attrs, _leaf$attrs$parameter, _leaf$attrs2;
48
- const nestedContent = (_leaf$attrs = leaf.attrs) === null || _leaf$attrs === void 0 ? void 0 : (_leaf$attrs$parameter = _leaf$attrs.parameters) === null || _leaf$attrs$parameter === void 0 ? void 0 : _leaf$attrs$parameter.nestedContent;
53
+ var _leaf$attrs;
54
+ const nestedContent = nestedContentOf(leaf.attrs);
49
55
  if (nestedContent) {
50
56
  return adfEntityText(nestedContent);
51
57
  }
52
- return asText((_leaf$attrs2 = leaf.attrs) === null || _leaf$attrs2 === void 0 ? void 0 : _leaf$attrs2.text);
58
+ return asText((_leaf$attrs = leaf.attrs) === null || _leaf$attrs === void 0 ? void 0 : _leaf$attrs.text);
59
+ };
60
+ const isRevealable = node => isExpandNode(node) || Boolean(nestedContentOf(node.attrs));
61
+ const revealableCache = new WeakMap();
62
+
63
+ /**
64
+ * Whether this block holds something that can be opened on its own: an expand, or an extension
65
+ * holding stashed ADF that a nested renderer will mount expands from.
66
+ *
67
+ * Those need a real element even while the expand around them is collapsed. The element is how we
68
+ * know find matched inside them rather than higher up: the beforematch event says nothing about
69
+ * where the match was, so the element the browser chose to reveal is the only clue.
70
+ */
71
+ export const holdsRevealableContent = node => {
72
+ const cached = revealableCache.get(node);
73
+ if (cached !== undefined) {
74
+ return cached;
75
+ }
76
+ let found = isRevealable(node);
77
+ if (!found) {
78
+ node.descendants(descendant => {
79
+ if (found) {
80
+ return false;
81
+ }
82
+ found = isRevealable(descendant);
83
+ return !found;
84
+ });
85
+ }
86
+ revealableCache.set(node, found);
87
+ return found;
53
88
  };
54
89
  const hasInlineComment = node => findChildrenByMark(node, node.type.schema.marks.annotation, true).some(annotation => annotation.node.marks.some(mark => mark.attrs.annotationType === 'inlineComment'));
55
90
 
56
- /** `null` is a cached "no mirror for this node", as distinct from `undefined` for a cache miss. */
91
+ /** `null` is a cached "no search text for this node", as distinct from `undefined` for a miss. */
57
92
  const searchTextCache = new WeakMap();
58
93
 
59
94
  /**
60
- * Plain-text mirror of an expand's body, rendered in its place while collapsed so browser
61
- * find-in-page can still reach the content.
95
+ * The text shown in place of one block of a collapsed expand's body, so browser find can still
96
+ * reach the content without the block being rendered.
62
97
  *
63
- * Cached on the node. ProseMirror nodes are immutable and `render-document` memoises
64
- * `nodeFromJSON`, so the same node instance is reused across renders including across the
65
- * serializer rebuilds that invalidate the module-level `memoizeOne` caches.
98
+ * Cached on the node. ProseMirror nodes never change, and the serializer reuses the same instances
99
+ * across renders, so the text only has to be worked out once per block.
66
100
  *
67
- * @returns the mirror text, or `undefined` if this expand must keep its body rendered.
101
+ * @returns the text, or `undefined` if this block has to be rendered instead.
68
102
  */
69
- export const getExpandSearchText = node => {
103
+ export const getBlockSearchText = node => {
70
104
  var _cached;
71
105
  let cached = searchTextCache.get(node);
72
106
  if (cached === undefined) {
73
107
  cached = hasInlineComment(node) ?
74
- // Withhold the mirror, which makes Expand keep rendering `children`. Comment
75
- // navigation walks the DOM to find the annotated node and scrolls it into view, so
76
- // the annotation has to exist as a real element a flat text mirror carries no
77
- // annotation marks and cannot stand in for it. Without this, stepping through
78
- // comments with the arrow keys silently fails to reach any comment inside a
79
- // collapsed expand. Costs the lazy-load win for these expands, deliberately.
80
- null : node.textBetween(0, node.content.size, BLOCK_SEPARATOR, leafText);
108
+ // No text, which makes the block render instead. Comment navigation looks through the DOM
109
+ // for the commented text and scrolls to it, so that text has to be a real element —
110
+ // plain text carries no comment marks and cannot stand in for it. Without this, the
111
+ // arrow keys silently skip every comment inside a collapsed expand. We give up the
112
+ // saving for this block on purpose.
113
+ null :
114
+ // A leaf block is not inside its own range, so `textBetween` would return nothing and its
115
+ // text would be lost — a macro sitting straight in an expand body, for instance.
116
+ node.isLeaf ? leafText(node) : node.textBetween(0, node.content.size, BLOCK_SEPARATOR, leafText);
81
117
  searchTextCache.set(node, cached);
82
118
  }
83
119
  return (_cached = cached) !== null && _cached !== void 0 ? _cached : undefined;
@@ -11,6 +11,7 @@ import React from 'react';
11
11
  import { MarkType } from '@atlaskit/editor-prosemirror/model';
12
12
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
13
13
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
14
+ import { mergeExpandBodyText, withExpandBodyBlock } from '../ui/expand-body';
14
15
  import { Doc, DocWithSelectAllTrap, isTextNode, isTextWrapper, mergeTextNodes, toReact } from './nodes';
15
16
  import TextWrapperComponent from './nodes/text-wrapper';
16
17
  import { isNestedHeaderLinksEnabled } from './utils/links';
@@ -92,12 +93,13 @@ var ReactSerializer = /*#__PURE__*/function () {
92
93
  var shouldSkipLinkMark = function shouldSkipLinkMark(mark) {
93
94
  return _this.allowMediaLinking !== true && isMedia && mark.type.name === 'link';
94
95
  };
95
- return marks.reduceRight(function (content, mark) {
96
+ var serialized = marks.reduceRight(function (content, mark) {
96
97
  if (shouldSkipLinkMark(mark) || shouldSkipBorderMark(mark)) {
97
98
  return content;
98
99
  }
99
100
  return _this.renderMark(markToReact(mark), _this.withMediaMarkProps(node, mark, _this.getMarkProps(mark, [], node)), "".concat(mark.type.name, "-").concat(index), content);
100
101
  }, serializedContent);
102
+ return withExpandBodyBlock(node, currentPath, index, serialized);
101
103
  });
102
104
  // Ignored via go/ees005
103
105
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -276,7 +278,7 @@ var ReactSerializer = /*#__PURE__*/function () {
276
278
  case 'expand':
277
279
  return this.getExpandProps(node, path);
278
280
  case 'nestedExpand':
279
- if (fg('hot-121622_lazy_load_expand_content')) {
281
+ if (isExperimentEnabled('platform_editor_defer_collapsed_expand_body')) {
280
282
  return this.getExpandProps(node, path);
281
283
  }
282
284
  return this.getProps(node, path);
@@ -308,7 +310,7 @@ var ReactSerializer = /*#__PURE__*/function () {
308
310
  if (key === 'root-0') {
309
311
  this.resetState();
310
312
  }
311
- return this.renderNode(target, props, key, this.getChildNodes(fragment).map(function (node, index) {
313
+ return this.renderNode(target, props, key, mergeExpandBodyText(this.getChildNodes(fragment).map(function (node, index) {
312
314
  if (isTextWrapper(node)) {
313
315
  return _this2.serializeTextWrapper(node.content, {
314
316
  index: index,
@@ -319,7 +321,7 @@ var ReactSerializer = /*#__PURE__*/function () {
319
321
  index: index,
320
322
  parentInfo: parentInfo
321
323
  });
322
- }));
324
+ })));
323
325
  }
324
326
  }, {
325
327
  key: "serializeTextWrapper",
@@ -729,11 +731,8 @@ var ReactSerializer = /*#__PURE__*/function () {
729
731
  value: function getExpandProps(node) {
730
732
  var _this7 = this;
731
733
  var _path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
732
- // Expand receives the node rather than a precomputed string so it can derive the collapsed
733
- // text mirror only when it actually renders one a nested expand is not mounted until its
734
- // ancestor opens — and so that derivation can reach content the model walk cannot, such as
735
- // the ADF stashed in a coerced extension's `parameters.nestedContent`. Expand owns the
736
- // feature gate check.
734
+ // Expand is given the node so it can tell whether it opted into lazy body loading at all. The
735
+ // text each block shows is attached to the block itself, by `withExpandBodyBlock`.
737
736
  if (!isNestedHeaderLinksEnabled(this.allowHeadingAnchorLinks)) {
738
737
  return _objectSpread(_objectSpread({}, this.getProps(node)), {}, {
739
738
  node: node