@salesforcedevs/docs-components 1.21.1-redocly1 → 1.21.1-redocly3

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-redocly1",
3
+ "version": "1.21.1-redocly3",
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
 
@@ -81,13 +82,40 @@ export default class RedocReference extends LightningElement {
81
82
  disconnectedCallback(): void {
82
83
  window.removeEventListener("scroll", this.handleScrollAndResize);
83
84
  window.removeEventListener("resize", this.handleScrollAndResize);
85
+
86
+ // Clean up cached DOM element references to prevent memory leaks
87
+ this.docHeaderElement = null;
88
+ this.docPhaseWrapperElement = null;
84
89
  }
85
90
 
86
- @api
87
- setDocPhaseInfo(docPhaseInfo: string): void {
88
- this._parentDocPhaseInfo = docPhaseInfo;
91
+ // Displays error UI and logs error message for debugging
92
+ private showErrorUI(message: string, error?: any): void {
93
+ this.showError = true;
94
+ console.error(message, error);
95
+ }
96
+
97
+ private getRedocContainer(): HTMLElement | null {
98
+ return document.querySelector(".redoc-container");
89
99
  }
90
100
 
101
+ private getSelectedReference(): ReferenceItem | null {
102
+ return (
103
+ this._referenceConfig?.refList?.find((ref) => ref.isSelected) ||
104
+ this._referenceConfig?.refList?.[0]
105
+ );
106
+ }
107
+
108
+ private getDocPhaseInfo(): string | null {
109
+ if (this._parentDocPhaseInfo) {
110
+ return this._parentDocPhaseInfo;
111
+ }
112
+ const selectedRef = this.getSelectedReference();
113
+ return selectedRef?.docPhase
114
+ ? JSON.stringify(selectedRef.docPhase)
115
+ : null;
116
+ }
117
+
118
+ // Extracts numeric value from CSS custom properties
91
119
  private getGlobalCSSVariableValue(variableName: string): number {
92
120
  const value = getComputedStyle(
93
121
  document.documentElement
@@ -95,11 +123,11 @@ export default class RedocReference extends LightningElement {
95
123
  return parseInt(value, 10) || 0;
96
124
  }
97
125
 
126
+ /*
127
+ ** Since we could not use --dx-g-global-header-height as getPropertyValue returns a calc expression,
128
+ ** we are using the respective CSS variables to calculate the height.
129
+ */
98
130
  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
131
  const rowHeight = this.getGlobalCSSVariableValue(
104
132
  "--dx-g-global-header-nav-row-height"
105
133
  );
@@ -109,59 +137,62 @@ export default class RedocReference extends LightningElement {
109
137
  return rowHeight * rowCount;
110
138
  }
111
139
 
140
+ // Gets doc header height with element caching for performance
112
141
  private getDocHeaderHeight(): number {
113
142
  if (!this.docHeaderElement) {
114
143
  this.docHeaderElement =
115
144
  document.querySelector(".sticky-doc-header");
116
145
  }
117
- return this.docHeaderElement?.getBoundingClientRect().height || 0;
146
+ return this.docHeaderElement?.getBoundingClientRect()?.height || 0;
118
147
  }
119
148
 
120
- private handleLayoutChange = () => {
149
+ // Gets doc phase wrapper height with element caching for performance
150
+ private getDocPhaseWrapperHeight(): number {
151
+ if (!this.docPhaseWrapperElement) {
152
+ this.docPhaseWrapperElement =
153
+ document.querySelector(".doc-phase-wrapper");
154
+ }
155
+ return (
156
+ this.docPhaseWrapperElement?.getBoundingClientRect()?.height || 0
157
+ );
158
+ }
159
+
160
+ calculateHeaderOffset = () => {
161
+ const globalHeaderHeight = this.getGlobalHeaderHeight();
162
+ const docHeaderHeight = this.getDocHeaderHeight();
163
+ return globalHeaderHeight + docHeaderHeight;
164
+ };
165
+
166
+ // Dynamic scroll offset calculation that Redoc will call
167
+ calculateScrollYOffset = () => {
168
+ const headerOffset = this.calculateHeaderOffset();
169
+ const phaseHeight = this.getDocPhaseWrapperHeight();
170
+ return headerOffset + phaseHeight;
171
+ };
172
+
173
+ // Updates sidebar positioning based on current header heights
174
+ private updateSidebarPosition = () => {
121
175
  requestAnimationFrame(() => {
122
176
  const redocContainer = this.getRedocContainer();
123
177
  if (!redocContainer) {
124
178
  return;
125
179
  }
126
180
 
127
- const globalNavHeight = this.getGlobalHeaderHeight();
128
- const docHeaderHeight = this.getDocHeaderHeight();
129
- const calculatedTopValue = `${globalNavHeight + docHeaderHeight}px`;
181
+ const currentSidebarTop = this.calculateHeaderOffset();
182
+ if (currentSidebarTop === this.lastSidebarTop) {
183
+ return;
184
+ }
130
185
 
131
- // Set updated top value for the sidebar and doc phase
186
+ const sidebarTopValue = `${currentSidebarTop}px`;
132
187
  this.template.host.style.setProperty(
133
188
  "--doc-c-redoc-sidebar-top",
134
- calculatedTopValue
189
+ sidebarTopValue
135
190
  );
191
+ this.lastSidebarTop = currentSidebarTop;
136
192
  });
137
193
  };
138
194
 
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
-
195
+ // Updates browser URL while preserving query params and hash
165
196
  private updateUrlWithReference(selectedRef: ReferenceItem): void {
166
197
  if (selectedRef?.href) {
167
198
  const parentReferencePath = selectedRef.href;
@@ -176,46 +207,62 @@ export default class RedocReference extends LightningElement {
176
207
  }
177
208
  }
178
209
 
210
+ private handleScrollAndResize = throttle(
211
+ SCROLL_THROTTLE_DELAY,
212
+ () => !this.showError && this.updateSidebarPosition()
213
+ );
214
+
215
+ // Initializes Redoc library with selected reference configuration
179
216
  private async initializeRedoc(): Promise<void> {
180
217
  try {
181
218
  this.redocInitialized = true;
182
-
183
219
  await this.waitForRedoc();
220
+
221
+ const redocContainer = this.getRedocContainer();
222
+ if (!redocContainer) {
223
+ this.showErrorUI("Redoc container is not found.");
224
+ return;
225
+ }
226
+
184
227
  const selectedRef = this.getSelectedReference();
185
228
  if (selectedRef) {
186
229
  this.updateUrlWithReference(selectedRef);
187
230
 
188
231
  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
- }
232
+ if (!specUrl) {
233
+ this.showErrorUI(
234
+ "Spec URL or Redoc container is not found."
208
235
  );
209
- } else {
210
- this.showError = true;
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,52 +330,59 @@ 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
- const docPhaseElement = createElement("doc-phase", { is: DocPhase });
286
- Object.assign(docPhaseElement, { docPhaseInfo });
287
-
288
335
  const wrapper = document.createElement("div");
289
336
  wrapper.className = "doc-phase-wrapper";
290
- wrapper.appendChild(docPhaseElement);
291
-
292
337
  container.insertBefore(wrapper, container.firstChild);
338
+
339
+ const docPhaseElement = createElement("doc-phase", { is: DocPhase });
340
+ Object.assign(docPhaseElement, { docPhaseInfo });
341
+ wrapper.appendChild(docPhaseElement);
293
342
  }
294
343
 
344
+ // Appends footer component to container
295
345
  private insertFooter(container: HTMLElement): void {
296
346
  const footerElement = createElement("dx-footer", { is: DxFooter });
297
347
  Object.assign(footerElement, { variant: "no-signup" });
298
348
  container.appendChild(footerElement);
299
349
  }
300
350
 
351
+ // Appends Sprig survey component to container
301
352
  private insertSprigSurvey(container: HTMLElement): void {
353
+ const wrapper = document.createElement("div");
354
+ wrapper.className = "sprig-survey-wrapper";
355
+ container.appendChild(wrapper);
356
+
302
357
  const feedbackElement = createElement("doc-sprig-survey", {
303
358
  is: SprigSurvey
304
359
  });
305
-
306
- const wrapper = document.createElement("div");
307
- wrapper.className = "sprig-survey-wrapper";
308
360
  wrapper.appendChild(feedbackElement);
309
- container.appendChild(wrapper);
310
361
  }
311
362
 
363
+ // Adjusts third column bottom position to prevent footer overlap
312
364
  private updateRedocThirdColumnStyle(redocContainer: HTMLElement): void {
313
365
  const footer = redocContainer.querySelector(
314
366
  ".redoc-wrap .api-content dx-footer"
315
367
  ) as HTMLElement;
316
- 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(
317
376
  ".redoc-wrap > div:last-child"
318
377
  ) as HTMLElement;
319
-
320
- if (!thirdColumnContainer || !footer) {
378
+ if (!redocThirdColumnElement) {
321
379
  console.warn(
322
- !thirdColumnContainer
323
- ? "Redoc Third column container not found"
324
- : "Footer not found in DOM, skipping third column styling"
380
+ "Third column element not found, skipping third column styling"
325
381
  );
326
382
  return;
327
383
  }
328
384
 
329
- const footerHeight = footer.getBoundingClientRect().height || 0;
385
+ const footerHeight = footer.getBoundingClientRect()?.height || 0;
330
386
  const footerMarginTop = parseInt(
331
387
  getComputedStyle(this.template.host).getPropertyValue(
332
388
  "--dx-footer-margin-top"
@@ -334,9 +390,38 @@ export default class RedocReference extends LightningElement {
334
390
  10
335
391
  );
336
392
 
337
- thirdColumnContainer.style.setProperty(
393
+ redocThirdColumnElement.style.setProperty(
338
394
  "bottom",
339
395
  `${footerHeight + footerMarginTop}px`
340
396
  );
341
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
+ }
342
427
  }