@salesforcedevs/docs-components 1.34.4-final-alpha1 → 1.34.4-scroll-2

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.34.4-final-alpha1",
3
+ "version": "1.34.4-scroll-2",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -25,5 +25,6 @@
25
25
  "@types/lodash.orderby": "4.6.9",
26
26
  "@types/lodash.uniqby": "4.7.9"
27
27
  },
28
- "gitHead": "4629fdd9ca18a13480044ad43515b91945d16aad"
28
+ "gitHead": "4629fdd9ca18a13480044ad43515b91945d16aad",
29
+ "stableVersion": "1.34.4"
29
30
  }
@@ -496,8 +496,18 @@ export default class ContentLayout extends LightningElement {
496
496
  }
497
497
 
498
498
  if (!this.didScrollToSelectedHash) {
499
+ this.tryScrollToHash(headingElements);
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Completes the one-shot if no hash exists or scrolling succeeds,
505
+ * retrying only when the target isn't available yet to prevent
506
+ * unexpected scrolls on observer re-attach. */
507
+ // eslint-disable-next-line no-undef
508
+ protected tryScrollToHash(headingElements: NodeListOf<Element>): void {
509
+ if (!window.location.hash || this.scrollToHash(headingElements)) {
499
510
  this.didScrollToSelectedHash = true;
500
- this.scrollToHash(headingElements);
501
511
  }
502
512
  }
503
513
 
@@ -585,6 +595,10 @@ export default class ContentLayout extends LightningElement {
585
595
  this.updateRNB();
586
596
  this.updateContentActionToolbar();
587
597
  this.contentLoaded = true;
598
+
599
+ if (this.enableSlotChange && !this.didScrollToSelectedHash) {
600
+ this.tryScrollToHash(this.getHeadingElements());
601
+ }
588
602
  }
589
603
 
590
604
  protected disconnectObserver(): void {
@@ -595,7 +609,7 @@ export default class ContentLayout extends LightningElement {
595
609
  }
596
610
 
597
611
  // eslint-disable-next-line no-undef
598
- protected scrollToHash(headingElements: NodeListOf<Element>): void {
612
+ protected scrollToHash(headingElements: NodeListOf<Element>): boolean {
599
613
  let { hash } = window.location;
600
614
  if (hash) {
601
615
  hash = hash.substr(1);
@@ -610,14 +624,18 @@ export default class ContentLayout extends LightningElement {
610
624
  "hgf-c360contextnav"
611
625
  ) as HTMLElement;
612
626
 
627
+ if (!docHeaderEl || !globalNavEl || !contextNavEl) {
628
+ return false;
629
+ }
630
+
613
631
  const headerHeight =
614
- docHeaderEl?.offsetHeight +
615
- globalNavEl?.offsetHeight +
616
- contextNavEl?.offsetHeight;
632
+ (docHeaderEl.offsetHeight || 0) +
633
+ (globalNavEl.offsetHeight || 0) +
634
+ (contextNavEl.offsetHeight || 0);
617
635
 
618
636
  const docPhaseEl = (
619
637
  this.template.querySelector("[name=doc-phase]")! as any
620
- ).assignedElements()[0] as HTMLSlotElement;
638
+ )?.assignedElements?.()[0] as HTMLSlotElement | undefined;
621
639
 
622
640
  const offset = docPhaseEl
623
641
  ? headerHeight + docPhaseEl.offsetHeight
@@ -626,10 +644,11 @@ export default class ContentLayout extends LightningElement {
626
644
  for (const headingElement of headingElements as any) {
627
645
  if (headingElement.getAttribute("id") === hash) {
628
646
  this.scrollIntoViewWithOffset(headingElement, offset);
629
- break;
647
+ return true;
630
648
  }
631
649
  }
632
650
  }
651
+ return false;
633
652
  }
634
653
 
635
654
  protected scrollIntoViewWithOffset(
@@ -3,5 +3,4 @@
3
3
  --doc-c-redoc-sidebar-top: calc(
4
4
  var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
5
5
  );
6
- --doc-c-redoc-sidebar-bg: white;
7
6
  }
@@ -4,22 +4,11 @@ import DocPhase from "doc/phase";
4
4
  import DxFooter from "dx/footer";
5
5
  import DxIcon from "dx/icon";
6
6
  import SidebarFooterNav from "dx/sidebarFooterNav";
7
- import VersionPicker from "doc/versionPicker";
8
7
  import SprigSurvey from "doc/sprigSurvey";
9
8
  import { throttle } from "throttle-debounce";
10
9
  import { pollUntil } from "dxUtils/async";
11
10
  import { toJson } from "dxUtils/normalizers";
12
- import { oldVersionDocInfo } from "docUtils/utils";
13
- import {
14
- applyVersionToHref,
15
- collectSubtreeCanonicalHrefs,
16
- normalizePath,
17
- getTopicVersionKey,
18
- persistTopicVersion,
19
- toCanonicalHref
20
- } from "dxUtils/topicVersion";
21
- import type { VersionedNode } from "dxUtils/topicVersion";
22
- import type { DocPhaseInfo, OptionWithLink } from "typings/custom";
11
+ import type { OptionWithLink } from "typings/custom";
23
12
 
24
13
  declare global {
25
14
  interface Window {
@@ -44,20 +33,8 @@ type ReferenceItem = {
44
33
  topic?: ReferenceTopic;
45
34
  };
46
35
 
47
- /** A selectable spec version. Mirrors `ReferenceVersion` in `doc/amfReference`. */
48
- type ReferenceVersion = {
49
- id: string;
50
- label: string;
51
- deprecated?: boolean;
52
- selected?: boolean;
53
- link: { href: string };
54
- };
55
-
56
36
  type ReferenceConfig = {
57
37
  refList: ReferenceItem[];
58
- versions?: ReferenceVersion[];
59
- /** Sidebar topic name. Sidebar stores the selected version under this key. */
60
- versionTopicKey?: string;
61
38
  };
62
39
 
63
40
  const SCROLL_THROTTLE_DELAY = 50;
@@ -65,7 +42,6 @@ const ELEMENT_TIMEOUT = 10000;
65
42
  const ELEMENT_CHECK_INTERVAL = 100;
66
43
  const DEFAULT_PROJECT_TITLE = "All Reference";
67
44
  const BACK_TARGET_STORAGE_KEY = "redoc-back-target";
68
- const SELECTED_VIEW_STORAGE_KEY = "redoc-selected-view";
69
45
  const DOCS_PATH_SEGMENT = "docs";
70
46
  const DEFAULT_LOCALE = "en-us";
71
47
 
@@ -140,29 +116,6 @@ export default class RedocReference extends LightningElement {
140
116
  */
141
117
  @api headerHref: string | null = null;
142
118
 
143
- /**
144
- * Versioned topic that owns this spec, including its children.
145
- * Href matches this page, so an older version still has its version
146
- * in the path.
147
- */
148
- @api
149
- set parentTopicFamily(value: string | { topics?: VersionedNode[] } | null) {
150
- try {
151
- const parsed = toJson(value) as { topics?: VersionedNode[] } | null;
152
- this._parentTopicFamilyNode = parsed?.topics?.[0] ?? null;
153
- } catch (error) {
154
- this._parentTopicFamilyNode = null;
155
- }
156
- }
157
-
158
- get parentTopicFamily(): { topics?: VersionedNode[] } | null {
159
- return this._parentTopicFamilyNode
160
- ? { topics: [this._parentTopicFamilyNode] }
161
- : null;
162
- }
163
-
164
- private _parentTopicFamilyNode: VersionedNode | null = null;
165
-
166
119
  @api
167
120
  get contentType(): string {
168
121
  return this._contentType;
@@ -199,40 +152,6 @@ export default class RedocReference extends LightningElement {
199
152
  return refCount > 1;
200
153
  }
201
154
 
202
- get versions(): ReferenceVersion[] {
203
- return this._referenceConfig?.versions ?? [];
204
- }
205
-
206
- /** The version flagged `selected`, else the first (the latest/GA version). */
207
- get selectedVersion(): ReferenceVersion | null {
208
- const versions = this.versions;
209
- return versions.find((v) => v.selected) ?? versions[0] ?? null;
210
- }
211
-
212
- /** True when the selected version is the latest/GA one (index 0). */
213
- get latestVersion(): boolean {
214
- return this.versions.length
215
- ? this.selectedVersion?.id === this.versions[0].id
216
- : false;
217
- }
218
-
219
- get hasVersionPicker(): boolean {
220
- return this.versions.length > 0;
221
- }
222
-
223
- /** "Newer version available" banner info when on a non-latest version, else null. */
224
- private get oldVersionInfo(): DocPhaseInfo | null {
225
- if (this.versions.length > 1 && !this.latestVersion) {
226
- return oldVersionDocInfo(this.versions[0].link.href);
227
- }
228
- return null;
229
- }
230
-
231
- /** A single version has nothing to choose, so it renders read-only. */
232
- get isVersionReadOnly(): boolean {
233
- return this.versions.length === 1;
234
- }
235
-
236
155
  // Reads stored back target
237
156
  private getBackTargetFromSession(): string | null {
238
157
  return sessionStorage.getItem(BACK_TARGET_STORAGE_KEY);
@@ -267,64 +186,13 @@ export default class RedocReference extends LightningElement {
267
186
  private onBackClick = (event: Event): void => {
268
187
  event.preventDefault();
269
188
 
270
- const target = this.getVersionedBackTarget() ?? this.headerHref;
189
+ const target = this.getTranslatedBackTarget() ?? this.headerHref;
271
190
 
272
191
  if (target) {
273
192
  window.location.assign(target);
274
193
  }
275
194
  };
276
195
 
277
- /**
278
- * When the back target is in this spec's topic, return it on the
279
- * selected version. With no back target, do the same for the header
280
- * link when that link is the versioned parent.
281
- */
282
- private getVersionedBackTarget(): string | null {
283
- const stored = this.getTranslatedBackTarget();
284
- const selectedVersion = this.selectedVersion;
285
- const target = stored ?? this.headerHref;
286
- if (!target || !selectedVersion) {
287
- return stored;
288
- }
289
-
290
- let targetUrl: URL;
291
- try {
292
- targetUrl = new URL(target, window.location.origin);
293
- } catch {
294
- return target;
295
- }
296
-
297
- // Family hrefs match this page's version. Compare them without
298
- // the version segment. Rewrite only the pathname so the target's
299
- // query and hash survive.
300
- const familyHrefs = this._parentTopicFamilyNode
301
- ? collectSubtreeCanonicalHrefs(this._parentTopicFamilyNode).map(
302
- (href) => toCanonicalHref(href, this.versions)
303
- )
304
- : [];
305
-
306
- const canonicalTarget = toCanonicalHref(
307
- targetUrl.pathname,
308
- this.versions
309
- );
310
- const inFamily = familyHrefs.some(
311
- (href) => normalizePath(href) === normalizePath(canonicalTarget)
312
- );
313
- if (!inFamily) {
314
- return target;
315
- }
316
-
317
- const versioned = applyVersionToHref(
318
- canonicalTarget,
319
- selectedVersion,
320
- this.versions
321
- );
322
- if (!versioned) {
323
- return target;
324
- }
325
- return `${versioned}${targetUrl.search}${targetUrl.hash}`;
326
- }
327
-
328
196
  private getTranslatedBackTarget(): string | null {
329
197
  const stored = this.getBackTargetFromSession();
330
198
  if (stored) {
@@ -334,9 +202,7 @@ export default class RedocReference extends LightningElement {
334
202
  }
335
203
 
336
204
  const referrer = this.getSameOriginReferrerHref();
337
- return referrer &&
338
- !this.isLocaleHref(new URL(referrer).pathname) &&
339
- !this.isVersionHref(new URL(referrer).pathname)
205
+ return referrer && !this.isLocaleHref(new URL(referrer).pathname)
340
206
  ? referrer
341
207
  : null;
342
208
  }
@@ -375,10 +241,7 @@ export default class RedocReference extends LightningElement {
375
241
  }
376
242
 
377
243
  const referrerUrl = new URL(referrerHref);
378
- if (
379
- this.isLocaleHref(referrerUrl.pathname) ||
380
- this.isVersionHref(referrerUrl.pathname)
381
- ) {
244
+ if (this.isLocaleHref(referrerUrl.pathname)) {
382
245
  return;
383
246
  }
384
247
 
@@ -419,19 +282,6 @@ export default class RedocReference extends LightningElement {
419
282
  });
420
283
  }
421
284
 
422
- /**
423
- * switching version on this same page.
424
- */
425
- private isVersionHref(pathname: string): boolean {
426
- return this.versions.some((version) => {
427
- const href = version?.link?.href;
428
- return (
429
- !!href &&
430
- new URL(href, window.location.origin).pathname === pathname
431
- );
432
- });
433
- }
434
-
435
285
  /** When origin is provided, pass it to the footer; otherwise use dx-footer's default. */
436
286
  get effectiveFooterOrigin(): string {
437
287
  return (
@@ -447,13 +297,6 @@ export default class RedocReference extends LightningElement {
447
297
  // Track the back target, preserving it across locale switches
448
298
  this.trackBackTarget();
449
299
 
450
- // Remember the selected version for the sidebar. Changing version
451
- // reloads this page.
452
- const topicKey = this.getParentTopicVersionKey();
453
- if (topicKey && this.selectedVersion) {
454
- persistTopicVersion(topicKey, this.selectedVersion.id);
455
- }
456
-
457
300
  window.addEventListener("scroll", this.handleScrollAndResize);
458
301
  window.addEventListener("resize", this.handleScrollAndResize);
459
302
  }
@@ -511,14 +354,6 @@ export default class RedocReference extends LightningElement {
511
354
  return parseInt(value, 10) || 0;
512
355
  }
513
356
 
514
- /** Redoc's LNB background color, themeable via `--doc-c-redoc-sidebar-bg`. */
515
- private get sidebarBackgroundColor(): string {
516
- const value = getComputedStyle(this.template.host).getPropertyValue(
517
- "--doc-c-redoc-sidebar-bg"
518
- );
519
- return value.trim() || "white";
520
- }
521
-
522
357
  /*
523
358
  ** Since we could not use --dx-g-global-header-height as getPropertyValue returns a calc expression,
524
359
  ** we are using the respective CSS variables to calculate the height.
@@ -634,14 +469,7 @@ export default class RedocReference extends LightningElement {
634
469
  specUrl,
635
470
  {
636
471
  // Dynamic scroll offset to account for headers
637
- scrollYOffset: this.calculateScrollYOffset,
638
- // Redoc's own styled background outranks injected
639
- // CSS, so the LNB background must be set via theme.
640
- theme: {
641
- sidebar: {
642
- backgroundColor: this.sidebarBackgroundColor
643
- }
644
- }
472
+ scrollYOffset: this.calculateScrollYOffset
645
473
  },
646
474
  redocContainer,
647
475
  (error: any) => {
@@ -687,7 +515,10 @@ export default class RedocReference extends LightningElement {
687
515
  const apiContentDiv = await this.waitForApiContent(redocContainer);
688
516
  apiContentDiv.setAttribute("lwc:dom", "manual");
689
517
 
690
- this.insertStatusItems(apiContentDiv);
518
+ const docPhaseInfo = this.getDocPhaseInfo();
519
+ if (docPhaseInfo) {
520
+ this.insertDocPhase(apiContentDiv, docPhaseInfo);
521
+ }
691
522
 
692
523
  this.appendFooterItems(apiContentDiv);
693
524
 
@@ -696,22 +527,10 @@ export default class RedocReference extends LightningElement {
696
527
 
697
528
  // Wait for footer to be rendered before updating styles
698
529
  requestAnimationFrame(() => {
699
- try {
700
- this.updateRedocThirdColumnStyle(redocContainer);
701
-
702
- // Restore the view selected before a version switch only
703
- // after all layout mutations (footer, header) are
704
- // complete, so the scroll lands on the correct element.
705
- this.restoreSelectedView();
706
-
707
- // Fix initial hash scroll after doc phase insertion
708
- this.handleInitialHashScrollFix();
709
- } catch (error) {
710
- this.showErrorUI(
711
- "Failed to integrate custom components:",
712
- error
713
- );
714
- }
530
+ this.updateRedocThirdColumnStyle(redocContainer);
531
+
532
+ // Fix initial hash scroll after doc phase insertion
533
+ this.handleInitialHashScrollFix();
715
534
  });
716
535
  } catch (error) {
717
536
  this.showErrorUI("Failed to integrate custom components:", error);
@@ -741,65 +560,6 @@ export default class RedocReference extends LightningElement {
741
560
  }
742
561
  }
743
562
 
744
- /** Builds the version picker DOM by reusing `doc-version-picker`. */
745
- private buildVersionPickerDom(): HTMLElement {
746
- const wrapper = document.createElement("div");
747
- wrapper.className = "redoc-version-picker";
748
-
749
- const picker = createElement("doc-version-picker", {
750
- is: VersionPicker
751
- });
752
-
753
- Object.assign(picker, {
754
- versions: this.versions,
755
- selectedVersion: this.selectedVersion,
756
- latestVersion: this.latestVersion,
757
- readOnly: this.isVersionReadOnly
758
- });
759
- picker.addEventListener("change", this.onVersionChange);
760
- wrapper.appendChild(picker);
761
-
762
- return wrapper;
763
- }
764
-
765
- /**
766
- * Key the sidebar uses for this topic's selected version.
767
- * Missing `versionTopicKey` returns null so nothing is written.
768
- */
769
- private getParentTopicVersionKey(): string | null {
770
- const versionTopicKey = this._referenceConfig?.versionTopicKey;
771
- if (!versionTopicKey) {
772
- return null;
773
- }
774
- return getTopicVersionKey({ name: versionTopicKey }) || null;
775
- }
776
-
777
- /** Stashes the current view (URL hash) before the version link navigates. */
778
- private onVersionChange = (): void => {
779
- sessionStorage.setItem(
780
- SELECTED_VIEW_STORAGE_KEY,
781
- window.location.hash || ""
782
- );
783
- };
784
-
785
- /**
786
- * Re-applies the view stashed before a version switch, if its anchor exists
787
- * in the new spec; otherwise a no-op (lands on the spec root).
788
- */
789
- private restoreSelectedView(): void {
790
- const storedHash = sessionStorage.getItem(SELECTED_VIEW_STORAGE_KEY);
791
- sessionStorage.removeItem(SELECTED_VIEW_STORAGE_KEY);
792
-
793
- if (!storedHash || window.location.hash) {
794
- return;
795
- }
796
-
797
- const targetId = storedHash.replace(/^#/, "");
798
- if (targetId && document.getElementById(targetId)) {
799
- window.location.hash = storedHash;
800
- }
801
- }
802
-
803
563
  /**
804
564
  * Builds the locale picker DOM by reusing `dx-sidebar-footer-nav`
805
565
  */
@@ -821,17 +581,12 @@ export default class RedocReference extends LightningElement {
821
581
  }
822
582
 
823
583
  /**
824
- * Builds the doc header: a title group (back link + spec title) and, when
825
- * versions are available, the version picker as a sibling row. Layout/gaps
826
- * are styled by the developer-website's redoc CSS.
584
+ * Builds a fresh project-title/spec-title header DOM node.
827
585
  */
828
586
  private buildProjectHeaderDom(): HTMLElement {
829
587
  const wrapper = document.createElement("div");
830
588
  wrapper.className = "redoc-project-header";
831
589
 
832
- const main = document.createElement("div");
833
- main.className = "redoc-project-header-main";
834
-
835
590
  if (this.projectTitle) {
836
591
  const backLink = document.createElement("a");
837
592
  backLink.className = "redoc-project-back";
@@ -852,20 +607,14 @@ export default class RedocReference extends LightningElement {
852
607
 
853
608
  backLink.appendChild(icon);
854
609
  backLink.appendChild(label);
855
- main.appendChild(backLink);
610
+ wrapper.appendChild(backLink);
856
611
  }
857
612
 
858
613
  if (this.specTitle) {
859
614
  const specEl = document.createElement("h2");
860
615
  specEl.className = "redoc-spec-title dx-text-display-7";
861
616
  specEl.textContent = this.specTitle;
862
- main.appendChild(specEl);
863
- }
864
-
865
- wrapper.appendChild(main);
866
-
867
- if (this.hasVersionPicker) {
868
- wrapper.appendChild(this.buildVersionPickerDom());
617
+ wrapper.appendChild(specEl);
869
618
  }
870
619
 
871
620
  return wrapper;
@@ -890,40 +639,15 @@ export default class RedocReference extends LightningElement {
890
639
  return container.querySelector<HTMLElement>(".api-content")!;
891
640
  }
892
641
 
893
- // Inserts the doc phase and, on a non-latest version, a dismissible
894
- // "newer version" banner into a shared doc-phase-wrapper at container start
895
- private insertStatusItems(container: HTMLElement): void {
896
- const docPhaseInfo = this.getDocPhaseInfo();
897
- const oldVersionInfo = this.oldVersionInfo;
898
- if (!docPhaseInfo && !oldVersionInfo) {
899
- return;
900
- }
901
-
642
+ // Creates and inserts doc phase component at container start
643
+ private insertDocPhase(container: HTMLElement, docPhaseInfo: string): void {
902
644
  const wrapper = document.createElement("div");
903
645
  wrapper.className = "doc-phase-wrapper";
904
646
  container.insertBefore(wrapper, container.firstChild);
905
647
 
906
- if (docPhaseInfo) {
907
- const docPhaseElement = createElement("doc-phase", {
908
- is: DocPhase
909
- });
910
- Object.assign(docPhaseElement, { docPhaseInfo });
911
- wrapper.appendChild(docPhaseElement);
912
- }
913
-
914
- if (oldVersionInfo) {
915
- const versionBanner = createElement("doc-phase", { is: DocPhase });
916
- Object.assign(versionBanner, {
917
- docPhaseInfo: oldVersionInfo,
918
- dismissible: true,
919
- iconName: "warning"
920
- });
921
- versionBanner.addEventListener("dismissphase", () => {
922
- versionBanner.remove();
923
- this.updateSidebarPosition();
924
- });
925
- wrapper.appendChild(versionBanner);
926
- }
648
+ const docPhaseElement = createElement("doc-phase", { is: DocPhase });
649
+ Object.assign(docPhaseElement, { docPhaseInfo });
650
+ wrapper.appendChild(docPhaseElement);
927
651
  }
928
652
 
929
653
  // Appends footer component to container
@@ -24,14 +24,6 @@
24
24
  lwc:if={docPhaseInfo}
25
25
  doc-phase-info={docPhaseInfo}
26
26
  ></doc-phase>
27
- <doc-phase
28
- slot="version-banner"
29
- lwc:if={showVersionBanner}
30
- doc-phase-info={oldVersionInfo}
31
- icon-name="warning"
32
- dismissible="true"
33
- ondismissphase={handleDismissVersionBanner}
34
- ></doc-phase>
35
27
  <slot></slot>
36
28
  </doc-content-layout>
37
29
  </template>
@@ -1,12 +1,6 @@
1
- import { LightningElement, api, track } from "lwc";
1
+ import { LightningElement, api } from "lwc";
2
2
  import { toJson } from "dxUtils/normalizers";
3
- import type { VersionedNode } from "dxUtils/topicVersion";
4
- import {
5
- findTopicVersionForCurrentPage,
6
- latestHrefForCurrentPage
7
- } from "dxUtils/topicVersion";
8
- import { oldVersionDocInfo } from "docUtils/utils";
9
- import type { DocPhaseInfo, OptionWithLink, TreeNode } from "typings/custom";
3
+ import type { OptionWithLink, TreeNode } from "typings/custom";
10
4
 
11
5
  /**
12
6
  * Per-topic type emitted by the docs content-type parser
@@ -16,38 +10,61 @@ import type { DocPhaseInfo, OptionWithLink, TreeNode } from "typings/custom";
16
10
  */
17
11
  const TOPIC_TYPE_SPEC = "spec";
18
12
 
13
+ /**
14
+ * Walks down the first-child chain and returns the first markdown content
15
+ * page found, descending through nested content-less parents. Returns
16
+ * undefined if the chain breaks on a spec or external link before reaching a
17
+ * content page.
18
+ */
19
+ function firstChildContentPage(node: TreeNode): TreeNode | undefined {
20
+ const firstChild = node.children?.[0];
21
+ if (!firstChild) {
22
+ return undefined;
23
+ }
24
+
25
+ const isSpec =
26
+ (firstChild as { topicType?: string }).topicType === TOPIC_TYPE_SPEC;
27
+ const isExternal = firstChild.link?.target === "_blank";
28
+ if (isSpec || isExternal) {
29
+ return undefined;
30
+ }
31
+
32
+ if (firstChild.link?.href) {
33
+ return firstChild;
34
+ }
35
+
36
+ return firstChildContentPage(firstChild);
37
+ }
38
+
19
39
  /**
20
40
  * Translates per-topic `topicType` into the generic `showForwardArrow` flag
21
- * that `dx-tree-tile` reads (spec topics get the forward arrow icon for Redoc).
22
- *
23
- * Content-less parents are flagged with redirectToChild and left without a
24
- * link. The tree selects the first markdown descendant. Copying that child's
25
- * href onto the parent makes the parent win selection.
41
+ * that `dx-tree-tile` reads (spec topics get the forward arrow icon for Redoc),
42
+ * and redirects content-less parent topics whose first child is a content page
26
43
  */
27
- function decorateTopicsWithForwardArrow(
28
- topics:
29
- | Array<TreeNode & { topicType?: string; versions?: unknown }>
30
- | undefined
44
+ function decorateTopics(
45
+ topics: Array<TreeNode & { topicType?: string }> | undefined
31
46
  ): TreeNode[] | undefined {
32
47
  return topics?.map((topic) => {
33
- const { topicType, versions, ...decorated } = topic;
48
+ const { topicType, ...decorated } = topic;
34
49
  if (topicType === TOPIC_TYPE_SPEC) {
35
50
  decorated.showForwardArrow = true;
36
- // Nested and standalone specs never get a sidebar picker; only the
37
- // parent MD topic (when versioned) shows one.
38
- } else if (versions) {
39
- decorated.versions = versions;
40
51
  }
41
52
  if (decorated.children) {
42
- decorated.children = decorateTopicsWithForwardArrow(
43
- decorated.children
44
- );
53
+ decorated.children = decorateTopics(decorated.children);
45
54
  }
46
55
 
47
- // Flag a parent with no page of its own. Leave its link empty so the
48
- // child's URL stays indexed to the child.
49
- if (!!decorated.children?.length && !decorated.link?.href) {
56
+ /** Redirect a parent that has no content of its own to its first child
57
+ * but only when that first child is itself a content page
58
+ */
59
+ const isContentlessParent =
60
+ !!decorated.children?.length && !decorated.link?.href;
61
+
62
+ if (isContentlessParent) {
50
63
  decorated.redirectToChild = true;
64
+ const firstChild = firstChildContentPage(topic);
65
+ if (firstChild?.link) {
66
+ decorated.link = { ...firstChild.link };
67
+ }
51
68
  }
52
69
 
53
70
  return decorated;
@@ -84,9 +101,6 @@ export default class UnifiedContentLayout extends LightningElement {
84
101
 
85
102
  private _docPhaseInfo: string | null = null;
86
103
  private _sidebarContent: unknown = null;
87
- private _oldVersionInfo: DocPhaseInfo | null = null;
88
-
89
- @track showVersionBanner = false;
90
104
 
91
105
  @api
92
106
  get docPhaseInfo(): string | null {
@@ -103,40 +117,7 @@ export default class UnifiedContentLayout extends LightningElement {
103
117
  }
104
118
 
105
119
  set sidebarContent(value: string) {
106
- const topics = decorateTopicsWithForwardArrow(toJson(value)?.topics);
107
- this._sidebarContent = topics;
108
- this.updateTopicVersionBanner(topics as VersionedNode[] | undefined);
109
- }
110
-
111
- get oldVersionInfo(): DocPhaseInfo | null {
112
- return this._oldVersionInfo;
113
- }
114
-
115
- handleDismissVersionBanner(): void {
116
- this.showVersionBanner = false;
117
- }
118
-
119
- /**
120
- * Same banner references use when the reader is on an older version.
121
- * Topic versions are not a separate page property: the sidebar tree
122
- * carries each version's switching URL, and the current pathname says
123
- * which one is open.
124
- */
125
- private updateTopicVersionBanner(
126
- topics: VersionedNode[] | undefined
127
- ): void {
128
- const match = findTopicVersionForCurrentPage(topics);
129
- const latest = match?.node.versions?.[0];
130
- const latestHref = latestHrefForCurrentPage(match?.node.versions);
131
-
132
- if (match && latest && match.version.id !== latest.id && latestHref) {
133
- this._oldVersionInfo = oldVersionDocInfo(latestHref);
134
- this.showVersionBanner = true;
135
- return;
136
- }
137
-
138
- this._oldVersionInfo = null;
139
- this.showVersionBanner = false;
120
+ this._sidebarContent = decorateTopics(toJson(value)?.topics);
140
121
  }
141
122
 
142
123
  private get enableFooter(): boolean {
@@ -2,69 +2,28 @@
2
2
 
3
3
  /* NOTE: doc-version-picker-width width variable is used by both dx-button and dx-dropdown to maintain a consistent width. */
4
4
  :host {
5
- /* Always positioned so the popover's containing block stays the picker.
6
- Toggling position with aria-expanded lets the menu jump to the sidebar
7
- (top of the ToC) for a frame before LWC hides it. */
8
- position: relative;
9
-
10
5
  --dx-c-dropdown-option-font-weight: normal;
11
- --dx-c-dropdown-option-font-size: var(--dx-g-text-base);
12
6
  --dx-c-dropdown-option-label-color: var(--dx-g-gray-10);
13
- --dx-c-dropdown-option-active-label-color: var(--dx-g-gray-10);
14
- --dx-c-dropdown-option-padding: var(--dx-g-spacing-sm)
15
- var(--dx-g-spacing-lg);
16
- --dx-c-popover-padding: var(--dx-g-spacing-sm) 0;
17
7
  --popover-container-open-transform: translateY(4px);
18
- --dx-c-version-picker-focus-ring: 0 0 0 2px white,
19
- 0 0 0 4px var(--dx-g-blue-vibrant-60);
20
- }
21
-
22
- /* Open menus must stack above later sibling pickers in the sidebar. */
23
- :host(:has([aria-expanded="true"])) {
24
- z-index: 2;
25
8
  }
26
9
 
27
10
  .version-picker-container {
28
- /* Override --doc-version-picker-padding to change the inset (e.g. when the
29
- picker sits inside an already-padded container). */
30
- padding: var(
31
- --doc-version-picker-padding,
32
- var(--dx-g-spacing-sm) var(--dx-g-spacing-lg) var(--dx-g-spacing-sm)
33
- var(--dx-g-global-header-padding-horizontal)
34
- );
35
-
36
- /* Override --doc-version-picker-divider (e.g. `none`) to restyle dividers. */
37
- border-top: var(
38
- --doc-version-picker-divider,
39
- 1px solid var(--dx-g-gray-90)
40
- );
41
- border-bottom: var(
42
- --doc-version-picker-divider,
43
- 1px solid var(--dx-g-gray-90)
44
- );
45
- }
46
-
47
- /* Inline sidebar variant: drop the header-slot chrome so the picker sits
48
- beside a tree node without extra padding or divider lines. */
49
- .version-picker-container-small {
50
- display: flex;
51
- align-items: center;
52
- padding: 0;
53
- border: none;
11
+ padding: 8px var(--dx-g-spacing-lg) 8px
12
+ var(--dx-g-global-header-padding-horizontal);
13
+ border-top: 1px solid var(--dx-g-gray-90);
14
+ border-bottom: 1px solid var(--dx-g-gray-90);
54
15
  }
55
16
 
56
17
  .version-picker-button {
57
18
  display: flex;
58
- width: 100%;
59
- overflow: visible;
60
-
61
- --dx-c-button-horizontal-spacing: var(--dx-g-spacing-sm);
62
- --dx-g-button-icon-color: var(--dx-g-gray-50);
19
+ width: var(--doc-version-picker-width, 296px);
63
20
  }
64
21
 
65
22
  .version-picker-button:hover,
66
- .version-picker-button[aria-expanded="true"] {
67
- --dx-g-button-icon-color: var(--dx-g-blue-vibrant-20);
23
+ .version-picker-button:active,
24
+ .version-picker-button:focus {
25
+ --dx-c-button-secondary-color-hover: var(--dx-g-cloud-blue-vibrant-95);
26
+ --dx-c-button-primary-color: var(--dx-g-blue-vibrant-40);
68
27
  }
69
28
 
70
29
  /**
@@ -76,57 +35,6 @@ dx-button::part(content) {
76
35
  overflow: hidden;
77
36
  }
78
37
 
79
- /* Border width stays 1px in every state. Hover, open, and focus paint the
80
- 2px blue stroke with an inset shadow so the box does not grow.
81
- Tertiary's hover border is overridden. */
82
- .version-picker-button::part(container) {
83
- box-sizing: border-box;
84
- background: white;
85
- border: 1px solid var(--dx-g-gray-50);
86
- border-radius: var(--dx-g-spacing-xs);
87
- box-shadow: none;
88
- color: var(--dx-g-gray-10);
89
- outline: none;
90
- transition: var(--dx-g-transition-hue-1x), box-shadow 0.15s linear;
91
- }
92
-
93
- /* Hover only (not open): 2px #032D60 on #EEF4FF, no ring.
94
- Border color matches the fill so the 1px border does not add a third
95
- pixel around the inset stroke.
96
- :focus-visible is included so click-open + Escape (which upgrades the
97
- trigger to keyboard focus) still looks like hover while the pointer is on
98
- the picker. */
99
- .version-picker-button:hover:not([aria-expanded="true"])::part(container),
100
- .version-picker-button:hover:not([aria-expanded="true"])::part(
101
- container
102
- ):focus-visible {
103
- background: var(--dx-g-blue-vibrant-95);
104
- border: 1px solid var(--dx-g-blue-vibrant-95);
105
- box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20);
106
- }
107
-
108
- /* Open keeps the fill and the 2px stroke. The focus ring is keyboard-only,
109
- on the closed picker below. */
110
- .version-picker-button[aria-expanded="true"]::part(container) {
111
- background: var(--dx-g-blue-vibrant-95);
112
- border: 1px solid var(--dx-g-blue-vibrant-95);
113
- border-radius: var(--dx-g-spacing-xs);
114
- box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20);
115
- }
116
-
117
- /* Keyboard focus while closed. */
118
- .version-picker-button:not([aria-expanded="true"])::part(
119
- container
120
- ):focus-visible {
121
- --dx-g-button-icon-color: var(--dx-g-blue-vibrant-20);
122
-
123
- background: var(--dx-g-blue-vibrant-95);
124
- border: 1px solid var(--dx-g-blue-vibrant-95);
125
- border-radius: var(--dx-g-spacing-xs);
126
- box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20),
127
- var(--dx-c-version-picker-focus-ring);
128
- }
129
-
130
38
  .selected-version {
131
39
  display: flex;
132
40
  flex-direction: row;
@@ -141,111 +49,16 @@ dx-button::part(content) {
141
49
  white-space: nowrap;
142
50
  }
143
51
 
144
- dx-type-badge.latest-badge,
145
- dx-type-badge.not-latest-badge {
146
- margin-left: var(--dx-g-spacing-sm);
147
- }
148
-
149
- /* ------------------------------------------------------------------ *
150
- * Small "dot" variant (opt-in via `small`). Additive
151
- * ------------------------------------------------------------------ */
152
-
153
- /* Menu is capped at the ToC width (296px), not the 104px trigger, so options
154
- keep the same 16px / 8px 24px / gray-10 type as the default picker. */
155
- .version-picker-dropdown-small {
156
- --dx-c-dropdown-option-font-size: var(--dx-g-text-base);
157
- --dx-c-dropdown-option-padding: var(--dx-g-spacing-sm)
158
- var(--dx-g-spacing-lg);
159
- --dx-c-dropdown-option-label-color: var(--dx-g-gray-10);
160
- --dx-c-dropdown-option-border-radius: 0;
161
- --dx-c-popover-border: none;
162
-
163
- /* Hug the labels (down to the shortest) but never wider than the ToC
164
- content: 296px on desktop, and on the smallest breakpoint the sidebar
165
- variable grows with the viewport up to 720px. Long labels wrap inside
166
- that cap. */
167
- --dx-c-dropdown-menu-max-width: var(--doc-version-picker-width, 296px);
168
- --dx-c-dropdown-fit-white-space: normal;
169
- --dx-c-dropdown-fit-overflow: hidden;
170
- --dx-c-dropdown-fit-text-overflow: clip;
171
- --dx-c-dropdown-option-overflow-wrap: anywhere;
172
- }
173
-
174
- .version-picker-button-small {
175
- width: fit-content;
176
- max-width: 104px;
177
-
178
- --dx-c-button-font-size: var(--dx-g-text-xs);
179
- --dx-c-button-font-weight: var(--dx-g-font-normal);
180
- --dx-c-button-line-height: var(--dx-g-spacing-lg);
181
- --dx-c-button-icon-gap: var(--dx-g-spacing-2xs);
182
- }
52
+ dx-type-badge.latest-badge {
53
+ --dx-c-type-badge-color: var(--dx-g-green-vibrant-40);
54
+ --dx-c-type-badge-background: var(--dx-g-green-vibrant-95);
183
55
 
184
- /* min-width: 0 on each flex ancestor lets the label truncate: a flex item's
185
- default min-width: auto refuses to shrink below its content and overrides the
186
- host's max-width, so without this the trigger overflows instead. */
187
- .version-picker-button-small::part(content) {
188
- display: flex;
189
- flex: 1;
190
- min-width: 0;
191
- width: auto;
192
- overflow: hidden;
193
- }
194
-
195
- /* width:100% re-ties to the 104px-capped host (container is width:inherit,
196
- which copies width but not max-width). Border/bg/states are shared above. */
197
- .version-picker-button-small::part(container) {
198
- width: 100%;
199
- height: var(--dx-g-spacing-lg);
200
- padding: 0 var(--dx-g-spacing-xs);
201
- }
202
-
203
- .selected-version-small {
204
- flex: 1;
205
- min-width: 0;
206
- }
207
-
208
- .version-picker-dot {
209
- flex: 0 0 auto;
210
- width: var(--dx-g-spacing-sm);
211
- height: var(--dx-g-spacing-sm);
212
- margin-right: var(--dx-g-spacing-xs);
213
- border-radius: 50%;
214
- }
215
-
216
- .version-picker-dot-latest {
217
- background: var(--dx-g-green-vibrant-60);
218
- }
219
-
220
- .version-picker-dot-not-latest {
221
- background: var(--dx-g-yellow-vibrant-80);
56
+ margin-left: var(--dx-g-spacing-sm);
222
57
  }
223
58
 
224
- /* inline-block (not flex) so the label can truncate; line-height centers it. */
225
- .version-picker-readonly {
226
- box-sizing: border-box;
227
- display: inline-block;
228
- width: 100%;
229
- height: var(--dx-g-spacing-xl);
230
- margin: 0;
231
- padding: 0 var(--dx-g-spacing-sm);
232
- border: 1px solid var(--dx-g-gray-80);
233
- border-radius: var(--dx-g-spacing-xs);
234
- overflow: hidden;
235
- color: var(--dx-g-gray-10);
236
- text-overflow: ellipsis;
237
- white-space: nowrap;
238
- font: var(--dx-g-font-normal) var(--dx-g-text-sm) / var(--dx-g-spacing-xl)
239
- var(--dx-g-font-sans),
240
- sans-serif;
241
- }
59
+ dx-type-badge.not-latest-badge {
60
+ --dx-c-type-badge-color: var(--dx-g-red-vibrant-40);
61
+ --dx-c-type-badge-background: var(--dx-g-red-vibrant-95);
242
62
 
243
- .version-picker-readonly-small {
244
- width: fit-content;
245
- max-width: 104px;
246
- height: var(--dx-g-spacing-lg);
247
- padding: 0 var(--dx-g-spacing-xs);
248
- font: var(--dx-g-font-normal) var(--dx-g-text-xs) / var(--dx-g-spacing-lg)
249
- var(--dx-g-font-sans),
250
- sans-serif;
63
+ margin-left: var(--dx-g-spacing-sm);
251
64
  }
@@ -1,70 +1,37 @@
1
1
  <template>
2
- <div lwc:if={showVersionPicker} class={containerClass}>
3
- <!-- Small read-only: plain value, no dropdown -->
4
- <p
5
- lwc:if={readOnly}
6
- class={readOnlyClass}
7
- title={selectedVersion.label}
8
- >
9
- {selectedVersion.label}
10
- </p>
11
-
2
+ <div lwc:if={showVersionPicker} class="version-picker-container">
12
3
  <dx-dropdown
13
- lwc:else
14
- class={dropdownClass}
15
4
  options={versions}
16
5
  analytics-event="custEv_docVersionSelect"
17
6
  analytics-payload={analyticsPayload}
18
7
  value={selectedVersion.id}
19
- width-mode={dropdownWidthMode}
8
+ width="var(--doc-version-picker-width)"
20
9
  onchange={onVersionChange}
21
10
  >
22
11
  <dx-button
23
- class={triggerClass}
12
+ class="version-picker-button"
24
13
  variant="tertiary"
25
14
  size="small"
26
- font="sans"
27
15
  icon-symbol="chevrondown"
28
- icon-size={triggerIconSize}
29
- aria-label={triggerAriaLabel}
16
+ icon-size="medium"
30
17
  >
31
- <div class={selectedVersionClass}>
32
- <!-- Small: leading colored dot -->
33
- <template lwc:if={small}>
34
- <template lwc:if={showLatestTag}>
35
- <span class={dotClass} aria-hidden="true"></span>
36
- </template>
37
- <p
38
- class="selected-version-label"
39
- title={selectedVersion.label}
40
- >
41
- {selectedVersion.label}
42
- </p>
43
- </template>
44
- <!-- Default: trailing Latest/Not-Latest badge -->
45
- <template lwc:else>
46
- <p
47
- class="selected-version-label"
48
- title={selectedVersion.label}
49
- >
50
- {selectedVersion.label}
51
- </p>
52
- <template lwc:if={showLatestTag}>
53
- <dx-type-badge
54
- class="latest-badge"
55
- lwc:if={latestVersion}
56
- variant="status-success"
57
- value="Latest"
58
- size="small"
59
- ></dx-type-badge>
60
- <dx-type-badge
61
- class="not-latest-badge"
62
- lwc:else
63
- variant="status-warning"
64
- value="Not Latest"
65
- size="small"
66
- ></dx-type-badge>
67
- </template>
18
+ <div class="selected-version">
19
+ <p class="selected-version-label">
20
+ {selectedVersion.label}
21
+ </p>
22
+ <template lwc:if={showLatestTag}>
23
+ <dx-type-badge
24
+ class="latest-badge"
25
+ lwc:if={latestVersion}
26
+ value="Latest"
27
+ size="small"
28
+ ></dx-type-badge>
29
+ <dx-type-badge
30
+ class="not-latest-badge"
31
+ lwc:else
32
+ value="Not Latest"
33
+ size="small"
34
+ ></dx-type-badge>
68
35
  </template>
69
36
  </div>
70
37
  </dx-button>
@@ -1,5 +1,4 @@
1
1
  import { LightningElement, api, track } from "lwc";
2
- import cx from "classnames";
3
2
 
4
3
  import { AnalyticsPayload, OptionWithNested } from "typings/custom";
5
4
 
@@ -13,8 +12,6 @@ export default class VersionPicker extends LightningElement {
13
12
  private _selectedVersion?: OptionWithNested;
14
13
  private _latestVersion: boolean = false;
15
14
  private _hideBadge: boolean = false;
16
- private _small: boolean = false;
17
- private _readOnly: boolean = false;
18
15
 
19
16
  @api
20
17
  get versions() {
@@ -54,103 +51,14 @@ export default class VersionPicker extends LightningElement {
54
51
  this._hideBadge = normalizeBoolean(value);
55
52
  }
56
53
 
57
- // Opt-in compact "dot" variant (24px, abbreviated label). Off by default so
58
- // existing consumers render the default trigger unchanged.
59
- @api
60
- get small() {
61
- return this._small;
62
- }
63
-
64
- set small(value) {
65
- this._small = normalizeBoolean(value);
66
- }
67
-
68
- // Opt-in read-only state: plain value, no dropdown menu.
69
- @api
70
- get readOnly() {
71
- return this._readOnly;
72
- }
73
-
74
- set readOnly(value) {
75
- this._readOnly = normalizeBoolean(value);
76
- }
77
-
78
54
  private get showVersionPicker() {
79
55
  return this._versions && this._versions.length !== 0;
80
56
  }
81
57
 
82
- private get containerClass(): string {
83
- return this.small
84
- ? "version-picker-container-small"
85
- : "version-picker-container";
86
- }
87
-
88
58
  private get showLatestTag(): boolean {
89
59
  return !this.hideBadge;
90
60
  }
91
61
 
92
- private get readOnlyClass(): string {
93
- return cx(
94
- "version-picker-readonly",
95
- this.small && "version-picker-readonly-small"
96
- );
97
- }
98
-
99
- private get dotClass(): string {
100
- return `version-picker-dot ${
101
- this.latestVersion
102
- ? "version-picker-dot-latest"
103
- : "version-picker-dot-not-latest"
104
- }`;
105
- }
106
-
107
- // Accessible name for the small trigger: includes the latest state so it
108
- // isn't conveyed by the dot's color alone (WCAG 1.4.1). Omits the state
109
- // when the indicator is hidden.
110
- private get smallTriggerAriaLabel(): string {
111
- const label = this.selectedVersion?.label ?? "";
112
- if (!this.showLatestTag) {
113
- return label;
114
- }
115
- return `${label}, ${this.latestVersion ? "Latest" : "Not Latest"}`;
116
- }
117
-
118
- // --- Per-variant values for the shared dropdown/button markup below. The
119
- // small variant opts into extra classes and a compact chevron.
120
-
121
- // Not cx(): the default must have NO class attribute (cx returns "", which
122
- // would render class="" and break the frozen consumer snapshots).
123
- private get dropdownClass(): string | undefined {
124
- return this.small ? "version-picker-dropdown-small" : undefined;
125
- }
126
-
127
- // "full" matches the trigger (ToC width). "fit-end" sizes to the labels,
128
- // caps at the ToC width, and opens inward.
129
- private get dropdownWidthMode(): "full" | "fit-end" {
130
- return this.small ? "fit-end" : "full";
131
- }
132
-
133
- private get triggerClass(): string {
134
- return cx(
135
- "version-picker-button",
136
- this.small && "version-picker-button-small"
137
- );
138
- }
139
-
140
- private get triggerIconSize(): string {
141
- return this.small ? "xsmall" : "medium";
142
- }
143
-
144
- // Default returns "" (dx-button's own default) so the trigger's rendered
145
- // aria-label is unchanged for the default variant.
146
- private get triggerAriaLabel(): string {
147
- return this.small ? this.smallTriggerAriaLabel : "";
148
- }
149
-
150
- private get selectedVersionClass(): string {
151
- return cx("selected-version", this.small && "selected-version-small");
152
- }
153
-
154
62
  private onVersionChange(e: CustomEvent) {
155
63
  this.dispatchEvent(new CustomEvent("change", { detail: e.detail }));
156
64
  }
@@ -1,28 +1,10 @@
1
1
  import { CoveoAnalyticsClient } from "coveo.analytics/dist/browser.mjs"; // This fix is required for Node 20 upgrade, so that Coveo analytics is loaded properly by LWR.
2
2
 
3
- // Resolve against the current origin (version links are usually relative) and
4
- // keep only http/https. Blocks executable schemes like javascript:/data: that
5
- // would run when the innerHTML-rendered link is clicked
6
- // encodeURI then keeps the value from breaking out of the href attribute.
7
- const toSafeHref = (link: string): string => {
8
- try {
9
- const { protocol } = new URL(link, window.location.origin);
10
- if (protocol === "http:" || protocol === "https:") {
11
- return encodeURI(link);
12
- }
13
- } catch {
14
- // Fall through to the rejection below.
15
- }
16
- console.warn(`Blocked unsafe version link: ${link}`);
17
- return "#";
18
- };
19
-
20
3
  export const oldVersionDocInfo = (latestVersionLink: string) => {
21
- const safeLink = toSafeHref(latestVersionLink);
22
4
  return {
23
5
  title: "Newer Version Available",
24
- body: `This content describes an older version of this product.
25
- <a style="font-weight: bold;" href="${safeLink}">View Latest</a>`
6
+ body: `This content describes an older version of this product.
7
+ <a style="font-weight: bold;" href="${latestVersionLink}">View Latest</a>`
26
8
  };
27
9
  };
28
10