@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.
@@ -14,8 +14,7 @@ import { akEditorLineHeight, akEditorSwoopCubicBezier, akLayoutGutterOffset } fr
14
14
  import ChevronRightIcon from '@atlaskit/icon/core/chevron-right';
15
15
  import Tooltip from '@atlaskit/tooltip/Tooltip';
16
16
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
17
- import { fg } from '@atlaskit/platform-feature-flags/fg';
18
- import { getExpandSearchText } from './utils/expand-search-text';
17
+ import { ExpandBodyProvider, useExpandBody } from './expand-body';
19
18
  import _uniqueId from 'lodash/uniqueId';
20
19
  import { injectIntl } from 'react-intl';
21
20
  import { MODE, PLATFORM } from '../analytics/events';
@@ -140,12 +139,18 @@ var contentContainerStylesNotExpanded = css({
140
139
  // hidden="until-found" attribute on the outer container to hide content.
141
140
  // We remove height:0/overflow:hidden/clip from the inner wrapper so the
142
141
  // browser can actually search through the content.
142
+ //
143
+ // `user-select: none` must NOT be set here, unlike the variant above. WebKit scrolls to a
144
+ // find-in-page match by selecting it, so unselectable text is matched but never revealed: the
145
+ // reader is told there is a hit, the `hidden` attribute stays put, and the expand never opens.
146
+ // Chrome does not need a selection, so it hid the problem. Nothing is lost by leaving it out —
147
+ // content-visibility:hidden already makes a skipped subtree unselectable. It is only needed in the
148
+ // other variant, where the content is merely clipped and would otherwise be selectable.
143
149
  var contentContainerStylesNotExpandedBrowserFind = css({
144
150
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors
145
151
  '.expand-content-wrapper, .nestedExpand-content-wrapper': {
146
152
  width: '100%',
147
- display: 'block',
148
- userSelect: 'none'
153
+ display: 'block'
149
154
  }
150
155
  });
151
156
  var clearNextSiblingMarginTopStyle = css({
@@ -213,6 +218,7 @@ function Expand(_ref) {
213
218
  nestedHeaderIds = _ref.nestedHeaderIds,
214
219
  rendererContentMode = _ref.rendererContentMode,
215
220
  node = _ref.node;
221
+ var ancestorBody = useExpandBody();
216
222
  var _useState = useState(false),
217
223
  _useState2 = _slicedToArray(_useState, 2),
218
224
  expanded = _useState2[0],
@@ -222,12 +228,11 @@ function Expand(_ref) {
222
228
  focused = _useState4[0],
223
229
  setFocused = _useState4[1];
224
230
  /**
225
- * PGXT-9021: latches on the first open so the body stays mounted once rendered, including after
226
- * the expand is collapsed again. Remounting it would re-run every macro and Forge data fetch
227
- * inside on each reopen, which is both slower and visibly reloads content the reader has already
228
- * seen. The saving this gate exists for is at initial load, where the body has never been opened,
229
- * so nothing is given up by keeping it after that. Never set on the server, so the server and the
230
- * client's first render agree and hydration cannot desync.
231
+ * PGXT-9021: once opened, the body stays rendered even if the reader closes the expand again.
232
+ * Throwing it away would re-run every macro and Forge fetch inside it on the next open, which is
233
+ * slower and visibly reloads content the reader has already seen. We only wanted to save work on
234
+ * the first page load, and by now that has happened. Starts false on both the server and the
235
+ * client, so hydration cannot disagree.
231
236
  */
232
237
  var _useState5 = useState(false),
233
238
  _useState6 = _slicedToArray(_useState5, 2),
@@ -251,15 +256,21 @@ function Expand(_ref) {
251
256
  setExpanded(true);
252
257
  setHasBeenExpanded(true);
253
258
  }, []);
259
+ // The browser found the text inside this expand. Opening this one is not enough: if it sits
260
+ // inside other expands, those have to open too or the reader still cannot see it. Each expand
261
+ // asks the one above it, so a single match opens the whole chain and nothing else.
262
+ var openWithAncestors = useCallback(function () {
263
+ openBody();
264
+ ancestorBody === null || ancestorBody === void 0 || ancestorBody.openWithAncestors();
265
+ }, [ancestorBody, openBody]);
254
266
 
255
- // Feature-detect hidden="until-found" support via the beforematch event.
256
- // Chrome 102+ and Firefox 130+ support it; Safari does not yet.
257
- // In unsupported browsers, setting hidden="until-found" is treated as boolean hidden
258
- // (display:none), which would break the expand entirely.
259
- // Initialised as false and set in useEffect to avoid SSR/client hydration mismatch
260
- // useMemo would return true on the client's first render in supported browsers,
261
- // differing from the server snapshot which always produces false.
262
- var _useState7 = useState(false),
267
+ // Feature-detect hidden="until-found" support via the beforematch event. Chrome 102+,
268
+ // Firefox 139+ and Safari 26.2+ all have it; in a browser without it, hidden="until-found" is
269
+ // treated as plain boolean hidden (display:none), which would break the expand entirely.
270
+ // Starts as `undefined`, meaning "we do not know yet", and is only answered in an effect. We
271
+ // cannot check during render: the server has no browser to check, so it would say no there and
272
+ // yes on the client, and hydration would disagree.
273
+ var _useState7 = useState(undefined),
263
274
  _useState8 = _slicedToArray(_useState7, 2),
264
275
  supportsHiddenUntilFound = _useState8[0],
265
276
  setSupportsHiddenUntilFound = _useState8[1];
@@ -276,9 +287,9 @@ function Expand(_ref) {
276
287
  // because visibility:hidden blocks browser find. On expanded, we restore visibility to visible.
277
288
  //
278
289
  // Only applied when the browser supports hidden="until-found" (detected via onbeforematch).
279
- // In unsupported browsers (Safari), we skip this entirely and fall back to the normal
280
- // CSS hiding (visibility:hidden + height:0), which doesn't support find-in-page but
281
- // still works correctly for expand/collapse.
290
+ // Without it we skip this entirely and fall back to the normal CSS hiding
291
+ // (visibility:hidden + height:0), which doesn't support find-in-page but still works
292
+ // correctly for expand/collapse.
282
293
  useEffect(function () {
283
294
  var contentContainer = contentContainerRef.current;
284
295
  var contentWrapper = contentWrapperRef.current;
@@ -287,6 +298,13 @@ function Expand(_ref) {
287
298
  }
288
299
  if (supportsHiddenUntilFound && isExperimentEnabled('platform_editor_close_expand_find') && !expanded) {
289
300
  contentWrapper.setAttribute('hidden', 'until-found');
301
+ // Products ship a CSS reset with `[hidden] { display: none }` — Confluence does. That is an
302
+ // author rule, so it beats the UA stylesheet's
303
+ // `[hidden="until-found"] { content-visibility: hidden }` and takes the content out of the
304
+ // page altogether, where find cannot reach it. Setting display next to the attribute keeps
305
+ // the two together, rather than relying on a class selector elsewhere out-weighing a reset
306
+ // we do not own.
307
+ contentWrapper.style.display = 'block';
290
308
  // Override the CSS visibility:hidden from contentContainerStyles — hidden="until-found"
291
309
  // now handles hiding via content-visibility:hidden, which allows browser find to index
292
310
  // the content. We use 'visible' (not '') because '' only clears the inline style but
@@ -295,6 +313,7 @@ function Expand(_ref) {
295
313
  contentWrapper.style.visibility = 'visible';
296
314
  } else {
297
315
  contentWrapper.removeAttribute('hidden');
316
+ contentWrapper.style.display = '';
298
317
  contentContainer === null || contentContainer === void 0 || contentContainer.style.removeProperty('visibility');
299
318
  contentWrapper.style.visibility = '';
300
319
  }
@@ -306,22 +325,28 @@ function Expand(_ref) {
306
325
  var contentWrapper = contentWrapperRef.current;
307
326
  var unbindWrapperBeforeMatch = contentWrapper && supportsHiddenUntilFound ? bind(contentWrapper, {
308
327
  type: 'beforematch',
309
- listener: openBody
328
+ listener: openWithAncestors
310
329
  }) : undefined;
311
330
  return function () {
312
331
  unbindWrapperBeforeMatch === null || unbindWrapperBeforeMatch === void 0 || unbindWrapperBeforeMatch();
313
332
  };
314
- }, [expanded, openBody, supportsHiddenUntilFound]);
333
+ }, [expanded, openWithAncestors, supportsHiddenUntilFound]);
315
334
 
316
- // Until the expand is first opened, stand in a plain-text mirror of the body so
317
- // hidden="until-found" keeps the content findable by browser find without mounting the real
318
- // subtree. The beforematch listener above expands before the browser scrolls to its match.
319
- // Computed lazily: a nested expand is not mounted until its ancestor opens, so its mirror is
320
- // never built on initial load.
321
- var searchText = useMemo(function () {
322
- return node && fg('hot-121622_lazy_load_expand_content') ? getExpandSearchText(node) : undefined;
323
- }, [node]);
324
- var expandContent = searchText && !expanded && !hasBeenExpanded ? jsx("span", null, searchText) : children;
335
+ // While this is false, the blocks of the body show their text instead of rendering. It is true
336
+ // when:
337
+ // - the expand is open, or has been opened before, so the reader wants to see the content;
338
+ // - no node was given, so this expand never opted into lazy loading;
339
+ // - the experiment is off;
340
+ // - the browser has no hidden="until-found", so the text would not be findable and standing it in
341
+ // would gain nothing. While we still do not know, we assume the browser has it — assuming the
342
+ // opposite would render the whole body on the first paint and lose the saving entirely.
343
+ var revealed = expanded || hasBeenExpanded || !node || supportsHiddenUntilFound === false || !isExperimentEnabled('platform_editor_defer_collapsed_expand_body');
344
+ var expandBody = useMemo(function () {
345
+ return {
346
+ revealed: revealed,
347
+ openWithAncestors: openWithAncestors
348
+ };
349
+ }, [revealed, openWithAncestors]);
325
350
  return jsx(Container, {
326
351
  "data-testid": "expand-container-".concat(nodeType, "-").concat(id),
327
352
  "data-node-type": nodeType,
@@ -386,7 +411,9 @@ function Expand(_ref) {
386
411
  ref: contentWrapperRef
387
412
  }, jsx(WidthProvider, null, jsx("div", {
388
413
  css: clearNextSiblingMarginTopStyle
389
- }), expandContent))));
414
+ }), jsx(ExpandBodyProvider, {
415
+ value: expandBody
416
+ }, children)))));
390
417
  }
