@salesforcedevs/docs-components 1.3.344-spage → 1.3.345-refactor-tab-alpha

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/lwc.config.json CHANGED
@@ -13,6 +13,7 @@
13
13
  "doc/contentLayout",
14
14
  "doc/contentMedia",
15
15
  "doc/docXmlContent",
16
+ "doc/lwcContentLayout",
16
17
  "doc/header",
17
18
  "doc/heading",
18
19
  "doc/headingAnchor",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/docs-components",
3
- "version": "1.3.344-spage",
3
+ "version": "1.3.345-refactor-tab-alpha",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -1,131 +1 @@
1
- :host {
2
- --dx-c-content-vertical-spacing: var(--dx-g-spacing-lg);
3
- --dx-c-content-sidebar-sticky-top: calc(
4
- var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
5
- );
6
- --dx-c-sidebar-height: calc(
7
- 100vh -
8
- calc(
9
- var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
10
- )
11
- );
12
- --dx-c-content-scroll-margin-top: calc(
13
- var(--dx-g-global-header-height) + var(--dx-g-doc-header-height) +
14
- var(--dx-g-spacing-2xl)
15
- );
16
-
17
- display: block;
18
- }
19
-
20
- doc-breadcrumbs {
21
- --dx-c-popover-z-index: 5;
22
-
23
- display: block;
24
- margin-bottom: var(--dx-g-spacing-2xl);
25
- }
26
-
27
- dx-sidebar,
28
- dx-sidebar-old {
29
- --dx-c-sidebar-vertical-padding: var(--dx-g-spacing-md);
30
-
31
- z-index: calc(var(--dx-g-z-index-100) + 5);
32
- }
33
-
34
- dx-toc {
35
- --dx-c-toc-width: unset;
36
-
37
- height: calc(100% - var(--dx-c-content-vertical-spacing) * 2);
38
- margin: var(--dx-c-content-vertical-spacing) 0;
39
- overflow-y: auto;
40
- }
41
-
42
- dx-sidebar,
43
- dx-toc {
44
- display: block;
45
- }
46
-
47
- /* offset page jump link due to fixed header */
48
- ::slotted(doc-heading) {
49
- scroll-margin-top: var(--dx-c-content-scroll-margin-top);
50
- }
51
-
52
- .content {
53
- display: flex;
54
- position: relative;
55
- }
56
-
57
- .content-body-doc-phase-container {
58
- flex: 1;
59
- border-left: 1px solid var(--dx-g-gray-90);
60
- }
61
-
62
- .content-body-container {
63
- display: flex;
64
- flex-direction: row;
65
- justify-content: center;
66
- max-width: var(--dx-g-doc-content-max-width);
67
-
68
- /* Derived this manually by substracting (topHeader, doc header, banner and the content). */
69
- min-height: 62vh;
70
- margin: auto;
71
- padding: 0 var(--dx-g-global-header-padding-horizontal);
72
- margin-bottom: calc(2 * (var(--dx-g-spacing-5xl) + 4px));
73
- }
74
-
75
- .content-body {
76
- margin: var(--dx-g-spacing-md) 0 0;
77
- max-width: 900px;
78
- flex: 1;
79
- width: 0;
80
- }
81
-
82
- .is-sticky {
83
- align-self: flex-start;
84
- position: sticky;
85
- top: var(--dx-c-content-sidebar-sticky-top);
86
- }
87
-
88
- .right-nav-bar {
89
- margin-left: var(--dx-g-spacing-2xl);
90
- }
91
-
92
- @media screen and (max-width: 1024px) {
93
- .right-nav-bar {
94
- display: none;
95
- }
96
- }
97
-
98
- @media screen and (max-width: 800px) {
99
- .content-body {
100
- margin-top: var(--dx-c-content-vertical-spacing);
101
- }
102
-
103
- .content-body-doc-phase-container {
104
- border-left: 0;
105
- }
106
- }
107
-
108
- @media screen and (max-width: 768px) {
109
- .is-sticky {
110
- width: 100%;
111
- }
112
-
113
- .content {
114
- flex-direction: column;
115
- }
116
-
117
- .content-body-container {
118
- padding-right: 0;
119
- overflow-x: auto;
120
- margin-bottom: calc(var(--dx-g-spacing-5xl) + 4px);
121
- }
122
-
123
- .left-nav-bar {
124
- height: unset;
125
- }
126
-
127
- .content-body {
128
- margin-left: var(--dx-g-spacing-mlg, 20px);
129
- margin-right: var(--dx-g-spacing-mlg, 20px);
130
- }
131
- }
1
+ @import "docHelpers/contentLayoutStyle";
@@ -11,7 +11,7 @@ type AnchorMap = { [key: string]: { intersect: boolean; id: string } };
11
11
  declare const Sprig: (eventType: string, eventNme: string) => void;
12
12
 
13
13
  const TOC_HEADER_TAG = "doc-heading";
