@mui/internal-docs-infra 0.11.1-canary.16 → 0.11.1-canary.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-docs-infra",
3
- "version": "0.11.1-canary.16",
3
+ "version": "0.11.1-canary.17",
4
4
  "author": "MUI Team",
5
5
  "description": "MUI Infra - internal documentation creation tools.",
6
6
  "license": "MIT",
@@ -768,5 +768,5 @@
768
768
  "bin": {
769
769
  "docs-infra": "./cli/index.mjs"
770
770
  },
771
- "gitSha": "4c7e314cf1f7d63fe80e86d6e3c84fc7d15a053e"
771
+ "gitSha": "5f0871264ec4d7c78d5fd7e62888de0f7bf61cda"
772
772
  }
@@ -1146,12 +1146,19 @@ export const createEditableEngine = ctx => {
1146
1146
  state.repeatFlushId = null;
1147
1147
  // The user may have moved focus or cleared the selection in the
1148
1148
  // 100ms since the last repeat keydown (e.g. clicked elsewhere,
1149
- // unmounted, blurred). The debounced flush is best-effort; if
1150
- // there's no live selection inside the editable any more, skip
1151
- // — the next real event will pick up state. Without this guard
1152
- // `getPosition` throws from a stray timer after teardown.
1149
+ // unmounted, blurred). The debounced flush is best-effort; if the
1150
+ // engine is gone or there's no live selection inside the editable
1151
+ // any more, skip — the next real event will pick up state.
1152
+ //
1153
+ // Bail out before touching `window`: a stray timer can fire after
1154
+ // teardown, and in a test environment the `window` global may already
1155
+ // be removed, so `window.getSelection()` would throw a `ReferenceError`
1156
+ // (an unhandled rejection that can mask real failures).
1157
+ if (state.disconnected || typeof window === 'undefined') {
1158
+ return;
1159
+ }
1153
1160
  const selection = window.getSelection();
1154
- if (state.disconnected || !selection || selection.rangeCount === 0 || !element.contains(selection.getRangeAt(0).startContainer)) {
1161
+ if (!selection || selection.rangeCount === 0 || !element.contains(selection.getRangeAt(0).startContainer)) {
1155
1162
  return;
1156
1163
  }
1157
1164
  flushChanges();
@@ -48,6 +48,14 @@ export type UseCodeWindowResult<ToggleElement extends HTMLElement = HTMLElement,
48
48
  * so the anchor stays put against the panel's own scroll rather than the
49
49
  * page. When left unattached, the page is compensated — the right default
50
50
  * for code that grows the document flow. Forwarded from `useScrollAnchor`.
51
+ *
52
+ * When attached, this element is also treated as the horizontal scroll
53
+ * owner: the scrollbar-gutter swap (`data-scrollbar-gutter`) and the
54
+ * collapse scroll-back run on it instead of the inner `<pre>`. Use this when
55
+ * the window owns both scroll axes so the horizontal scrollbar sits at the
56
+ * window's edge (in view) rather than at the bottom of the inner `<pre>`,
57
+ * which can extend past the window's height and scroll out of view. Your
58
+ * gutter CSS must then key off this element's attribute.
51
59
  */
52
60
  scrollContainerRef: React.RefObject<ScrollElement | null>;
53
61
  /**
@@ -53,33 +53,38 @@ function cancelScheduled(handle) {
53
53
  * Smoothly slides the `<code>` element back to the left edge over `duration`
54
54
  * ms using an ease-out cubic via the Web Animations API.
55
55
  *
56
- * Used during collapse instead of tweening `pre.scrollLeft` because the
57
- * scrollbar-gutter animation forces `overflow-x: hidden` on the pre, which
56
+ * `scrollEl` is whichever element owns the horizontal scroll the inner
57
+ * `<pre>` by default, or an attached scroll container (see `scrollContainerRef`)
58
+ * when the code block is rendered inside a fixed-size window. `code` is this
59
+ * code window's own `<code>` (scoped to its container by the caller) so a shared
60
+ * scroll container holding several blocks animates the right one.
61
+ *
62
+ * Used during collapse instead of tweening `scrollEl.scrollLeft` because the
63
+ * scrollbar-gutter animation forces `overflow-x: hidden` on `scrollEl`, which
58
64
  * snaps `scrollLeft` to 0 instantly. Animating a transform on the inner
59
65
  * `code` element produces the same visual effect, isn't reset by the overflow
60
- * change, and is naturally clipped by the pre's hidden overflow.
66
+ * change, and is naturally clipped by the scroll element's hidden overflow.
61
67
  *
62
68
  * Honors `prefers-reduced-motion` by snapping immediately.
63
69
  */
64
- function smoothCollapseScrollLeft(pre, duration) {
65
- const startLeft = pre.scrollLeft;
70
+ function smoothCollapseScrollLeft(scrollEl, code, duration) {
71
+ const startLeft = scrollEl.scrollLeft;
66
72
  if (startLeft <= 0) {
67
73
  return null;
68
74
  }
69
- const code = pre.querySelector('code');
70
- if (!code || typeof code.animate !== 'function') {
71
- return null;
72
- }
73
75
 
74
76
  // Cancel any leftover scroll-back animation from a previous toggle so we
75
77
  // don't end up with two transforms competing on the same element.
76
- scrollbackAnimations.get(pre)?.cancel();
77
- scrollbackAnimations.delete(pre);
78
+ scrollbackAnimations.get(scrollEl)?.cancel();
79
+ scrollbackAnimations.delete(scrollEl);
78
80
 
79
- // Reset the actual scroll position now; the WAAPI animation visually
80
- // compensates by translating the element from `-startLeft` back to `0`.
81
- pre.scrollLeft = 0;
82
- if (prefersReducedMotion() || duration <= 0) {
81
+ // Snap the actual scroll position back to the left edge now. When we can
82
+ // animate, the WAAPI transform below visually compensates by translating the
83
+ // element from `-startLeft` back to `0`; otherwise (no WAAPI, no `code`,
84
+ // reduced motion, or zero duration) this stands as an instant snap — still
85
+ // the correct collapsed end state.
86
+ scrollEl.scrollLeft = 0;
87
+ if (!code || typeof code.animate !== 'function' || prefersReducedMotion() || duration <= 0) {
83
88
  return null;
84
89
  }
85
90
  const anim = code.animate([{
@@ -91,10 +96,10 @@ function smoothCollapseScrollLeft(pre, duration) {
91
96
  easing: 'cubic-bezier(0, 0, 0.2, 1)',
92
97
  fill: 'none'
93
98
  });
94
- scrollbackAnimations.set(pre, anim);
99
+ scrollbackAnimations.set(scrollEl, anim);
95
100
  const onSettle = () => {
96
- if (scrollbackAnimations.get(pre) === anim) {
97
- scrollbackAnimations.delete(pre);
101
+ if (scrollbackAnimations.get(scrollEl) === anim) {
102
+ scrollbackAnimations.delete(scrollEl);
98
103
  }
99
104
  };
100
105
  anim.finished.then(onSettle, onSettle);
@@ -106,74 +111,76 @@ function isElementInViewport(element) {
106
111
  }
107
112
 
108
113
  /**
109
- * Measures the horizontal scrollbar height of a `<pre>` element by
114
+ * Measures the horizontal scrollbar height of the scroll element by
110
115
  * temporarily forcing `overflow-x: scroll`.
111
116
  */
112
- function measureScrollbarHeight(pre) {
113
- const prevOverflow = pre.style.overflowX;
114
- pre.style.overflowX = 'scroll';
115
- const scrollbarHeight = pre.offsetHeight - pre.clientHeight;
116
- pre.style.overflowX = prevOverflow;
117
+ function measureScrollbarHeight(scrollEl) {
118
+ const prevOverflow = scrollEl.style.overflowX;
119
+ scrollEl.style.overflowX = 'scroll';
120
+ const scrollbarHeight = scrollEl.offsetHeight - scrollEl.clientHeight;
121
+ scrollEl.style.overflowX = prevOverflow;
117
122
  return scrollbarHeight;
118
123
  }
119
- function clearGutterState(pre) {
120
- cancelScheduled(gutterCleanupTimers.get(pre));
121
- gutterCleanupTimers.delete(pre);
122
- const flipTimer = gutterFlipTimers.get(pre);
124
+ function clearGutterState(scrollEl) {
125
+ cancelScheduled(gutterCleanupTimers.get(scrollEl));
126
+ gutterCleanupTimers.delete(scrollEl);
127
+ const flipTimer = gutterFlipTimers.get(scrollEl);
123
128
  if (flipTimer !== undefined) {
124
129
  clearTimeout(flipTimer);
125
- gutterFlipTimers.delete(pre);
130
+ gutterFlipTimers.delete(scrollEl);
126
131
  }
127
- pre.removeAttribute(GUTTER_STATE_ATTRIBUTE);
132
+ scrollEl.removeAttribute(GUTTER_STATE_ATTRIBUTE);
128
133
  }
129
- function cancelAllForPre(pre) {
130
- scrollbackAnimations.get(pre)?.cancel();
131
- scrollbackAnimations.delete(pre);
132
- clearGutterState(pre);
134
+ function cancelAllForScrollEl(scrollEl) {
135
+ scrollbackAnimations.get(scrollEl)?.cancel();
136
+ scrollbackAnimations.delete(scrollEl);
137
+ clearGutterState(scrollEl);
133
138
  }
134
139
 
135
140
  /**
136
141
  * Drives a from→to transition on the `data-scrollbar-gutter` attribute of
137
- * `pre`, which the consumer's CSS hooks into to animate the swap between
138
- * a real scrollbar and equivalent padding-bottom.
142
+ * the scroll element, which the consumer's CSS hooks into to animate the swap
143
+ * between a real scrollbar and equivalent padding-bottom.
144
+ *
145
+ * `scrollEl` is whichever element owns the horizontal scroll — the inner
146
+ * `<pre>` by default, or the attached `scrollContainerRef` when the code block
147
+ * is rendered inside a fixed-size window. `code` is this code window's own
148
+ * `<code>` (scoped to its container by the caller).
139
149
  *
140
150
  * Skips the animation when content doesn't overflow (no scrollbar exists)
141
151
  * or when the browser uses overlay scrollbars (zero height).
142
152
  */
143
- function animateScrollbarGutter(pre, from, to, durationMs) {
144
- const scrollbarHeight = measureScrollbarHeight(pre);
153
+ function animateScrollbarGutter(scrollEl, code, from, to, durationMs) {
154
+ const scrollbarHeight = measureScrollbarHeight(scrollEl);
145
155
  if (scrollbarHeight === 0) {
146
156
  return; // Overlay scrollbars, nothing to do
147
157
  }
148
158
 
149
- // For expand, check the inner code's scrollWidth (since `min-width:
150
- // fit-content` reflects hidden frames). For collapse, the pre's own
151
- // scrollWidth is enough.
152
- if (from === 'expand-from') {
153
- const code = pre.querySelector('code');
154
- if (code && code.scrollWidth <= pre.clientWidth) {
155
- return;
156
- }
157
- } else if (pre.scrollWidth <= pre.clientWidth) {
159
+ // Decide from this code window's own `<code>`, not from `scrollEl` — the
160
+ // scroll owner may be a shared container wrapping other content. `code`'s
161
+ // `scrollWidth` reflects hidden frames (via `min-width: fit-content`), so it
162
+ // predicts the post-expand width and still reflects the wide source during
163
+ // collapse; compare it against the scroll owner's visible width.
164
+ if (!code || code.scrollWidth <= scrollEl.clientWidth) {
158
165
  return;
159
166
  }
160
- clearGutterState(pre);
161
- pre.setAttribute(GUTTER_STATE_ATTRIBUTE, from);
167
+ clearGutterState(scrollEl);
168
+ scrollEl.setAttribute(GUTTER_STATE_ATTRIBUTE, from);
162
169
 
163
170
  // Move into the transition state on the next macrotask. Tracked so the
164
171
  // flip can be cancelled if the component unmounts before it fires.
165
172
  const flipTimer = setTimeout(() => {
166
- gutterFlipTimers.delete(pre);
167
- pre.setAttribute(GUTTER_STATE_ATTRIBUTE, to);
173
+ gutterFlipTimers.delete(scrollEl);
174
+ scrollEl.setAttribute(GUTTER_STATE_ATTRIBUTE, to);
168
175
  }, 0);
169
- gutterFlipTimers.set(pre, flipTimer);
176
+ gutterFlipTimers.set(scrollEl, flipTimer);
170
177
 
171
178
  // Schedule cleanup on the animation timeline so DevTools throttling
172
179
  // scales it together with the CSS transition.
173
- const cleanup = scheduleOnAnimationTimeline(pre, durationMs + 30, () => {
174
- clearGutterState(pre);
180
+ const cleanup = scheduleOnAnimationTimeline(scrollEl, durationMs + 30, () => {
181
+ clearGutterState(scrollEl);
175
182
  });
176
- gutterCleanupTimers.set(pre, cleanup);
183
+ gutterCleanupTimers.set(scrollEl, cleanup);
177
184
  }
178
185
  const DEFAULT_ANCHOR_SELECTOR = '[data-frame-type="highlighted"], [data-frame-type="focus"]';
179
186
  const DEFAULT_COLLAPSIBLE_SELECTOR = '[data-collapsible]';
@@ -218,7 +225,7 @@ export function useCodeWindow(options = {}) {
218
225
  collapsibleProbeSelector = DEFAULT_COLLAPSIBLE_SELECTOR
219
226
  } = options;
220
227
  const toggleRef = React.useRef(null);
221
- const lastPreRef = React.useRef(null);
228
+ const lastScrollElRef = React.useRef(null);
222
229
  const {
223
230
  containerRef,
224
231
  scrollContainerRef,
@@ -226,10 +233,10 @@ export function useCodeWindow(options = {}) {
226
233
  } = useScrollAnchor();
227
234
  React.useEffect(() => {
228
235
  return () => {
229
- const pre = lastPreRef.current;
230
- if (pre) {
231
- cancelAllForPre(pre);
232
- lastPreRef.current = null;
236
+ const scrollEl = lastScrollElRef.current;
237
+ if (scrollEl) {
238
+ cancelAllForScrollEl(scrollEl);
239
+ lastScrollElRef.current = null;
233
240
  }
234
241
  };
235
242
  }, []);
@@ -247,32 +254,42 @@ export function useCodeWindow(options = {}) {
247
254
  if (!anchor) {
248
255
  return;
249
256
  }
250
- const pre = container.querySelector('pre');
251
- if (pre) {
252
- lastPreRef.current = pre;
257
+
258
+ // The element whose horizontal scrollbar we smooth: the attached scroll
259
+ // container when one is provided (the code block lives inside a
260
+ // fixed-size window that owns both scroll axes), otherwise the inner
261
+ // `<pre>`, which scrolls horizontally on its own.
262
+ const scrollEl = scrollContainerRef.current ?? container.querySelector('pre');
263
+ // Scope content lookups to *this* code window's `container`, never to
264
+ // `scrollEl`: an attached scroll container may wrap several code blocks or
265
+ // unrelated content, so `scrollEl.querySelector('code')` could match the
266
+ // wrong block. The overflow decision and scroll-back both use this code.
267
+ const code = container.querySelector('code');
268
+ if (scrollEl) {
269
+ lastScrollElRef.current = scrollEl;
253
270
  if (direction === 'collapse') {
254
271
  // Smoothly return horizontal scroll to the left edge. We animate
255
272
  // via a transform on the inner `code` element rather than
256
- // tweening `pre.scrollLeft`, because the gutter animation below
273
+ // tweening `scrollEl.scrollLeft`, because the gutter animation below
257
274
  // sets `overflow-x: hidden` which would snap `scrollLeft` to 0
258
275
  // instantly. Both animations start in the same frame: the
259
276
  // scroll-back resets `scrollLeft` to 0 up front, so the gutter
260
277
  // swap's `overflow-x` change has nothing left to snap.
261
- smoothCollapseScrollLeft(pre, scrollBackDuration);
262
- animateScrollbarGutter(pre, 'collapse-from', 'collapse-to', collapseDuration);
278
+ smoothCollapseScrollLeft(scrollEl, code, scrollBackDuration);
279
+ animateScrollbarGutter(scrollEl, code, 'collapse-from', 'collapse-to', collapseDuration);
263
280
  }
264
281
  if (direction === 'expand') {
265
282
  // Cancel any in-flight collapse scroll-back so its leftover
266
283
  // transform can't drift the code horizontally during expand.
267
- scrollbackAnimations.get(pre)?.cancel();
268
- scrollbackAnimations.delete(pre);
269
- if (collapsibleProbeSelector && pre.querySelector(collapsibleProbeSelector)) {
270
- animateScrollbarGutter(pre, 'expand-from', 'expand-to', expandDuration);
284
+ scrollbackAnimations.get(scrollEl)?.cancel();
285
+ scrollbackAnimations.delete(scrollEl);
286
+ if (collapsibleProbeSelector && container.querySelector(collapsibleProbeSelector)) {
287
+ animateScrollbarGutter(scrollEl, code, 'expand-from', 'expand-to', expandDuration);
271
288
  }
272
289
  }
273
290
  }
274
291
  rawAnchorScroll(anchor, direction === 'collapse' ? collapseDuration : expandDuration);
275
- }, [containerRef, rawAnchorScroll, anchorSelector, collapsibleProbeSelector, collapseDuration, expandDuration, scrollBackDuration]);
292
+ }, [containerRef, scrollContainerRef, rawAnchorScroll, anchorSelector, collapsibleProbeSelector, collapseDuration, expandDuration, scrollBackDuration]);
276
293
  return {
277
294
  containerRef,
278
295
  scrollContainerRef,
@@ -42,10 +42,16 @@ export function useScrollAnchor() {
42
42
  activeSessionCleanupRef.current = null;
43
43
 
44
44
  // Snapshot the scroll target at session start so a later ref change
45
- // doesn't redirect compensation mid-flight.
46
- const scrollTarget = scrollContainerRef.current ?? window;
45
+ // doesn't redirect compensation mid-flight. `scrollElement` is the attached
46
+ // container (if any); `scrollTarget` is what receives the user-interaction
47
+ // listeners (the container or the window).
48
+ const scrollElement = scrollContainerRef.current;
49
+ const scrollTarget = scrollElement ?? window;
47
50
  const interactionTarget = scrollTarget;
48
- const initialTop = anchor.getBoundingClientRect().top;
51
+
52
+ // Mutable so it can be re-baselined when an attached container can't yet
53
+ // absorb a delta (see below).
54
+ let initialTop = anchor.getBoundingClientRect().top;
49
55
  let active = true;
50
56
  let cleanupTimer;
51
57
 
@@ -58,8 +64,25 @@ export function useScrollAnchor() {
58
64
  return;
59
65
  }
60
66
  const delta = anchor.getBoundingClientRect().top - initialTop;
61
- if (Math.abs(delta) > 0.5) {
62
- scrollTarget.scrollBy(0, delta);
67
+ if (Math.abs(delta) <= 0.5) {
68
+ return;
69
+ }
70
+ if (!scrollElement) {
71
+ window.scrollBy(0, delta);
72
+ return;
73
+ }
74
+ const before = scrollElement.scrollTop;
75
+ scrollElement.scrollBy(0, delta);
76
+ const remainder = delta - (scrollElement.scrollTop - before);
77
+ if (Math.abs(remainder) > 0.5) {
78
+ // The container couldn't absorb this part — it isn't scrollable yet
79
+ // (its content hasn't exceeded its `max-height`). Re-baseline instead
80
+ // of forcing the difference elsewhere: scrolling the page would shift
81
+ // the surrounding layout, and carrying the delta forward would snap the
82
+ // anchor back the instant the container becomes scrollable. Accepting
83
+ // the small drift now keeps the surrounding layout still and lets the
84
+ // container hold the anchor smoothly from here on.
85
+ initialTop += remainder;
63
86
  }
64
87
  });
65
88
  function cleanup() {