@salesforcedevs/docs-components 1.21.1-redocly2 → 1.21.1-redocly4

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": "@salesforcedevs/docs-components",
3
- "version": "1.21.1-redocly2",
3
+ "version": "1.21.1-redocly4",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -35,6 +35,8 @@ export default class RedocReference extends LightningElement {
35
35
  private redocInitialized = false;
36
36
 
37
37
  private docHeaderElement: Element | null = null;
38
+ private docPhaseWrapperElement: Element | null = null;
39
+ private lastSidebarTop = 0;
38
40
 
39
41
  showError = false;
40
42
 
@@ -49,12 +51,11 @@ export default class RedocReference extends LightningElement {
49
51
  typeof value === "string" ? JSON.parse(value) : value;
50
52
  this._referenceConfig = refConfig;
51
53
  } catch (error) {
52
- this.showError = true;
53
- console.error(
54
+ this._referenceConfig = { refList: [] };
55
+ this.showErrorUI(
54
56
  "Failed to parse reference configuration data",
55
57
  error
56
58
  );
57
- this._referenceConfig = { refList: [] };
58
59
  }
59
60
  }
60
61
 
@@ -74,6 +75,7 @@ export default class RedocReference extends LightningElement {
74
75
 
75
76
  renderedCallback(): void {
76
77
  if (!this.redocInitialized) {
78
+ this.redocInitialized = true;
77
79
  this.initializeRedoc();
78
80
  }
79
81
  }
@@ -81,13 +83,42 @@ export default class RedocReference extends LightningElement {
81
83
  disconnectedCallback(): void {
82
84
  window.removeEventListener("scroll", this.handleScrollAndResize);
83
85
  window.removeEventListener("resize", this.handleScrollAndResize);
86
+
87
+ this.handleScrollAndResize?.cancel?.();
88
+
89
+ // Clean up cached DOM element references to prevent memory leaks
90
+ this.docHeaderElement = null;
91
+ this.docPhaseWrapperElement = null;
84
92
  }
85
93
 
86
- @api
87
- setDocPhaseInfo(docPhaseInfo: string): void {
88
- this._parentDocPhaseInfo = docPhaseInfo;
94
+ // Displays error UI and logs error message for debugging
95
+ private showErrorUI(message: string, error?: any): void {
96
+ this.showError = true;
97
+ console.error(message, error);
98
+ }
99
+
100
+ private getRedocContainer(): HTMLElement | null {
101
+ return document.querySelector(".redoc-container");
102
+ }
103
+
104
+ private getSelectedReference(): ReferenceItem | null {
105
+ return (
106
+ this._referenceConfig?.refList?.find((ref) => ref.isSelected) ||
107
+ this._referenceConfig?.refList?.[0]
108
+ );
109
+ }
110
+
111
+ private getDocPhaseInfo(): string | null {
112
+ if (this._parentDocPhaseInfo) {
113
+ return this._parentDocPhaseInfo;
114
+ }
115
+ const selectedRef = this.getSelectedReference();
116
+ return selectedRef?.docPhase
117
+ ? JSON.stringify(selectedRef.docPhase)
118
+ : null;
89
119
  }
90
120
 
121
+ // Extracts numeric value from CSS custom properties
91
122
  private getGlobalCSSVariableValue(variableName: string): number {
92
123
  const value = getComputedStyle(
93
124
  document.documentElement
@@ -95,11 +126,11 @@ export default class RedocReference extends LightningElement {
95
126
  return parseInt(value, 10) || 0;
96
127
  }
97
128
 
129
+ /*
130
+ ** Since we could not use --dx-g-global-header-height as getPropertyValue returns a calc expression,
131
+ ** we are using the respective CSS variables to calculate the height.
132
+ */
98
133
  private getGlobalHeaderHeight(): number {
99
- /*
100
- ** Since we could not use --dx-g-global-header-height as getPropertyValue returns a calc expression,
101
- ** we are using the respective CSS variables to calculate the height.
102
- */
103
134
  const rowHeight = this.getGlobalCSSVariableValue(
104
135
  "--dx-g-global-header-nav-row-height"
105
136
  );
@@ -109,59 +140,62 @@ export default class RedocReference extends LightningElement {
109
140
  return rowHeight * rowCount;
110
141
  }
111
142
 
143
+ // Gets doc header height with element caching for performance
112
144
  private getDocHeaderHeight(): number {
113
145
  if (!this.docHeaderElement) {
114
146
  this.docHeaderElement =
115
147
  document.querySelector(".sticky-doc-header");
116
148
  }
117
- return this.docHeaderElement?.getBoundingClientRect().height || 0;
149
+ return this.docHeaderElement?.getBoundingClientRect()?.height || 0;
118
150
  }
119
151
 
120
- private handleLayoutChange = () => {
152
+ // Gets doc phase wrapper height with element caching for performance
153
+ private getDocPhaseWrapperHeight(): number {
154
+ if (!this.docPhaseWrapperElement) {
155
+ this.docPhaseWrapperElement =
156
+ document.querySelector(".doc-phase-wrapper");
157
+ }
158
+ return (
159
+ this.docPhaseWrapperElement?.getBoundingClientRect()?.height || 0
160
+ );
161
+ }
162
+
163
+ calculateHeaderOffset = () => {
164
+ const globalHeaderHeight = this.getGlobalHeaderHeight();
165
+ const docHeaderHeight = this.getDocHeaderHeight();
166
+ return globalHeaderHeight + docHeaderHeight;
167
+ };
168
+
169
+ // Dynamic scroll offset calculation that Redoc will call
170
+ calculateScrollYOffset = () => {
171
+ const headerOffset = this.calculateHeaderOffset();
172
+ const phaseHeight = this.getDocPhaseWrapperHeight();
173
+ return headerOffset + phaseHeight;
174
+ };
175
+
176
+ // Updates sidebar positioning based on current header heights
177
+ private updateSidebarPosition = () => {
121
178
  requestAnimationFrame(() => {
122
179
  const redocContainer = this.getRedocContainer();
123
180
  if (!redocContainer) {
124
181
  return;
125
182
  }
126
183
 
127
- const globalNavHeight = this.getGlobalHeaderHeight();
128
- const docHeaderHeight = this.getDocHeaderHeight();
129
- const calculatedTopValue = `${globalNavHeight + docHeaderHeight}px`;
184
+ const currentSidebarTop = this.calculateHeaderOffset();
185
+ if (currentSidebarTop === this.lastSidebarTop) {
186
+ return;
187
+ }
130
188
 
131
- // Set updated top value for the sidebar and doc phase
189
+ const sidebarTopValue = `${currentSidebarTop}px`;
132
190
  this.template.host.style.setProperty(
133
191
  "--doc-c-redoc-sidebar-top",
134
- calculatedTopValue
192
+ sidebarTopValue
135
193
  );
194
+ this.lastSidebarTop = currentSidebarTop;
136
195
  });
137
196
  };
138
197
 
139
- private handleScrollAndResize = throttle(
140
- SCROLL_THROTTLE_DELAY,
141
- () => !this.showError && this.handleLayoutChange()
142
- );
143
-
144
- private getDocPhaseInfo(): string | null {
145
- if (this._parentDocPhaseInfo) {
146
- return this._parentDocPhaseInfo;
147
- }
148
- const selectedRef = this.getSelectedReference();
149
- return selectedRef?.docPhase
150
- ? JSON.stringify(selectedRef.docPhase)
151
- : null;
152
- }
153
-
154
- private getSelectedReference(): ReferenceItem | null {
155
- return (
156
- this._referenceConfig?.refList?.find((ref) => ref.isSelected) ||
157
- this._referenceConfig?.refList?.[0]
158
- );
159
- }
160
-
161
- private getRedocContainer(): HTMLElement | null {
162
- return document.querySelector(".redoc-container");
163
- }
164
-
198
+ // Updates browser URL while preserving query params and hash
165
199
  private updateUrlWithReference(selectedRef: ReferenceItem): void {
166
200
  if (selectedRef?.href) {
167
201
  const parentReferencePath = selectedRef.href;
@@ -176,46 +210,59 @@ export default class RedocReference extends LightningElement {
176
210
  }
177
211
  }
178
212
 
213
+ private handleScrollAndResize = throttle(
214
+ SCROLL_THROTTLE_DELAY,
215
+ () => !this.showError && this.updateSidebarPosition()
216
+ );
217
+
218
+ // Initializes Redoc library with selected reference configuration
179
219
  private async initializeRedoc(): Promise<void> {
180
220
  try {
181
- this.redocInitialized = true;
182
-
183
221
  await this.waitForRedoc();
222
+
223
+ const redocContainer = this.getRedocContainer();
224
+ if (!redocContainer) {
225
+ this.showErrorUI("Redoc container is not found.");
226
+ return;
227
+ }
228
+
184
229
  const selectedRef = this.getSelectedReference();
185
230
  if (selectedRef) {
186
231
  this.updateUrlWithReference(selectedRef);
187
232
 
188
233
  const specUrl = selectedRef.source;
189
- const redocContainer = this.getRedocContainer();
190
- if (specUrl && redocContainer) {
191
- window.Redoc.init(
192
- specUrl,
193
- {
194
- expandResponses: "200,400"
195
- },
196
- redocContainer,
197
- (error: any) => {
198
- if (error) {
199
- this.showError = true;
200
- console.error(
201
- "Failed to show Reference UI using Redoc: ",
202
- error
203
- );
204
- } else {
205
- this.insertCustomLayoutElements();
206
- }
207
- }
208
- );
209
- } else {
210
- this.showError = true;
234
+ if (!specUrl) {
235
+ this.showErrorUI("Spec URL not found.");
236
+ return;
211
237
  }
238
+
239
+ window.Redoc.init(
240
+ specUrl,
241
+ {
242
+ // Auto-expand HTTP 200 and 400 response sections
243
+ expandResponses: "200,400",
244
+ // Dynamic scroll offset to account for headers
245
+ scrollYOffset: this.calculateScrollYOffset
246
+ },
247
+ redocContainer,
248
+ (error: any) => {
249
+ if (error) {
250
+ this.showErrorUI(
251
+ "Failed to show Reference UI using Redoc: ",
252
+ error
253
+ );
254
+ } else {
255
+ this.integrateCustomComponents();
256
+ }
257
+ }
258
+ );
212
259
  }
213
260
  } catch (error) {
214
- this.showError = true;
215
- console.error("Failed to load Redoc library:", error);
261
+ this.showErrorUI("Failed to load Redoc library:", error);
216
262
  }
217
263
  }
218
264
 
265
+ // Polls for Redoc library availability with timeout
219
266
  private async waitForRedoc(timeout = ELEMENT_TIMEOUT): Promise<void> {
220
267
  const success = await pollUntil(
221
268
  () => !!window.Redoc,
@@ -230,39 +277,41 @@ export default class RedocReference extends LightningElement {
230
277
  }
231
278
  }
232
279
 
233
- private async insertCustomLayoutElements(): Promise<void> {
280
+ // Integrates custom elements (doc phase, footer, survey) into Redoc container
281
+ private async integrateCustomComponents(): Promise<void> {
234
282
  try {
235
283
  const redocContainer = this.getRedocContainer();
284
+ if (!redocContainer) {
285
+ return;
286
+ }
236
287
 
237
- if (redocContainer) {
238
- const apiContentDiv = await this.waitForApiContent(
239
- redocContainer
240
- );
241
- apiContentDiv.setAttribute("lwc:dom", "manual");
288
+ const apiContentDiv = await this.waitForApiContent(redocContainer);
289
+ apiContentDiv.setAttribute("lwc:dom", "manual");
242
290
 
243
- const docPhaseInfo = this.getDocPhaseInfo();
244
- if (docPhaseInfo) {
245
- this.insertDocPhase(apiContentDiv, docPhaseInfo);
246
- }
291
+ const docPhaseInfo = this.getDocPhaseInfo();
292
+ if (docPhaseInfo) {
293
+ this.insertDocPhase(apiContentDiv, docPhaseInfo);
294
+ }
247
295
 
248
- if (typeof Sprig !== "undefined") {
249
- this.insertSprigSurvey(apiContentDiv);
250
- }
296
+ if (typeof Sprig !== "undefined") {
297
+ this.insertSprigSurvey(apiContentDiv);
298
+ }
251
299
 
252
- this.insertFooter(apiContentDiv);
300
+ this.insertFooter(apiContentDiv);
253
301
 
254
- // Wait for footer to be rendered before updating styles
255
- requestAnimationFrame(() => {
256
- // Update third column bottom position to avoid footer overlap.
257
- this.updateRedocThirdColumnStyle(redocContainer);
258
- });
259
- }
302
+ // Wait for footer to be rendered before updating styles
303
+ requestAnimationFrame(() => {
304
+ this.updateRedocThirdColumnStyle(redocContainer);
305
+
306
+ // Fix initial hash scroll after doc phase insertion
307
+ this.handleInitialHashScrollFix();
308
+ });
260
309
  } catch (error) {
261
- this.showError = true;
262
- console.error("Failed to insert layout elements:", error);
310
+ this.showErrorUI("Failed to integrate custom components:", error);
263
311
  }
264
312
  }
265
313
 
314
+ // Waits for Redoc's API content element to be rendered
266
315
  private async waitForApiContent(
267
316
  container: HTMLElement
268
317
  ): Promise<HTMLElement> {
@@ -281,24 +330,25 @@ export default class RedocReference extends LightningElement {
281
330
  return container.querySelector<HTMLElement>(".api-content")!;
282
331
  }
283
332
 
333
+ // Creates and inserts doc phase component at container start
284
334
  private insertDocPhase(container: HTMLElement, docPhaseInfo: string): void {
285
335
  const wrapper = document.createElement("div");
286
336
  wrapper.className = "doc-phase-wrapper";
287
-
288
337
  container.insertBefore(wrapper, container.firstChild);
289
338
 
290
339
  const docPhaseElement = createElement("doc-phase", { is: DocPhase });
291
340
  Object.assign(docPhaseElement, { docPhaseInfo });
292
-
293
341
  wrapper.appendChild(docPhaseElement);
294
342
  }
295
343
 
344
+ // Appends footer component to container
296
345
  private insertFooter(container: HTMLElement): void {
297
346
  const footerElement = createElement("dx-footer", { is: DxFooter });
298
347
  Object.assign(footerElement, { variant: "no-signup" });
299
348
  container.appendChild(footerElement);
300
349
  }
301
350
 
351
+ // Appends Sprig survey component to container
302
352
  private insertSprigSurvey(container: HTMLElement): void {
303
353
  const wrapper = document.createElement("div");
304
354
  wrapper.className = "sprig-survey-wrapper";
@@ -310,24 +360,29 @@ export default class RedocReference extends LightningElement {
310
360
  wrapper.appendChild(feedbackElement);
311
361
  }
312
362
 
363
+ // Adjusts third column bottom position to prevent footer overlap
313
364
  private updateRedocThirdColumnStyle(redocContainer: HTMLElement): void {
314
365
  const footer = redocContainer.querySelector(
315
366
  ".redoc-wrap .api-content dx-footer"
316
367
  ) as HTMLElement;
317
- const thirdColumnContainer = redocContainer.querySelector(
368
+ if (!footer) {
369
+ console.warn(
370
+ "Footer element not found, skipping third column styling"
371
+ );
372
+ return;
373
+ }
374
+
375
+ const redocThirdColumnElement = redocContainer.querySelector(
318
376
  ".redoc-wrap > div:last-child"
319
377
  ) as HTMLElement;
320
-
321
- if (!thirdColumnContainer || !footer) {
378
+ if (!redocThirdColumnElement) {
322
379
  console.warn(
323
- !thirdColumnContainer
324
- ? "Redoc Third column container not found"
325
- : "Footer not found in DOM, skipping third column styling"
380
+ "Third column element not found, skipping third column styling"
326
381
  );
327
382
  return;
328
383
  }
329
384
 
330
- const footerHeight = footer.getBoundingClientRect().height || 0;
385
+ const footerHeight = footer.getBoundingClientRect()?.height || 0;
331
386
  const footerMarginTop = parseInt(
332
387
  getComputedStyle(this.template.host).getPropertyValue(
333
388
  "--dx-footer-margin-top"
@@ -335,9 +390,38 @@ export default class RedocReference extends LightningElement {
335
390
  10
336
391
  );
337
392
 
338
- thirdColumnContainer.style.setProperty(
393
+ redocThirdColumnElement.style.setProperty(
339
394
  "bottom",
340
395
  `${footerHeight + footerMarginTop}px`
341
396
  );
342
397
  }
398
+
399
+ // Fixes initial hash scroll positioning after doc phase insertion
400
+ private handleInitialHashScrollFix(): void {
401
+ const hash = window.location.hash;
402
+ if (!hash || hash.length <= 1) {
403
+ return;
404
+ }
405
+
406
+ // Small delay to ensure all components are fully rendered
407
+ requestAnimationFrame(() => {
408
+ const targetId = hash.substring(1);
409
+ const targetElement = document.getElementById(targetId);
410
+
411
+ if (!targetElement) {
412
+ return;
413
+ }
414
+
415
+ const elementTop =
416
+ (targetElement.getBoundingClientRect()?.top || 0) +
417
+ window.pageYOffset;
418
+ const scrollOffset = this.calculateScrollYOffset();
419
+ const correctedScrollPosition = elementTop - scrollOffset;
420
+
421
+ window.scrollTo({
422
+ top: correctedScrollPosition,
423
+ behavior: "smooth"
424
+ });
425
+ });
426
+ }
343
427
  }