14
- const RNB_BY_TAB = "docs-tab";
14
+
15
15
  const HIGHLIGHTABLE_SELECTOR = [
16
16
  "p",
17
17
  "h1",
@@ -83,87 +83,26 @@ export default class ContentLayout extends LightningElement {
83
83
  }
84
84
 
85
85
  @track
86
- private _sidebarContent: unknown;
86
+ protected _sidebarContent: unknown;
87
87
 
88
- private _breadcrumbs = null;
88
+ protected _breadcrumbs = null;
89
89
 
90
90
  @track
91
- private _tocOptions!: Array<unknown>;
91
+ protected _tocOptions!: Array<unknown>;
92
92
 
93
- private tocOptionIdsSet = new Set();
94
- private anchoredElements: AnchorMap = {};
95
- private lastScrollPosition!: number;
96
- private observer?: IntersectionObserver;
97
- private hasRendered: boolean = false;
98
- private contentLoaded: boolean = false;
99
- private sidebarOpen: boolean = false;
100
- private rnbByTab: boolean = false;
93
+ protected tocOptionIdsSet = new Set();
94
+ protected anchoredElements: AnchorMap = {};
95
+ protected lastScrollPosition!: number;
96
+ protected observer?: IntersectionObserver;
97
+ protected hasRendered: boolean = false;
98
+ protected contentLoaded: boolean = false;
99
+ protected sidebarOpen: boolean = false;
101
100
 
102
101
  get shouldDisplayFeedback() {
103
102
  return this.contentLoaded && typeof Sprig !== "undefined";
104
103
  }
105
104
 
106
- private setRNBByTab() {
107
- const tabPanelListItem: any = this.getTabPanelList();
108
- this.rnbByTab = tabPanelListItem?.id === RNB_BY_TAB ? true : false;
109
- }
110
-
111
- get showTabBasedRNB() {
112
- return this.rnbByTab;
113
- }
114
-
115
- onTabChanged = () => {
116
- this.updateRNB();
117
- };
118
-
119
- private getTabPanelList() {
120
- return document.querySelector("dx-tab-panel-list");
121
- }
122
-
123
- updateRNB = () => {
124
- const headingElements = this.getHeadingElements();
125
- headingElements.forEach((headingElement: any) => {
126
- // Sometimes elements hash and header is not being set when slot content is wrapped with div
127
- headingElement.hash = headingElement.attributes.hash?.nodeValue;
128
- headingElement.header = headingElement.attributes.header?.nodeValue;
129
- });
130
- this.updateTocItems(headingElements);
131
- };
132
-
133
- private getHeadingElements() {
134
- let headingElements = document.querySelectorAll(TOC_HEADER_TAG);
135
- if (this.showTabBasedRNB) {
136
- const tabPanelListItem: any = this.getTabPanelList();
137
- const tabPanels =
138
- tabPanelListItem?.querySelectorAll("dx-tab-panel");
139
- for (const tabPanelItem of tabPanels) {
140
- if (tabPanelItem.active) {
141
- headingElements =
142
- tabPanelItem.querySelectorAll(TOC_HEADER_TAG);
143
- break;
144
- }
145
- }
146
- }
147
- return headingElements;
148
- }
149
-
150
- private updateURL() {
151
- const tabs = this.getAllTabs();
152
- const selectedTabId = this.getSelectedTabId();
153
- tabs.forEach((tab: any) => {
154
- if (tab.getAttribute("aria-selected") === "true") {
155
- const tabID = tab.getAttribute("aria-label");
156
- const url = new URL(window.location.href);
157
- if (selectedTabId !== tabID) {
158
- url.searchParams.set("type", tabID);
159
- url.hash = "";
160
- window.history.pushState({}, "", url.toString());
161
- }
162
- }
163
- });
164
- }
165
-
166
- private searchSyncer = new SearchSyncer({
105
+ protected searchSyncer = new SearchSyncer({
167
106
  callbacks: {
168
107
  onSearchChange: (nextSearchString: string): void => {
169
108
  this.dispatchHighlightChange(
@@ -177,11 +116,11 @@ export default class ContentLayout extends LightningElement {
177
116
  shouldStopPropagation: true,
178
117
  target: window
179
118
  });
180
- private tocValue?: string = undefined;
119
+ protected tocValue?: string = undefined;
181
120
  // eslint-disable-next-line no-undef
182
- private observerTimerId?: NodeJS.Timeout;
183
- private didScrollToSelectedHash = false;
184
- private _scrollInterval = 0;
121
+ protected observerTimerId?: NodeJS.Timeout;
122
+ protected didScrollToSelectedHash = false;
123
+ protected _scrollInterval = 0;
185
124
 
186
125
  get showToc(): boolean {
187
126
  return this.tocOptions && this.tocOptions.length > 0;
@@ -198,13 +137,6 @@ export default class ContentLayout extends LightningElement {
198
137
  );
199
138
  }
200
139
 
201
- // This event gets triggered when navigating back/forward
202
- handlePopState = (): void => {
203
- if (this.showTabBasedRNB) {
204
- this.restoreTabSelection();
205
- }
206
- };
207
-
208
140
  connectedCallback(): void {
209
141
  const hasParentHighlightListener = closest(
210
142
  "doc-xml-content",
@@ -217,45 +149,6 @@ export default class ContentLayout extends LightningElement {
217
149
  );
218
150
  this.searchSyncer.init();
219
151
  }
220
- window.addEventListener("popstate", this.handlePopState);
221
- }
222
-
223
- private getSelectedTabId() {
224
- const urlParams = new URLSearchParams(window.location.search);
225
- const selectedTabId = urlParams.get("type");
226
- return selectedTabId;
227
- }
228
-
229
- private restoreTabSelection() {
230
- requestAnimationFrame(() => {
231
- const selectedTabId = this.getSelectedTabId();
232
- if (selectedTabId) {
233
- this.selectTabById(selectedTabId);
234
- }
235
- });
236
- }
237
-
238
- private getAllTabs(): any[] {
239
- const tabPanelListItem: any = this.getTabPanelList();
240
- if (tabPanelListItem?.shadowRoot) {
241
- return Array.from(
242
- tabPanelListItem.shadowRoot.querySelectorAll(
243
- "dx-tab-panel-item"
244
- )
245
- ).map((tabPanelItem: any) =>
246
- tabPanelItem.shadowRoot.querySelector("button")
247
- );
248
- }
249
- return [];
250
- }
251
-
252
- private selectTabById(tabId: string) {
253
- const tabs = this.getAllTabs();
254
- tabs.forEach((tab: any) => {
255
- if (tab.getAttribute("aria-label") === tabId) {
256
- tab.click();
257
- }
258
- });
259
152
  }
260
153
 
261
154
  renderedCallback(): void {
@@ -275,11 +168,6 @@ export default class ContentLayout extends LightningElement {
275
168
 
276
169
  if (!this.hasRendered) {
277
170
  this.hasRendered = true;
278
- this.setRNBByTab();
279
- if (this.showTabBasedRNB) {
280
- window.addEventListener("tabchanged", this.onTabChanged);
281
- this.restoreTabSelection();
282
- }
283
171
  this.restoreScroll();
284
172
  }
285
173
  }
@@ -292,8 +180,6 @@ export default class ContentLayout extends LightningElement {
292
180
  );
293
181
  window.removeEventListener("scroll", this.adjustNavPosition);
294
182
  window.removeEventListener("resize", this.adjustNavPosition);
295
- window.removeEventListener("tabchanged", this.onTabChanged);
296
- window.removeEventListener("popstate", this.handlePopState);
297
183
  this.searchSyncer.dispose();
298
184
  this.clearRenderObserverTimer();
299
185
 
@@ -439,12 +325,7 @@ export default class ContentLayout extends LightningElement {
439
325
  );
440
326
 
441
327
  // Note: We are doing document.querySelectorAll as a quick fix as we are not getting heading elements reference this.querySelectorAll
442
- const headingElements = this.getHeadingElements();
443
-
444
- // We only need to update URL in case of /docs and ignore if tabs are used anywhere else in DSC
445
- if (this.showTabBasedRNB) {
446
- this.updateURL();
447
- }
328
+ const headingElements = document.querySelectorAll(TOC_HEADER_TAG);
448
329
 
449
330
  for (const headingElement of headingElements as any) {
450
331
  // Add headingElements to intersectionObserver for highlighting respective RNB item when user scroll
@@ -462,38 +343,51 @@ export default class ContentLayout extends LightningElement {
462
343
  }
463
344
  };
464
345
 
465
- onSlotChange(): void {
466
- this.updateRNB();
467
- this.contentLoaded = true;
468
- }
346
+ onSlotChange(event: Event): void {
347
+ const slotElements = (
348
+ event.target as HTMLSlotElement
349
+ ).assignedElements();
350
+
351
+ if (slotElements.length) {
352
+ this.contentLoaded = true;
353
+ const slotContentElement = slotElements[0];
354
+ const headingElements =
355
+ slotContentElement.ownerDocument?.getElementsByTagName(
356
+ TOC_HEADER_TAG
357
+ );
469
358
 
470
- // eslint-disable-next-line no-undef
471
- private updateTocItems(headingElements: NodeListOf<Element>): void {
472
- const tocOptions = [];
359
+ for (const headingElement of headingElements as any) {
360
+ // Sometimes elements hash and header is not being set when slot content is wrapped with div
361
+ headingElement.hash = headingElement.attributes.hash?.nodeValue;
362
+ headingElement.header =
363
+ headingElement.attributes.header?.nodeValue;
364
+ }
473
365
 
474
- for (const headingElement of headingElements as any) {
475
- headingElement.id = headingElement.hash;
476
-
477
- // Update tocOptions from anchorTags only for H2, consider default as 2 as per component
478
- const headingAriaLevel =
479
- headingElement.attributes["aria-level"]?.nodeValue || "2";
480
- const isH2 = headingAriaLevel === "2";
481
-
482
- if (isH2) {
483
- const tocItem = {
484
- anchor: `#${headingElement.hash}`,
485
- id: headingElement.id,
486
- label: headingElement.header
487
- };
488
- tocOptions.push(tocItem);
489
- this.tocOptionIdsSet.add(headingElement.id);
366
+ const tocOptions = [];
367
+ for (const headingElement of headingElements as any) {
368
+ headingElement.id = headingElement.hash;
369
+
370
+ // Update tocOptions from anchorTags only for H2, consider default as 2 as per component
371
+ const headingAriaLevel =
372
+ headingElement.attributes["aria-level"]?.nodeValue || "2";
373
+ const isH2 = headingAriaLevel === "2";
374
+
375
+ if (isH2) {
376
+ const tocItem = {
377
+ anchor: `#${headingElement.hash}`,
378
+ id: headingElement.id,
379
+ label: headingElement.header
380
+ };
381
+ tocOptions.push(tocItem);
382
+ this.tocOptionIdsSet.add(headingElement.id);
383
+ }
490
384
  }
491
- }
492
385
 
493
- this._tocOptions = tocOptions;
386
+ this._tocOptions = tocOptions;
387
+ }
494
388
  }
495
389
 
496
- private disconnectObserver(): void {
390
+ protected disconnectObserver(): void {
497
391
  if (this.observer) {
498
392
  this.observer.disconnect();
499
393
  this.observer = undefined;
@@ -501,7 +395,7 @@ export default class ContentLayout extends LightningElement {
501
395
  }
502
396
 
503
397
  // eslint-disable-next-line no-undef
504
- private scrollToHash(headingElements: NodeListOf<Element>): void {
398
+ protected scrollToHash(headingElements: NodeListOf<Element>): void {
505
399
  let { hash } = window.location;
506
400
  if (hash) {
507
401
  hash = hash.substr(1);
@@ -538,7 +432,7 @@ export default class ContentLayout extends LightningElement {
538
432
  }
539
433
  }
540
434
 
541
- private scrollIntoViewWithOffset(
435
+ protected scrollIntoViewWithOffset(
542
436
  headingElement: HTMLElement,
543
437
  offset: number
544
438
  ) {
@@ -551,7 +445,7 @@ export default class ContentLayout extends LightningElement {
551
445
  });
552
446
  }
553
447
 
554
- private calculateActualSection(): void {
448
+ protected calculateActualSection(): void {
555
449
  const currentScrollPosition = document.documentElement.scrollTop;
556
450
  const id = Object.keys(this.anchoredElements).find(
557
451
  (_id) => this.anchoredElements[_id].intersect
@@ -566,21 +460,21 @@ export default class ContentLayout extends LightningElement {
566
460
  this.lastScrollPosition = currentScrollPosition;
567
461
  }
568
462
 
569
- private calculatePreviousElementId(): string | undefined {
463
+ protected calculatePreviousElementId(): string | undefined {
570
464
  const keys = Object.keys(this.anchoredElements);
571
465
  const currentIndex = keys.findIndex((id) => this.tocValue === id);
572
466
 
573
467
  return currentIndex > 0 ? keys[currentIndex - 1] : undefined;
574
468
  }
575
469
 
576
- private assignElementId(id: string | undefined): void {
470
+ protected assignElementId(id: string | undefined): void {
577
471
  // Change toc(RNB) highlight only for H2
578
472
  if (this.tocOptionIdsSet.has(id)) {
579
473
  this.tocValue = id;
580
474
  }
581
475
  }
582
476
 
583
- private dispatchHighlightChange(term: string): void {
477
+ protected dispatchHighlightChange(term: string): void {
584
478
  this.dispatchEvent(
585
479
  new CustomEvent("highlightedtermchange", {
586
480
  detail: term,
@@ -590,14 +484,14 @@ export default class ContentLayout extends LightningElement {
590
484
  );
591
485
  }
592
486
 
593
- private updateHighlightsAndSearch(nextSearchString: string): void {
487
+ protected updateHighlightsAndSearch(nextSearchString: string): void {
594
488
  const nextSearchParam =
595
489
  new URLSearchParams(nextSearchString).get("q") || "";
596
490
  this.setSidebarInputValue(nextSearchParam);
597
491
  this.dispatchHighlightChange(nextSearchParam);
598
492
  }
599
493
 
600
- private onToggleSidebar(e: CustomEvent): void {
494
+ protected onToggleSidebar(e: CustomEvent): void {
601
495
  this.sidebarOpen = e.detail.open;
602
496
 
603
497
  // eslint-disable-next-line @lwc/lwc/no-document-query
@@ -0,0 +1 @@
1
+ @import "docHelpers/contentLayoutStyle";
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <div class="content">
3
+ <template lwc:if={useOldSidebar}>
4
+ <dx-sidebar-old
5
+ class="is-sticky left-nav-bar"
6
+ trees={sidebarContent}
7
+ value={sidebarValue}
8
+ header={sidebarHeader}
9
+ ontogglesidebar={onToggleSidebar}
10
+ languages={languages}
11
+ language={language}
12
+ bail-href={bailHref}
13
+ bail-label={bailLabel}
14
+ >
15
+ <slot name="sidebar-header" slot="version-picker"></slot>
16
+ </dx-sidebar-old>
17
+ </template>
18
+ <template lwc:else>
19
+ <dx-sidebar
20
+ class="is-sticky left-nav-bar"
21
+ trees={sidebarContent}
22
+ value={sidebarValue}
23
+ header={sidebarHeader}
24
+ coveo-organization-id={coveoOrganizationId}
25
+ coveo-public-access-token={coveoPublicAccessToken}
26
+ coveo-search-hub={coveoSearchHub}
27
+ coveo-advanced-query-config={coveoAdvancedQueryConfig}
28
+ ontogglesidebar={onToggleSidebar}
29
+ languages={languages}
30
+ language={language}
31
+ bail-href={bailHref}
32
+ bail-label={bailLabel}
33
+ >
34
+ <slot name="sidebar-header" slot="version-picker"></slot>
35
+ </dx-sidebar>
36
+ </template>
37
+ <div class="content-body-doc-phase-container">
38
+ <slot name="doc-phase"></slot>
39
+ <slot name="version-banner"></slot>
40
+ <div class="content-body-container">
41
+ <div class="content-body">
42
+ <doc-breadcrumbs
43
+ lwc:if={showBreadcrumbs}
44
+ breadcrumbs={breadcrumbs}
45
+ ></doc-breadcrumbs>
46
+ <slot onslotchange={onSlotChange}></slot>
47
+ <doc-sprig-survey
48
+ lwc:if={shouldDisplayFeedback}
49
+ ></doc-sprig-survey>
50
+ </div>
51
+ <div lwc:if={showToc} class="right-nav-bar is-sticky">
52
+ <dx-toc
53
+ header={tocTitle}
54
+ options={tocOptions}
55
+ value={tocValue}
56
+ ></dx-toc>
57
+ </div>
58
+ </div>
59
+ <div lwc:if={showFooter} class="footer-container">
60
+ <dx-footer variant="no-signup"></dx-footer>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </template>
@@ -0,0 +1,277 @@
1
+ /* eslint-disable @lwc/lwc/no-document-query */
2
+ import { closest } from "kagekiri";
3
+ import ContentLayout from "doc/contentLayout";
4
+
5
+ const TOC_HEADER_TAG = "doc-heading";
6
+ const RNB_BY_TAB = "docs-tab";
7
+ const SPECIFICATION_TAB_TITLE = "Specification";
8
+ export const OBSERVER_ATTACH_WAIT_TIME = 500;
9
+
10
+ export default class LwcContentLayout extends ContentLayout {
11
+ private rnbByTab: boolean = false;
12
+
13
+ private setRNBByTab() {
14
+ const tabPanelListItem: any = this.getTabPanelList();
15
+ this.rnbByTab = tabPanelListItem?.id === RNB_BY_TAB ? true : false;
16
+ }
17
+
18
+ get showTabBasedRNB() {
19
+ return this.rnbByTab;
20
+ }
21
+
22
+ onRNBClick = (event: CustomEvent) => {
23
+ event.stopPropagation();
24
+ const currentTab = this.getSelectedTabId();
25
+ if (currentTab === SPECIFICATION_TAB_TITLE) {
26
+ this.didScrollToSelectedHash = false;
27
+ }
28
+ };
29
+
30
+ onTabChanged = () => {
31
+ this.updateRNB();
32
+ };
33
+
34
+ private getTabPanelList() {
35
+ return document.querySelector("dx-tab-panel-list");
36
+ }
37
+
38
+ updateRNB = () => {
39
+ const headingElements = this.getHeadingElements();
40
+ headingElements.forEach((headingElement: any) => {
41
+ // Sometimes elements hash and header is not being set when slot content is wrapped with div
42
+ if (!headingElement.hash) {
43
+ headingElement.hash = headingElement.attributes.hash?.nodeValue;
44
+ }
45
+
46
+ if (!headingElement.header) {
47
+ headingElement.header =
48
+ headingElement.attributes.header?.nodeValue;
49
+ }
50
+ });
51
+ this.updateTocItems(headingElements);
52
+ };
53
+
54
+ private getHeadingElements() {
55
+ let headingElements = document.querySelectorAll(TOC_HEADER_TAG);
56
+ if (this.showTabBasedRNB) {
57
+ const tabPanelListItem: any = this.getTabPanelList();
58
+ const tabPanels =
59
+ tabPanelListItem?.querySelectorAll("dx-tab-panel");
60
+ for (const tabPanelItem of tabPanels) {
61
+ if (tabPanelItem.active) {
62
+ // This is needed for Specification tab content
63
+ const specificationElement = tabPanelItem.querySelector(
64
+ "doc-specification-content"
65
+ );
66
+ if (specificationElement) {
67
+ headingElements =
68
+ specificationElement.shadowRoot.querySelectorAll(
69
+ TOC_HEADER_TAG
70
+ );
71
+ } else {
72
+ headingElements =
73
+ tabPanelItem.querySelectorAll(TOC_HEADER_TAG);
74
+ }
75
+ break;
76
+ }
77
+ }
78
+ }
79
+ return headingElements;
80
+ }
81
+
82
+ private updateURL() {
83
+ const tabs = this.getAllTabs();
84
+ const selectedTabId = this.getSelectedTabId();
85
+ tabs.forEach((tab: any) => {
86
+ if (tab.getAttribute("aria-selected") === "true") {
87
+ const tabID = tab.getAttribute("aria-label");
88
+ const url = new URL(window.location.href);
89
+ if (selectedTabId !== tabID) {
90
+ url.searchParams.set("type", tabID);
91
+ url.hash = "";
92
+ window.history.pushState({}, "", url.toString());
93
+ }
94
+ }
95
+ });
96
+ }
97
+
98
+ // This event gets triggered when navigating back/forward
99
+ handlePopState = (): void => {
100
+ if (this.showTabBasedRNB) {
101
+ this.restoreTabSelection();
102
+ }
103
+ };
104
+
105
+ connectedCallback(): void {
106
+ const hasParentHighlightListener = closest(
107
+ "doc-xml-content",
108
+ this.template.host
109
+ );
110
+ if (!hasParentHighlightListener) {
111
+ window.addEventListener(
112
+ "highlightedtermchange",
113
+ this.updateHighlighted
114
+ );
115
+ this.searchSyncer.init();
116
+ }
117
+ window.addEventListener("popstate", this.handlePopState);
118
+ }
119
+
120
+ private getSelectedTabId() {
121
+ const urlParams = new URLSearchParams(window.location.search);
122
+ const selectedTabId = urlParams.get("type");
123
+ return selectedTabId;
124
+ }
125
+
126
+ private restoreTabSelection() {
127
+ requestAnimationFrame(() => {
128
+ const selectedTabId = this.getSelectedTabId();
129
+ if (selectedTabId) {
130
+ this.selectTabById(selectedTabId);
131
+ }
132
+ });
133
+ }
134
+
135
+ private getAllTabs(): any[] {
136
+ const tabPanelListItem: any = this.getTabPanelList();
137
+ if (tabPanelListItem?.shadowRoot) {
138
+ return Array.from(
139
+ tabPanelListItem.shadowRoot.querySelectorAll(
140
+ "dx-tab-panel-item"
141
+ )
142
+ ).map((tabPanelItem: any) =>
143
+ tabPanelItem.shadowRoot.querySelector("button")
144
+ );
145
+ }
146
+ return [];
147
+ }
148
+
149
+ private selectTabById(tabId: string) {
150
+ const tabs = this.getAllTabs();
151
+ tabs.forEach((tab: any) => {
152
+ if (tab.getAttribute("aria-label") === tabId) {
153
+ tab.click();
154
+ }
155
+ });
156
+ }
157
+
158
+ renderedCallback(): void {
159
+ /**
160
+ * Note: We are adding timeout because chrome is optimizing and not triggering recent renderedCallback though elements reference is changed
161
+ * Also we are considering recent renderedCallback
162
+ */
163
+ this.clearRenderObserverTimer();
164
+ this.observerTimerId = setTimeout(
165
+ this.attachInteractionObserver,
166
+ OBSERVER_ATTACH_WAIT_TIME
167
+ );
168
+
169
+ this.adjustNavPosition();
170
+ window.addEventListener("scroll", this.adjustNavPosition);
171
+ window.addEventListener("resize", this.adjustNavPosition);
172
+
173
+ if (!this.hasRendered) {
174
+ this.hasRendered = true;
175
+ this.setRNBByTab();
176
+ if (this.showTabBasedRNB) {
177
+ window.addEventListener("tabchanged", this.onTabChanged);
178
+ this.restoreTabSelection();
179
+ }
180
+ this.restoreScroll();
181
+ }
182
+ }
183
+
184
+ disconnectedCallback(): void {
185
+ if (this.showTabBasedRNB) {
186
+ window.removeEventListener("tabchanged", this.onTabChanged);
187
+ window.removeEventListener(
188
+ "specificationdatarendered",
189
+ this.onTabChanged
190
+ );
191
+ window.removeEventListener("selectedcontent", (event) =>
192
+ this.onRNBClick(event as CustomEvent)
193
+ );
194
+ window.removeEventListener("popstate", this.handlePopState);
195
+ }
196
+ }
197
+
198
+ attachInteractionObserver = (): void => {
199
+ if (!this.enableSlotChange) {
200
+ return;
201
+ }
202
+ this.disconnectObserver();
203
+
204
+ const globalNavOffset = `-${getComputedStyle(
205
+ document.documentElement
206
+ ).getPropertyValue("--dx-g-doc-header-main-nav-height")}`;
207
+
208
+ this.observer = new IntersectionObserver(
209
+ (entries) => {
210
+ entries.forEach(
211
+ (entry) =>
212
+ (this.anchoredElements[
213
+ entry.target.getAttribute("id")!
214
+ ].intersect = entry.isIntersecting)
215
+ );
216
+ this.calculateActualSection();
217
+ },
218
+ {
219
+ rootMargin: globalNavOffset.trim()
220
+ }
221
+ );
222
+
223
+ // Note: We are doing document.querySelectorAll as a quick fix as we are not getting heading elements reference this.querySelectorAll
224
+ const headingElements = this.getHeadingElements();
225
+
226
+ // We only need to update URL in case of /docs and ignore if tabs are used anywhere else in DSC
227
+ if (this.showTabBasedRNB) {
228
+ this.updateURL();
229
+ }
230
+
231
+ for (const headingElement of headingElements as any) {
232
+ // Add headingElements to intersectionObserver for highlighting respective RNB item when user scroll
233
+ const id = headingElement.getAttribute("id")!;
234
+ this.anchoredElements[id] = {
235
+ id,
236
+ intersect: false
237
+ };
238
+ this.observer.observe(headingElement);
239
+ }
240
+
241
+ if (!this.didScrollToSelectedHash) {
242
+ this.didScrollToSelectedHash = true;
243
+ this.scrollToHash(headingElements);
244
+ }
245
+ };
246
+
247
+ onSlotChange(): void {
248
+ this.updateRNB();
249
+ this.contentLoaded = true;
250
+ }
251
+
252
+ // eslint-disable-next-line no-undef
253
+ private updateTocItems(headingElements: NodeListOf<Element>): void {
254
+ const tocOptions = [];
255
+
256
+ for (const headingElement of headingElements as any) {
257
+ headingElement.id = headingElement.hash;
258
+
259
+ // Update tocOptions from anchorTags only for H2, consider default as 2 as per component
260
+ const headingAriaLevel =
261
+ headingElement.attributes["aria-level"]?.nodeValue || "2";
262
+ const isH2 = headingAriaLevel === "2";
263
+
264
+ if (isH2) {
265
+ const tocItem = {
266
+ anchor: `#${headingElement.hash}`,
267
+ id: headingElement.id,
268
+ label: headingElement.header
269
+ };
270
+ tocOptions.push(tocItem);
271
+ this.tocOptionIdsSet.add(headingElement.id);
272
+ }
273
+ }
274
+
275
+ this._tocOptions = tocOptions;
276
+ }
277
+ }
@@ -0,0 +1,3 @@
1
+ @import "dxHelpers/reset";
2
+ @import "dxHelpers/text";
3
+ @import "dxHelpers/table";
@@ -1,46 +1,145 @@
1
1
  <template>
2
- <lightning-card title="Fetched Data">
3
- <!-- Display Attributes if they exist -->
2
+ <div class="specification-properties">
4
3
  <template if:true={hasAttributes}>
5
- <h3>Attributes</h3>
6
- <ul>
7
- <template for:each={data.attribute} for:item="attr">
8
- <li key={attr.name}>
9
- <strong>{attr.name}:</strong>
10
- {attr.desc}
11
- </li>
12
- </template>
13
- </ul>
4
+ <doc-heading
5
+ header="Attributes"
6
+ hash="attributes"
7
+ aria-level="2"
8
+ id="attributes"
9
+ ></doc-heading>
10
+ <table>
11
+ <thead>
12
+ <tr>
13
+ <th>Name</th>
14
+ <th>Description</th>
15
+ <th>Type</th>
16
+ <th>Default</th>
17
+ <th>Required</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <template for:each={attributes} for:item="attribute">
22
+ <tr key={attribute.name}>
23
+ <td>{attribute.nameInKebabCase}</td>
24
+ <td>{attribute.description}</td>
25
+ <td>{attribute.type}</td>
26
+ <td>{attribute.default}</td>
27
+ <td>
28
+ <template lwc:if={attribute.required}>
29
+ <dx-icon
30
+ symbol="success"
31
+ size="large"
32
+ color="green-vibrant-65"
33
+ ></dx-icon>
34
+ </template>
35
+ </td>
36
+ </tr>
37
+ </template>
38
+ </tbody>
39
+ </table>
14
40
  </template>
15
41
 
16
- <!-- Display Methods if they exist -->
17
42
  <template if:true={hasMethods}>
18
- <h3>Methods</h3>
19
- <ul>
20
- <template for:each={data.method} for:item="method">
21
- <li key={method.name}>
22
- <strong>{method.name}:</strong>
23
- {method.desc}
24
- </li>
25
- </template>
26
- </ul>
43
+ <doc-heading
44
+ header="Methods"
45
+ hash="methods"
46
+ aria-level="2"
47
+ id="methods"
48
+ ></doc-heading>
49
+ <table>
50
+ <thead>
51
+ <tr>
52
+ <th>Name</th>
53
+ <th>Description</th>
54
+ <th>Argument Name</th>
55
+ <th>Argument Type</th>
56
+ <th>Argument Description</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ <template for:each={processedMethods} for:item="method">
61
+ <template if:true={method.firstArgument}>
62
+ <tr key={method.name}>
63
+ <td rowspan={method.arguments.length}>
64
+ {method.nameInKebabCase}
65
+ </td>
66
+ <td rowspan={method.arguments.length}>
67
+ {method.description}
68
+ </td>
69
+ <td>{method.firstArgument.name}</td>
70
+ <td>{method.firstArgument.type}</td>
71
+ <td>{method.firstArgument.description}</td>
72
+ </tr>
73
+ <template
74
+ for:each={method.remainingArguments}
75
+ for:item="arg"
76
+ >
77
+ <tr key={arg.name}>
78
+ <td>{arg.name}</td>
79
+ <td>{arg.type}</td>
80
+ <td>{arg.description}</td>
81
+ </tr>
82
+ </template>
83
+ </template>
84
+ <template if:false={method.firstArgument}>
85
+ <tr key={method.name}>
86
+ <td>{method.nameInKebabCase}</td>
87
+ <td>{method.description}</td>
88
+ <td colspan="3"></td>
89
+ </tr>
90
+ </template>
91
+ </template>
92
+ </tbody>
93
+ </table>
27
94
  </template>
28
95
 
29
- <!-- Display Slots if they exist -->
30
96
  <template if:true={hasSlots}>
31
- <h3>Slots</h3>
32
- <ul>
33
- <template for:each={data.slots} for:item="slot">
34
- <li key={slot.name}>
35
- <strong>{slot.name}:</strong>
36
- {slot.desc}
37
- </li>
38
- </template>
39
- </ul>
97
+ <doc-heading
98
+ header="Slots"
99
+ hash="slots"
100
+ aria-level="2"
101
+ id="slots"
102
+ ></doc-heading>
103
+ <table>
104
+ <thead>
105
+ <tr>
106
+ <th>Name</th>
107
+ <th>Description</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ <template for:each={slots} for:item="slot">
112
+ <tr key={slot.name}>
113
+ <td>{slot.nameInKebabCase}</td>
114
+ <td>{slot.description}</td>
115
+ </tr>
116
+ </template>
117
+ </tbody>
118
+ </table>
40
119
  </template>
41
120
 
42
- <template if:true={error}>
43
- <p>Error fetching data: {error}</p>
121
+ <template if:true={hasEvents}>
122
+ <doc-heading
123
+ header="Events"
124
+ hash="events"
125
+ aria-level="2"
126
+ ></doc-heading>
127
+ <table>
128
+ <thead>
129
+ <tr>
130
+ <th>Name</th>
131
+ <th>Description</th>
132
+ </tr>
133
+ </thead>
134
+ <tbody>
135
+ <template for:each={events} for:item="event">
136
+ <tr key={event.name}>
137
+ <td>{event.nameInKebabCase}</td>
138
+ <td>{event.description}</td>
139
+ </tr>
140
+ </template>
141
+ </tbody>
142
+ </table>
44
143
  </template>
45
- </lightning-card>
144
+ </div>
46
145
  </template>
@@ -1,41 +1,112 @@
1
- import { LightningElement, track } from "lwc";
2
- import { toJson } from "dxUtils/normalizers";
1
+ import { LightningElement, track, api } from "lwc";
2
+ import { Method, Specification } from "typings/custom";
3
+ import debounce from "debounce";
3
4
 
4
- export default class specificationContent extends LightningElement {
5
- @track data: any; // Reactive property to store fetched data
6
- @track error: any; // To track any errors
5
+ export default class SpecificationContent extends LightningElement {
6
+ @track data: any;
7
+ // TODO: added these default values for testing, will drop this once the backend is ready.
8
+ @api component: string = "button";
9
+ @api model: string = "lwc";
10
+ @api namespace: string = "lightning";
11
+
12
+ /* TODO: The actual URL is as follows:
13
+ * http://api.salesforce.com/doc-platform/developer/v1/{type}/{sub-type}/{component-name}
14
+ * Until the API integration is ready, we will go ahead with mocked-router-url.
15
+ */
16
+ @api apiBaseUrl: string =
17
+ "https://cx-mock-router-internal-07a18d7b3f61.herokuapp.com";
18
+
19
+ private attributes: Specification[] = [];
20
+ private methods: Method[] = [];
21
+ private slots: Specification[] = [];
22
+ private events: Specification[] = [];
23
+
24
+ /* TODO: For now setting the timeout to 300ms,
25
+ * post integration with CX-Router API will test and change if required.
26
+ */
27
+ private debouncedNotifyDataRendered = debounce(() => {
28
+ this.notifySpecificationDataRendered();
29
+ }, 300);
7
30
 
8
- // Lifecycle method to fetch data when the component is inserted into the DOM
9
31
  connectedCallback() {
10
- this.loadData();
32
+ this.fetchComponentMetadata();
11
33
  }
12
34
 
13
- // Method to make the API call
14
- loadData() {
15
- fetch("http://localhost:3002/card") // Sample API URL
16
- .then((response) => response.json()) // Convert response to JSON
17
- .then((result) => {
18
- this.data = toJson(result); // Store data to update UI
19
- this.error = undefined; // Clear any previous errors
20
- })
21
- .catch((error) => {
22
- this.error = error; // Capture any errors
23
- this.data = {}; // Clear data on error
24
- });
35
+ async fetchComponentMetadata() {
36
+ const url = `${this.apiBaseUrl}/${this.model}/${this.namespace}/${this.component}`;
37
+
38
+ try {
39
+ const response = await fetch(url);
40
+
41
+ if (!response.ok) {
42
+ // TODO: Will add loader and show error as follow-up
43
+ throw new Error(`Failed to fetch: ${response.statusText}`);
44
+ }
45
+
46
+ const result = await response.json();
47
+ this.data = result;
48
+ ({
49
+ attributes: this.attributes,
50
+ methods: this.methods,
51
+ slots: this.slots,
52
+ events: this.events
53
+ } = this.data);
54
+ } catch (error) {
55
+ this.data = {};
56
+ console.error("fetchComponentMetadata() failed for:" + url);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * This getter is to preprocess the methods for easier rendering in the template.
62
+ * Each method is augmented with additional properties:
63
+ * - `firstArgument`: The first argument (if any).
64
+ * - `remainingArguments`: All other arguments (if any).
65
+ * - `hasArguments`: A boolean indicating whether the method has arguments or not.
66
+ */
67
+ get processedMethods(): Method[] {
68
+ return this.methods.map((method) => {
69
+ const [firstArgument, ...remainingArguments] =
70
+ method.arguments || [];
71
+ return {
72
+ ...method,
73
+ firstArgument,
74
+ remainingArguments,
75
+ hasArguments: method.arguments && method.arguments.length > 0
76
+ };
77
+ });
25
78
  }
26
79
 
27
- // Helper to check if attributes exist
28
80
  get hasAttributes() {
29
- return this.data?.attribute;
81
+ return this.attributes?.length > 0;
30
82
  }
31
83
 
32
- // Helper to check if methods exist
33
84
  get hasMethods() {
34
- return this.data?.method;
85
+ return this.methods?.length > 0;
35
86
  }
36
87
 
37
- // Helper to check if slots exist
38
88
  get hasSlots() {
39
- return this.data?.slots;
89
+ return this.slots?.length > 0;
90
+ }
91
+
92
+ get hasEvents() {
93
+ return this.events?.length > 0;
94
+ }
95
+
96
+ renderedCallback(): void {
97
+ if (this.data) {
98
+ this.debouncedNotifyDataRendered();
99
+ }
100
+ }
101
+
102
+ notifySpecificationDataRendered() {
103
+ // Dispatch a custom event to notify the specification tab has rendered.
104
+ this.dispatchEvent(
105
+ new CustomEvent("specificationdatarendered", {
106
+ detail: { name: "doc-specification-content" },
107
+ bubbles: true,
108
+ composed: true
109
+ })
110
+ );
40
111
  }
41
112
  }
@@ -0,0 +1,131 @@
1
+ :host {
2
+ --dx-c-content-vertical-spacing: var(--dx-g-spacing-lg);
3
+ --dx-c-content-sidebar-sticky-top: calc(
4
+ var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
5
+ );
6
+ --dx-c-sidebar-height: calc(
7
+ 100vh -
8
+ calc(
9
+ var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
10
+ )
11
+ );
12
+ --dx-c-content-scroll-margin-top: calc(
13
+ var(--dx-g-global-header-height) + var(--dx-g-doc-header-height) +
14
+ var(--dx-g-spacing-2xl)
15
+ );
16
+
17
+ display: block;
18
+ }
19
+
20
+ doc-breadcrumbs {
21
+ --dx-c-popover-z-index: 5;
22
+
23
+ display: block;
24
+ margin-bottom: var(--dx-g-spacing-2xl);
25
+ }
26
+
27
+ dx-sidebar,
28
+ dx-sidebar-old {
29
+ --dx-c-sidebar-vertical-padding: var(--dx-g-spacing-md);
30
+
31
+ z-index: calc(var(--dx-g-z-index-100) + 5);
32
+ }
33
+
34
+ dx-toc {
35
+ --dx-c-toc-width: unset;
36
+
37
+ height: calc(100% - var(--dx-c-content-vertical-spacing) * 2);
38
+ margin: var(--dx-c-content-vertical-spacing) 0;
39
+ overflow-y: auto;
40
+ }
41
+
42
+ dx-sidebar,
43
+ dx-toc {
44
+ display: block;
45
+ }
46
+
47
+ /* offset page jump link due to fixed header */
48
+ ::slotted(doc-heading) {
49
+ scroll-margin-top: var(--dx-c-content-scroll-margin-top);
50
+ }
51
+
52
+ .content {
53
+ display: flex;
54
+ position: relative;
55
+ }
56
+
57
+ .content-body-doc-phase-container {
58
+ flex: 1;
59
+ border-left: 1px solid var(--dx-g-gray-90);
60
+ }
61
+
62
+ .content-body-container {
63
+ display: flex;
64
+ flex-direction: row;
65
+ justify-content: center;
66
+ max-width: var(--dx-g-doc-content-max-width);
67
+
68
+ /* Derived this manually by substracting (topHeader, doc header, banner and the content). */
69
+ min-height: 62vh;
70
+ margin: auto;
71
+ padding: 0 var(--dx-g-global-header-padding-horizontal);
72
+ margin-bottom: calc(2 * (var(--dx-g-spacing-5xl) + 4px));
73
+ }
74
+
75
+ .content-body {
76
+ margin: var(--dx-g-spacing-md) 0 0;
77
+ max-width: 900px;
78
+ flex: 1;
79
+ width: 0;
80
+ }
81
+
82
+ .is-sticky {
83
+ align-self: flex-start;
84
+ position: sticky;
85
+ top: var(--dx-c-content-sidebar-sticky-top);
86
+ }
87
+
88
+ .right-nav-bar {
89
+ margin-left: var(--dx-g-spacing-2xl);
90
+ }
91
+
92
+ @media screen and (max-width: 1024px) {
93
+ .right-nav-bar {
94
+ display: none;
95
+ }
96
+ }
97
+
98
+ @media screen and (max-width: 800px) {
99
+ .content-body {
100
+ margin-top: var(--dx-c-content-vertical-spacing);
101
+ }
102
+
103
+ .content-body-doc-phase-container {
104
+ border-left: 0;
105
+ }
106
+ }
107
+
108
+ @media screen and (max-width: 768px) {
109
+ .is-sticky {
110
+ width: 100%;
111
+ }
112
+
113
+ .content {
114
+ flex-direction: column;
115
+ }
116
+
117
+ .content-body-container {
118
+ padding-right: 0;
119
+ overflow-x: auto;
120
+ margin-bottom: calc(var(--dx-g-spacing-5xl) + 4px);
121
+ }
122
+
123
+ .left-nav-bar {
124
+ height: unset;
125
+ }
126
+
127
+ .content-body {
128
+ margin-left: var(--dx-g-spacing-mlg, 20px);
129
+ margin-right: var(--dx-g-spacing-mlg, 20px);
130
+ }
131
+ }