391
418
 
392
419
  // eslint-disable-next-line @typescript-eslint/ban-types
@@ -65,7 +65,7 @@ export var DEGRADED_SEVERITY_THRESHOLD = 3000;
65
65
  var TABLE_INFO_TIMEOUT = 10000;
66
66
  var RENDER_EVENT_SAMPLE_RATE = 0.1;
67
67
  var packageName = "@atlaskit/renderer";
68
- var packageVersion = "137.1.5";
68
+ var packageVersion = "137.1.6";
69
69
  var setAsQueryContainerStyles = css({
70
70
  containerName: 'ak-renderer-wrapper',
71
71
  containerType: 'inline-size'
@@ -0,0 +1,114 @@
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
+ var ExpandBodyContext = /*#__PURE__*/createContext(null);
5
+ export var ExpandBodyProvider = ExpandBodyContext.Provider;
6
+ export var useExpandBody = function useExpandBody() {
7
+ return useContext(ExpandBodyContext);
8
+ };
9
+ /**
10
+ * One or more neighbouring blocks of an expand's body. Shows their text until the expand is
11
+ * opened, then renders them. Falls back to rendering if there is no text, or if there is no
12
+ * expand above it.
13
+ *
14
+ * The text is rendered as-is, with no element around it. Nothing reads it but browser find,
15
+ * which only needs the characters to be in the DOM.
16
+ */
17
+ export var ExpandBodyBlock = function ExpandBodyBlock(_ref) {
18
+ var children = _ref.children,
19
+ searchText = _ref.searchText;
20
+ var body = useExpandBody();
21
+ if (searchText === undefined || body === null || body.revealed) {
22
+ return /*#__PURE__*/React.createElement(React.Fragment, null, children);
23
+ }
24
+ return searchText;
25
+ };
26
+
27
+ /**
28
+ * Called for every node the serializer renders. If the node is a block of an expand's body,
29
+ * wraps it so it can show its text instead of rendering while that expand is collapsed.
30
+ * Everything else is returned untouched.
31
+ *
32
+ * A block holding an expand of its own is also returned untouched, so a table with a nested
33
+ * expand in it, or an extension holding stashed ADF, renders even while collapsed. That
34
+ * expand needs an element of its own, because the element browser find reveals is the only
35
+ * way we learn the match was inside it rather than higher up. We give up the saving on those
36
+ * blocks so the reader searches once instead of once per level.
37
+ *
38
+ * `ancestors` is the node's ancestor chain, nearest last.
39
+ */
40
+ export var withExpandBodyBlock = function withExpandBodyBlock(node, ancestors, index, serialized) {
41
+ var parent = ancestors[ancestors.length - 1];
42
+ if (!parent || !isExpandNode(parent)) {
43
+ return serialized;
44
+ }
45
+ if (!isExperimentEnabled('platform_editor_defer_collapsed_expand_body') || holdsRevealableContent(node)) {
46
+ return serialized;
47
+ }
48
+ var searchText = getBlockSearchText(node);
49
+ if (searchText === undefined) {
50
+ return serialized;
51
+ }
52
+ return /*#__PURE__*/React.createElement(ExpandBodyBlock, {
53
+ key: "expand-body-block-".concat(index),
54
+ searchText: searchText
55
+ }, serialized);
56
+ };
57
+
58
+ /** Whatever the serializer produced for one child: an element, a string, an array of either. */
59
+
60
+ var searchTextOf = function searchTextOf(child) {
61
+ return /*#__PURE__*/isValidElement(child) && child.type === ExpandBodyBlock ? child.props.searchText : undefined;
62
+ };
63
+
64
+ /**
65
+ * Joins neighbouring blocks that are showing text into one, so a run of them is a single string in
66
+ * the DOM rather than one per block. A body of twenty paragraphs becomes one text node instead of
67
+ * twenty.
68
+ *
69
+ * A block that is being rendered — a nested expand, say — ends the run, because the text either
70
+ * side of it has to stay either side of it.
71
+ */
72
+ export var mergeExpandBodyText = function mergeExpandBodyText(children) {
73
+ // Called for every fragment in the document, and only an expand's body has anything to merge, so
74
+ // leave the array alone unless two neighbours are actually showing text.
75
+ var hasRun = children.some(function (child, index) {
76
+ return index > 0 && searchTextOf(child) !== undefined && searchTextOf(children[index - 1]) !== undefined;
77
+ });
78
+ if (!hasRun) {
79
+ return children;
80
+ }
81
+ var merged = [];
82
+ var run = [];
83
+ var endRun = function endRun() {
84
+ if (run.length === 0) {
85
+ return;
86
+ }
87
+ if (run.length === 1) {
88
+ merged.push(run[0]);
89
+ } else {
90
+ var _key;
91
+ var text = run.map(searchTextOf).filter(Boolean).join(BLOCK_SEPARATOR);
92
+ // The blocks themselves, not the wrappers around them: nesting the wrappers would show
93
+ // each block's text again inside the joined run.
94
+ var blocks = run.map(function (child) {
95
+ return child.props.children;
96
+ });
97
+ merged.push( /*#__PURE__*/React.createElement(ExpandBodyBlock, {
98
+ key: (_key = run[0].key) !== null && _key !== void 0 ? _key : undefined,
99
+ searchText: text
100
+ }, blocks));
101
+ }
102
+ run = [];
103
+ };
104
+ children.forEach(function (child) {
105
+ if (searchTextOf(child) === undefined) {
106
+ endRun();
107
+ merged.push(child);
108
+ return;
109
+ }
110
+ run.push(child);
111
+ });
112
+ endRun();
113
+ return merged;
114
+ };
@@ -6,22 +6,29 @@ 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
- var BLOCK_SEPARATOR = ' ';
9
+ export var BLOCK_SEPARATOR = ' ';
10
10
  var asText = function asText(value) {
11
11
  return typeof value === 'string' ? value : '';
12
12
  };
13
+ var nestedContentOf = function nestedContentOf(attrs) {
14
+ var _attrs$parameters;
15
+ return attrs === null || attrs === void 0 || (_attrs$parameters = attrs.parameters) === null || _attrs$parameters === void 0 ? void 0 : _attrs$parameters.nestedContent;
16
+ };
17
+ export var isExpandNode = function isExpandNode(node) {
18
+ return node.type.name === 'expand' || node.type.name === 'nestedExpand';
19
+ };
13
20
 
14
21
  /**
15
- * Extracts text from raw ADF JSON.
22
+ * Reads the text out of raw ADF JSON.
16
23
  *
17
- * Needed because some extensions stash a whole ADF subtree in `attrs.parameters.nestedContent`
18
- * instead of in their node content Confluence does this when it coerces nesting the schema
19
- * cannot represent (an expand more than two collapsible layers deep, say). Such a node is a
20
- * ProseMirror leaf, so the model walk cannot see the subtree even though it renders on the page
21
- * via a nested renderer. Recurses, because nestedContent routinely holds further nested content.
24
+ * Some extensions keep a whole ADF subtree in `attrs.parameters.nestedContent` rather than in their
25
+ * node content. Confluence does this when the authored nesting is deeper than the schema allows —
26
+ * an expand more than two collapsible layers deep, for example. To ProseMirror the extension is a
27
+ * leaf, so walking the node tree never sees that subtree, even though a nested renderer puts it on
28
+ * the page. Recurses, because stashed content often stashes more content of its own.
22
29
  */
23
30
  var _adfEntityText = function adfEntityText(entity) {
24
- var _entity$attrs, _entity$content;
31
+ var _entity$content;
25
32
  if (!entity) {
26
33
  return '';
27
34
  }
@@ -29,13 +36,13 @@ var _adfEntityText = function adfEntityText(entity) {
29
36
  if (entity.type === 'text') {
30
37
  parts.push(asText(entity.text));
31
38
  }
32
- var nestedContent = (_entity$attrs = entity.attrs) === null || _entity$attrs === void 0 || (_entity$attrs = _entity$attrs.parameters) === null || _entity$attrs === void 0 ? void 0 : _entity$attrs.nestedContent;
39
+ var nestedContent = nestedContentOf(entity.attrs);
33
40
  if (nestedContent) {
34
41
  // Prefer the real subtree over `attrs.text`, which is only a placeholder label for it.
35
42
  parts.push(_adfEntityText(nestedContent));
36
43
  } else {
37
- var _entity$attrs2;
38
- parts.push(asText((_entity$attrs2 = entity.attrs) === null || _entity$attrs2 === void 0 ? void 0 : _entity$attrs2.text));
44
+ var _entity$attrs;
45
+ parts.push(asText((_entity$attrs = entity.attrs) === null || _entity$attrs === void 0 ? void 0 : _entity$attrs.text));
39
46
  }
40
47
  (_entity$content = entity.content) === null || _entity$content === void 0 || _entity$content.forEach(function (child) {
41
48
  return parts.push(_adfEntityText(child));
@@ -44,16 +51,48 @@ var _adfEntityText = function adfEntityText(entity) {
44
51
  };
45
52
 
46
53
  /**
47
- * `textBetween` drops every non-text leaf unless the node spec declares `leafText`, and no ADF
48
- * node does. Leaves carry their visible text in attrs, so recover it here.
54
+ * `textBetween` skips every leaf node unless its spec declares `leafText`, and no ADF node
55
+ * does so mentions, emoji, dates and statuses would all be lost. They keep their visible
56
+ * text in attrs, so read it from there.
49
57
  */
50
58
  var leafText = function leafText(leaf) {
51
- var _leaf$attrs, _leaf$attrs2;
52
- var nestedContent = (_leaf$attrs = leaf.attrs) === null || _leaf$attrs === void 0 || (_leaf$attrs = _leaf$attrs.parameters) === null || _leaf$attrs === void 0 ? void 0 : _leaf$attrs.nestedContent;
59
+ var _leaf$attrs;
60
+ var nestedContent = nestedContentOf(leaf.attrs);
53
61
  if (nestedContent) {
54
62
  return _adfEntityText(nestedContent);
55
63
  }
56
- return asText((_leaf$attrs2 = leaf.attrs) === null || _leaf$attrs2 === void 0 ? void 0 : _leaf$attrs2.text);
64
+ return asText((_leaf$attrs = leaf.attrs) === null || _leaf$attrs === void 0 ? void 0 : _leaf$attrs.text);
65
+ };
66
+ var isRevealable = function isRevealable(node) {
67
+ return isExpandNode(node) || Boolean(nestedContentOf(node.attrs));
68
+ };
69
+ var revealableCache = new WeakMap();
70
+
71
+ /**
72
+ * Whether this block holds something that can be opened on its own: an expand, or an extension
73
+ * holding stashed ADF that a nested renderer will mount expands from.
74
+ *
75
+ * Those need a real element even while the expand around them is collapsed. The element is how we
76
+ * know find matched inside them rather than higher up: the beforematch event says nothing about
77
+ * where the match was, so the element the browser chose to reveal is the only clue.
78
+ */
79
+ export var holdsRevealableContent = function holdsRevealableContent(node) {
80
+ var cached = revealableCache.get(node);
81
+ if (cached !== undefined) {
82
+ return cached;
83
+ }
84
+ var found = isRevealable(node);
85
+ if (!found) {
86
+ node.descendants(function (descendant) {
87
+ if (found) {
88
+ return false;
89
+ }
90
+ found = isRevealable(descendant);
91
+ return !found;
92
+ });
93
+ }
94
+ revealableCache.set(node, found);
95
+ return found;
57
96
  };
58
97
  var hasInlineComment = function hasInlineComment(node) {
59
98
  return findChildrenByMark(node, node.type.schema.marks.annotation, true).some(function (annotation) {
@@ -63,30 +102,31 @@ var hasInlineComment = function hasInlineComment(node) {
63
102
  });
64
103
  };
65
104
 
66
- /** `null` is a cached "no mirror for this node", as distinct from `undefined` for a cache miss. */
105
+ /** `null` is a cached "no search text for this node", as distinct from `undefined` for a miss. */
67
106
  var searchTextCache = new WeakMap();
68
107
 
69
108
  /**
70
- * Plain-text mirror of an expand's body, rendered in its place while collapsed so browser
71
- * find-in-page can still reach the content.
109
+ * The text shown in place of one block of a collapsed expand's body, so browser find can still
110
+ * reach the content without the block being rendered.
72
111
  *
73
- * Cached on the node. ProseMirror nodes are immutable and `render-document` memoises
74
- * `nodeFromJSON`, so the same node instance is reused across renders including across the
75
- * serializer rebuilds that invalidate the module-level `memoizeOne` caches.
112
+ * Cached on the node. ProseMirror nodes never change, and the serializer reuses the same instances
113
+ * across renders, so the text only has to be worked out once per block.
76
114
  *
77
- * @returns the mirror text, or `undefined` if this expand must keep its body rendered.
115
+ * @returns the text, or `undefined` if this block has to be rendered instead.
78
116
  */
79
- export var getExpandSearchText = function getExpandSearchText(node) {
117
+ export var getBlockSearchText = function getBlockSearchText(node) {
80
118
  var cached = searchTextCache.get(node);
81
119
  if (cached === undefined) {
82
120
  cached = hasInlineComment(node) ?
83
- // Withhold the mirror, which makes Expand keep rendering `children`. Comment
84
- // navigation walks the DOM to find the annotated node and scrolls it into view, so
85
- // the annotation has to exist as a real element a flat text mirror carries no
86
- // annotation marks and cannot stand in for it. Without this, stepping through
87
- // comments with the arrow keys silently fails to reach any comment inside a
88
- // collapsed expand. Costs the lazy-load win for these expands, deliberately.
89
- null : node.textBetween(0, node.content.size, BLOCK_SEPARATOR, leafText);
121
+ // No text, which makes the block render instead. Comment navigation looks through the DOM
122
+ // for the commented text and scrolls to it, so that text has to be a real element —
123
+ // plain text carries no comment marks and cannot stand in for it. Without this, the
124
+ // arrow keys silently skip every comment inside a collapsed expand. We give up the
125
+ // saving for this block on purpose.
126
+ null :
127
+ // A leaf block is not inside its own range, so `textBetween` would return nothing and its
128
+ // text would be lost — a macro sitting straight in an expand body, for instance.
129
+ node.isLeaf ? leafText(node) : node.textBetween(0, node.content.size, BLOCK_SEPARATOR, leafText);
90
130
  searchTextCache.set(node, cached);
91
131
  }
92
132
  return cached !== null && cached !== void 0 ? cached : undefined;
@@ -9,10 +9,11 @@ export interface ExpandProps {
9
9
  localId?: string;
10
10
  nestedHeaderIds?: Array<string>;
11
11
  /**
12
- * This expand's ProseMirror node. Supplying it opts the expand into lazy body loading: until the
13
- * expand is first opened, a plain-text mirror of the body is rendered in place of `children`.
14
- * Once opened, `children` stay mounted for the rest of the page's life, including after the
15
- * expand is collapsed again (PGXT-9021). Omit it to keep `children` in the DOM at all times.
12
+ * This expand's ProseMirror node. Passing it turns on lazy body loading: until the expand is
13
+ * first opened, the blocks the serializer wrapped show their text rather than rendering. After
14
+ * that they stay rendered, even if the reader closes the expand again (PGXT-9021). Leave it out
15
+ * to always render the body. Browsers without `hidden="until-found"` render it in full shortly
16
+ * after the page loads, since the text would not be findable there anyway.
16
17
  */
17
18
  node?: PMNode;
18
19
  nodeType: 'expand' | 'nestedExpand';
@@ -0,0 +1,64 @@
1
+ import type { Provider, ReactNode } from 'react';
2
+ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
3
+ type ExpandBody = {
4
+ /**
5
+ * Opens this expand, then asks the expand above it to do the same.
6
+ *
7
+ * Browser find reveals the element holding the text it matched, and that element belongs to the
8
+ * expand the text is really in — even a deeply nested one, because an expand only ever replaces
9
+ * its own blocks with text, never a nested expand. So the expands to open are just that expand's
10
+ * ancestors.
11
+ */
12
+ openWithAncestors: () => void;
13
+ /**
14
+ * True once this expand has been opened. While it is false, the blocks of the body show
15
+ * their text instead of rendering.
16
+ *
17
+ * Each block reads this and decides for itself. The expand does not swap its body out,
18
+ * because that would throw away and rebuild everything inside it — including a nested
19
+ * expand find had just opened, and any nested renderer that had already loaded.
20
+ */
21
+ revealed: boolean;
22
+ };
23
+ export declare const ExpandBodyProvider: Provider<ExpandBody | null>;
24
+ export declare const useExpandBody: () => ExpandBody | null;
25
+ type ExpandBodyBlockProps = {
26
+ children: ReactNode;
27
+ /** The block's text, or `undefined` if it must always be rendered. */
28
+ searchText?: string;
29
+ };
30
+ /**
31
+ * One or more neighbouring blocks of an expand's body. Shows their text until the expand is
32
+ * opened, then renders them. Falls back to rendering if there is no text, or if there is no
33
+ * expand above it.
34
+ *
35
+ * The text is rendered as-is, with no element around it. Nothing reads it but browser find,
36
+ * which only needs the characters to be in the DOM.
37
+ */
38
+ export declare const ExpandBodyBlock: ({ children, searchText }: ExpandBodyBlockProps) => ReactNode;
39
+ /**
40
+ * Called for every node the serializer renders. If the node is a block of an expand's body,
41
+ * wraps it so it can show its text instead of rendering while that expand is collapsed.
42
+ * Everything else is returned untouched.
43
+ *
44
+ * A block holding an expand of its own is also returned untouched, so a table with a nested
45
+ * expand in it, or an extension holding stashed ADF, renders even while collapsed. That
46
+ * expand needs an element of its own, because the element browser find reveals is the only
47
+ * way we learn the match was inside it rather than higher up. We give up the saving on those
48
+ * blocks so the reader searches once instead of once per level.
49
+ *
50
+ * `ancestors` is the node's ancestor chain, nearest last.
51
+ */
52
+ export declare const withExpandBodyBlock: (node: PMNode, ancestors: readonly PMNode[], index: number, serialized: JSX.Element | null) => JSX.Element | null;
53
+ /** Whatever the serializer produced for one child: an element, a string, an array of either. */
54
+ type SerializedChild = ReactNode;
55
+ /**
56
+ * Joins neighbouring blocks that are showing text into one, so a run of them is a single string in
57
+ * the DOM rather than one per block. A body of twenty paragraphs becomes one text node instead of
58
+ * twenty.
59
+ *
60
+ * A block that is being rendered — a nested expand, say — ends the run, because the text either
61
+ * side of it has to stay either side of it.
62
+ */
63
+ export declare const mergeExpandBodyText: (children: SerializedChild[]) => SerializedChild[];
64
+ export {};
@@ -1,12 +1,23 @@
1
1
  import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
2
+ /** Separates text from adjacent blocks so phrases cannot fuse across block boundaries. */
3
+ export declare const BLOCK_SEPARATOR = " ";
4
+ export declare const isExpandNode: (node: PMNode) => boolean;
2
5
  /**
3
- * Plain-text mirror of an expand's body, rendered in its place while collapsed so browser
4
- * find-in-page can still reach the content.
6
+ * Whether this block holds something that can be opened on its own: an expand, or an extension
7
+ * holding stashed ADF that a nested renderer will mount expands from.
5
8
  *
6
- * Cached on the node. ProseMirror nodes are immutable and `render-document` memoises
7
- * `nodeFromJSON`, so the same node instance is reused across renders including across the
8
- * serializer rebuilds that invalidate the module-level `memoizeOne` caches.
9
+ * Those need a real element even while the expand around them is collapsed. The element is how we
10
+ * know find matched inside them rather than higher up: the beforematch event says nothing about
11
+ * where the match was, so the element the browser chose to reveal is the only clue.
12
+ */
13
+ export declare const holdsRevealableContent: (node: PMNode) => boolean;
14
+ /**
15
+ * The text shown in place of one block of a collapsed expand's body, so browser find can still
16
+ * reach the content without the block being rendered.
17
+ *
18
+ * Cached on the node. ProseMirror nodes never change, and the serializer reuses the same instances
19
+ * across renders, so the text only has to be worked out once per block.
9
20
  *
10
- * @returns the mirror text, or `undefined` if this expand must keep its body rendered.
21
+ * @returns the text, or `undefined` if this block has to be rendered instead.
11
22
  */
12
- export declare const getExpandSearchText: (node: PMNode) => string | undefined;
23
+ export declare const getBlockSearchText: (node: PMNode) => string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/renderer",
3
- "version": "137.1.6",
3
+ "version": "137.1.7",
4
4
  "description": "Renderer component",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -58,8 +58,8 @@
58
58
  "@atlaskit/media-client-react": "^6.2.0",
59
59
  "@atlaskit/media-common": "^14.5.0",
60
60
  "@atlaskit/media-filmstrip": "^52.3.0",
61
- "@atlaskit/media-ui": "^30.13.0",
62
- "@atlaskit/media-viewer": "^54.6.0",
61
+ "@atlaskit/media-ui": "^30.14.0",
62
+ "@atlaskit/media-viewer": "^54.7.0",
63
63
  "@atlaskit/platform-feature-experiments": "^0.3.0",
64
64
  "@atlaskit/platform-feature-flags": "^2.1.0",
65
65
  "@atlaskit/platform-feature-flags-react": "^1.1.0",
@@ -70,7 +70,7 @@
70
70
  "@atlaskit/status": "^5.8.0",
71
71
  "@atlaskit/task-decision": "^21.8.0",
72
72
  "@atlaskit/theme": "^28.1.0",
73
- "@atlaskit/tmp-editor-statsig": "^163.0.0",
73
+ "@atlaskit/tmp-editor-statsig": "^164.0.0",
74
74
  "@atlaskit/tokens": "^16.8.0",
75
75
  "@atlaskit/tooltip": "^24.2.0",
76
76
  "@atlaskit/visually-hidden": "^4.3.0",
@@ -229,9 +229,6 @@
229
229
  "jfp-magma-ssr-iv-editor-links": {
230
230
  "type": "boolean"
231
231
  },
232
- "hot-121622_lazy_load_expand_content": {
233
- "type": "boolean"
234
- },
235
232
  "media-perf-uplift-mutation-fix": {
236
233
  "type": "boolean"
237
234
  },