@salesforcedevs/dx-components 1.40.6-tlv-alpha-1 → 1.40.7-bug-fix-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
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "modules": [
3
3
  { "dir": "src/modules" },
4
- { "npm": "@salesforcedevs/dw-components" },
5
- { "npm": "@salesforcedevs/docs-components" }
4
+ { "npm": "@salesforcedevs/dw-components" }
6
5
  ],
7
6
  "expose": [
8
7
  "dx/agenda",
@@ -111,6 +110,7 @@
111
110
  "dx/tooltip",
112
111
  "dx/trafficLabeler",
113
112
  "dx/tree",
113
+ "dx/versionPicker",
114
114
  "dx/typeBadge",
115
115
  "dx/vimeoPlayer",
116
116
  "dxBaseElements/headerBase",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "1.40.6-tlv-alpha-1",
3
+ "version": "1.40.7-bug-fix-alpha",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -43,6 +43,5 @@
43
43
  "eventsourcemock": "2.0.0",
44
44
  "luxon": "3.4.4",
45
45
  "msw": "^2.12.4"
46
- },
47
- "stableVersion": "1.40.5"
46
+ }
48
47
  }
@@ -15,9 +15,6 @@ import { normalizeBoolean } from "dxUtils/normalizers";
15
15
  - SQL Documentation -> SQL
16
16
  */
17
17
 
18
- // Used for remove enclosing <pre> tag's (if occurs)
19
- const preTagRegexp: RegExp = /^<pre.*?>(.*)<\/pre>$/is;
20
-
21
18
  export const LIGHT = "light";
22
19
  export const DARK = "dark";
23
20
  export const EVENT_NAME = "dx-codeblock-theme";
@@ -87,12 +84,7 @@ export default class CodeBlock extends LightningElement {
87
84
  set codeBlock(value: string) {
88
85
  this._codeBlockRendered = false;
89
86
  this._isCbHeightCalculated = false;
90
- let match;
91
- this._codeBlock = (
92
- (match = preTagRegexp.exec(value.trim())) === null
93
- ? value.trim()
94
- : match[1]
95
- ).trim();
87
+ this._codeBlock = value.trim();
96
88
  }
97
89
 
98
90
  @api
@@ -29,10 +29,7 @@
29
29
  /* Opt-in hooks; defaults preserve today's look. */
30
30
  padding: var(--dx-c-dropdown-option-padding, var(--dx-g-spacing-sm));
31
31
  font-size: var(--dx-c-dropdown-option-font-size, var(--dx-g-text-sm));
32
- border-radius: var(
33
- --dx-c-dropdown-option-border-radius,
34
- var(--dx-g-spacing-xs)
35
- );
32
+ border-radius: var(--dx-c-dropdown-option-border-radius, 0);
36
33
  transition: var(--dx-g-transition-hue-1x);
37
34
  align-items: center;
38
35
  outline: none !important;
@@ -106,7 +106,7 @@
106
106
  {result.contentTypeLabel}
107
107
  </span>
108
108
  </div>
109
- <a href={result.url} class="dx-result-title">
109
+ <a href={result.href} class="dx-result-title">
110
110
  {result.title}
111
111
  </a>
112
112
  <p if:true={result.hasDate} class="dx-result-date">
@@ -8,6 +8,7 @@ import {
8
8
  decodeHtmlEntities,
9
9
  highlightedSnippetHtml,
10
10
  isSortOption,
11
+ resultHref,
11
12
  SORT_OPTIONS,
12
13
  type SearchResult,
13
14
  type SortOption
@@ -30,6 +31,8 @@ const SORT_LABELS: Record<SortOption, string> = {
30
31
  interface DisplayResult {
31
32
  key: string;
32
33
  url: string;
34
+ // Matched-section link; falls back to the page url.
35
+ href: string;
33
36
  title: string;
34
37
  description: string | null;
35
38
  hasDescription: boolean;
@@ -167,6 +170,7 @@ export default class SearchHybrid extends LightningElement {
167
170
  return {
168
171
  key: `${r.url}-${i}`,
169
172
  url: r.url,
173
+ href: resultHref(r),
170
174
  title: decodeHtmlEntities(r.title),
171
175
  description,
172
176
  hasDescription: description !== null,
@@ -102,6 +102,7 @@
102
102
  for:each={trees}
103
103
  for:item="tree"
104
104
  key={tree.key}
105
+ language={language}
105
106
  tree-root={tree.tree}
106
107
  onselect={onSelect}
107
108
  value={value}
@@ -71,6 +71,7 @@
71
71
  for:each={filteredTrees}
72
72
  for:item="tree"
73
73
  key={tree.key}
74
+ language={language}
74
75
  tree-root={tree.tree}
75
76
  onselect={onSelect}
76
77
  value={value}
@@ -4,6 +4,7 @@
4
4
  is-root
5
5
  tree-node={treeRoot}
6
6
  selected-key={selectedKey}
7
+ language={language}
7
8
  ontileselected={onTileSelected}
8
9
  onexpandcollapse={onExpandCollapse}
9
10
  onregister={onRegister}
@@ -4,6 +4,10 @@ import { toJson } from "dxUtils/normalizers";
4
4
  import { TreeHandler } from "./treeHandler";
5
5
 
6
6
  export default class Tree extends LightningElement {
7
+ // Must be set before `treeRoot`. Both sidebar templates pass `language`
8
+ // first; the tree resolves stored topic versions with it while being built.
9
+ @api language?: string;
10
+
7
11
  @api
8
12
  get treeRoot() {
9
13
  return this._treeRoot!;
@@ -11,7 +15,7 @@ export default class Tree extends LightningElement {
11
15
 
12
16
  set treeRoot(value: TreeNode | string) {
13
17
  value = toJson(value);
14
- this._treeHandler = new TreeHandler();
18
+ this._treeHandler = new TreeHandler(this.language);
15
19
  this._treeRoot = this._treeHandler.initialize(value as TreeNode);
16
20
  if (this._value) {
17
21
  this.selectedKey = this._treeHandler.getKeyByName(this._value);
@@ -22,8 +22,10 @@ const ROOT_KEY = "0";
22
22
  export class TreeHandler {
23
23
  private _nodeMap: { [key: string]: MapItem };
24
24
  private _nameKeyMap: { [key: string]: string };
25
+ private _language?: string;
25
26
 
26
- constructor() {
27
+ constructor(language?: string) {
28
+ this._language = language;
27
29
  this._nodeMap = {};
28
30
  this._nameKeyMap = {};
29
31
  }
@@ -73,7 +75,7 @@ export class TreeHandler {
73
75
  }
74
76
  ): VersionedInternalTreeNode {
75
77
  const ownSelected = node.versions?.length
76
- ? resolveSelectedVersion(node)
78
+ ? resolveSelectedVersion(node, this._language)
77
79
  : undefined;
78
80
  const versionContext =
79
81
  ownSelected && node.versions
@@ -115,8 +117,13 @@ export class TreeHandler {
115
117
  };
116
118
 
117
119
  const mapItem = { node: convertedNode, parent };
118
- if (node.children && node.children.length > 0) {
119
- convertedNode.children = node.children.map((child, index) =>
120
+ // Prefer the selected version's own topic list when the server sent one.
121
+ // Otherwise keep the latest children and rewrite their hrefs.
122
+ const childNodes = Array.isArray(ownSelected?.children)
123
+ ? ownSelected.children
124
+ : node.children;
125
+ if (childNodes && childNodes.length > 0) {
126
+ convertedNode.children = childNodes.map((child, index) =>
120
127
  this.convert(child, level + 1, mapItem, index, versionContext)
121
128
  );
122
129
  }
@@ -16,6 +16,14 @@
16
16
  display: flex;
17
17
  align-items: center;
18
18
  min-width: 0;
19
+ position: relative;
20
+ }
21
+
22
+ /* The tile's keyboard focus layers fill this row, picker included. isolate
23
+ keeps the fill above the row's background and below its content. Only while
24
+ focus is inside: a row that is always isolated traps its picker's menu. */
25
+ .tree-item-row:focus-within {
26
+ isolation: isolate;
19
27
  }
20
28
 
21
29
  .tree-item-row dx-tree-tile {
@@ -15,7 +15,7 @@
15
15
  has-version-picker={showVersionPicker}
16
16
  onclick={onLinkClick}
17
17
  ></dx-tree-tile>
18
- <doc-version-picker
18
+ <dx-version-picker
19
19
  if:true={showVersionPicker}
20
20
  class="tree-item-version-picker"
21
21
  small="true"
@@ -25,7 +25,7 @@
25
25
  onchange={onVersionChange}
26
26
  onclick={onVersionPickerClick}
27
27
  onmousedown={onVersionPickerClick}
28
- ></doc-version-picker>
28
+ ></dx-version-picker>
29
29
  </div>
30
30
  <img
31
31
  if:true={showChildrenLoading}
@@ -39,6 +39,7 @@
39
39
  key={child.key}
40
40
  tree-node={child}
41
41
  selected-key={selectedKey}
42
+ language={language}
42
43
  ></dx-tree-item>
43
44
  </template>
44
45
  </template>
@@ -23,6 +23,7 @@ type VersionedInternalTreeNode = InternalTreeNode & {
23
23
  export default class TreeItem extends LightningElement {
24
24
  @api selectedKey?: string;
25
25
  @api isRoot: boolean = false;
26
+ @api language?: string;
26
27
 
27
28
  @api
28
29
  public get treeNode() {
@@ -127,7 +128,11 @@ export default class TreeItem extends LightningElement {
127
128
  }
128
129
 
129
130
  private get selectedVersion(): TopicVersionOption | undefined {
130
- return resolveSelectedVersion(this._treeNode);
131
+ return resolveSelectedVersion(this._treeNode, this.language);
132
+ }
133
+
134
+ private get topicVersionKey(): string {
135
+ return getTopicVersionKey(this._treeNode, this.language);
131
136
  }
132
137
 
133
138
  private get isLatestVersion(): boolean {
@@ -171,8 +176,8 @@ export default class TreeItem extends LightningElement {
171
176
 
172
177
  // Remember this topic's resolved version (own URL, descendant URL, or
173
178
  // session) so sibling pickers stay independent for the tab.
174
- const selected = resolveSelectedVersion(this._treeNode);
175
- const topicKey = getTopicVersionKey(this._treeNode);
179
+ const selected = this.selectedVersion;
180
+ const topicKey = this.topicVersionKey;
176
181
  if (this.versions?.length && selected && topicKey) {
177
182
  persistTopicVersion(topicKey, selected.id);
178
183
  }
@@ -180,7 +185,7 @@ export default class TreeItem extends LightningElement {
180
185
 
181
186
  private onVersionChange(event: CustomEvent): void {
182
187
  event.stopPropagation();
183
- persistTopicVersion(getTopicVersionKey(this._treeNode), event.detail);
188
+ persistTopicVersion(this.topicVersionKey, event.detail);
184
189
  }
185
190
 
186
191
  private onVersionPickerClick(event: Event): void {
@@ -213,8 +218,12 @@ export default class TreeItem extends LightningElement {
213
218
  }
214
219
 
215
220
  private preventDefaultLinkBehavior(event: Event) {
216
- // prevent page refresh if href isn't present or link is already active
221
+ // Nodes with no link.href (AMF items use name) must not follow the
222
+ // anchor. Parent-only topics are excluded: their href is the child page.
223
+ // Otherwise cancel only an empty href or a click on the current path.
217
224
  if (
225
+ (!this._treeNode.link?.href &&
226
+ !isParentOnlyTopic(this._treeNode)) ||
218
227
  !this.href ||
219
228
  normalizePath(this.href) === normalizePath(window.location.pathname)
220
229
  ) {
@@ -217,3 +217,56 @@ a:visited.tile-root,
217
217
  a:link.tile-root {
218
218
  color: var(--dx-g-blue-vibrant-20);
219
219
  }
220
+
221
+ /* Keyboard focus matches dx-dropdown-option: a light fill, a 2px blue inset
222
+ ring, and a white inner band. The layers fill dx-tree-item's row (it sets
223
+ position: relative), so a version picker beside the link sits inside the
224
+ focus box. Undo the shared tree item focus box (commonTreeItem) first. */
225
+ :host .sidebar-item:focus-visible {
226
+ margin: 0;
227
+ padding: 6px var(--dx-g-spacing-lg) 6px
228
+ var(--dx-g-global-header-padding-horizontal);
229
+ outline: none;
230
+ border-radius: 0;
231
+ }
232
+
233
+ /* Keep the picker row's 4px right padding on focus, so the label does not rewrap. */
234
+ :host .tile-with-version-picker:focus-visible {
235
+ padding-right: var(--dx-g-spacing-xs);
236
+ }
237
+
238
+ :host .sidebar-item:focus-visible .indentation {
239
+ padding-top: 0;
240
+ padding-bottom: 0;
241
+ outline: none;
242
+ }
243
+
244
+ :host .sidebar-item:focus-visible::before,
245
+ :host .sidebar-item:focus-visible::after {
246
+ content: "";
247
+ position: absolute;
248
+ inset: 0;
249
+ pointer-events: none;
250
+ }
251
+
252
+ :host .sidebar-item:focus-visible::before {
253
+ z-index: -1;
254
+ background: var(--dx-g-cloud-blue-vibrant-95);
255
+ }
256
+
257
+ :host .sidebar-item:focus-visible::after {
258
+ z-index: 1;
259
+ box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-60),
260
+ inset 0 0 0 4px white;
261
+ }
262
+
263
+ /* A selected item keeps its left bar, inside the ring, when it also has keyboard focus. */
264
+ :host
265
+ .sidebar-item:is(
266
+ .sidebar-item-selected,
267
+ .sidebar-item-selected-text
268
+ ):focus-visible::after {
269
+ box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-60),
270
+ inset 0 0 0 4px white,
271
+ inset calc(2 * var(--dx-g-spacing-xs)) 0 0 0 var(--dx-g-blue-vibrant-40);
272
+ }
@@ -0,0 +1,248 @@
1
+ @import "dxHelpers/reset";
2
+
3
+ /* NOTE: dx-version-picker-width is used by both dx-button and dx-dropdown to maintain a consistent width. */
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
+ --dx-c-dropdown-option-font-weight: normal;
11
+ --dx-c-dropdown-option-font-size: var(--dx-g-text-base);
12
+ --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
+ --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
+ }
26
+
27
+ .version-picker-container {
28
+ /* Override --dx-version-picker-padding to change the inset (e.g. when the
29
+ picker sits inside an already-padded container). */
30
+ padding: var(
31
+ --dx-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 --dx-version-picker-divider (e.g. `none`) to restyle dividers. */
37
+ border-top: var(--dx-version-picker-divider, 1px solid var(--dx-g-gray-90));
38
+ border-bottom: var(
39
+ --dx-version-picker-divider,
40
+ 1px solid var(--dx-g-gray-90)
41
+ );
42
+ }
43
+
44
+ /* Inline sidebar variant: drop the header-slot chrome so the picker sits
45
+ beside a tree node without extra padding or divider lines. */
46
+ .version-picker-container-small {
47
+ display: flex;
48
+ align-items: center;
49
+ padding: 0;
50
+ border: none;
51
+ }
52
+
53
+ .version-picker-button {
54
+ display: flex;
55
+ width: var(--dx-version-picker-width, 296px);
56
+ overflow: visible;
57
+
58
+ --dx-c-button-horizontal-spacing: var(--dx-g-spacing-sm);
59
+ --dx-g-button-icon-color: var(--dx-g-gray-50);
60
+ }
61
+
62
+ .version-picker-button:hover,
63
+ .version-picker-button[aria-expanded="true"] {
64
+ --dx-g-button-icon-color: var(--dx-g-blue-vibrant-20);
65
+ }
66
+
67
+ /**
68
+ * NOTE: This CSS ensures the span inside the button stays within the parent's width, avoiding overflow.
69
+ * Not keeping this in common component to ensure that existing functionality works as it is.
70
+ */
71
+ dx-button::part(content) {
72
+ width: inherit;
73
+ overflow: hidden;
74
+ }
75
+
76
+ /* Border width stays 1px in every state. Hover, open, and focus paint the
77
+ 2px blue stroke with an inset shadow so the box does not grow.
78
+ Tertiary's hover border is overridden. */
79
+ .version-picker-button::part(container) {
80
+ box-sizing: border-box;
81
+ background: white;
82
+ border: 1px solid var(--dx-g-gray-50);
83
+ border-radius: var(--dx-g-spacing-xs);
84
+ box-shadow: none;
85
+ color: var(--dx-g-gray-10);
86
+ outline: none;
87
+ transition: var(--dx-g-transition-hue-1x), box-shadow 0.15s linear;
88
+ }
89
+
90
+ /* Hover only (not open): 2px #032D60 on #EEF4FF, no ring.
91
+ Border color matches the fill so the 1px border does not add a third
92
+ pixel around the inset stroke.
93
+ :focus-visible is included so click-open + Escape (which upgrades the
94
+ trigger to keyboard focus) still looks like hover while the pointer is on
95
+ the picker. */
96
+ .version-picker-button:hover:not([aria-expanded="true"])::part(container),
97
+ .version-picker-button:hover:not([aria-expanded="true"])::part(
98
+ container
99
+ ):focus-visible {
100
+ background: var(--dx-g-blue-vibrant-95);
101
+ border: 1px solid var(--dx-g-blue-vibrant-95);
102
+ box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20);
103
+ }
104
+
105
+ /* Open keeps the fill and the 2px stroke. The focus ring is keyboard-only,
106
+ on the closed picker below. */
107
+ .version-picker-button[aria-expanded="true"]::part(container) {
108
+ background: var(--dx-g-blue-vibrant-95);
109
+ border: 1px solid var(--dx-g-blue-vibrant-95);
110
+ border-radius: var(--dx-g-spacing-xs);
111
+ box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20);
112
+ }
113
+
114
+ /* Keyboard focus while closed. */
115
+ .version-picker-button:not([aria-expanded="true"])::part(
116
+ container
117
+ ):focus-visible {
118
+ --dx-g-button-icon-color: var(--dx-g-blue-vibrant-20);
119
+
120
+ background: var(--dx-g-blue-vibrant-95);
121
+ border: 1px solid var(--dx-g-blue-vibrant-95);
122
+ border-radius: var(--dx-g-spacing-xs);
123
+ box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20),
124
+ var(--dx-c-version-picker-focus-ring);
125
+ }
126
+
127
+ .selected-version {
128
+ display: flex;
129
+ flex-direction: row;
130
+ align-items: center;
131
+ }
132
+
133
+ .selected-version-label {
134
+ flex: 1;
135
+ overflow: hidden;
136
+ text-align: left;
137
+ text-overflow: ellipsis;
138
+ white-space: nowrap;
139
+ }
140
+
141
+ dx-type-badge.latest-badge,
142
+ dx-type-badge.not-latest-badge {
143
+ margin-left: var(--dx-g-spacing-sm);
144
+ }
145
+
146
+ /* ------------------------------------------------------------------ *
147
+ * Small "dot" variant (opt-in via `small`). Additive
148
+ * ------------------------------------------------------------------ */
149
+
150
+ /* Menu is capped at the ToC width (296px), not the 104px trigger, so options
151
+ keep the same 16px / 8px 24px / gray-10 type as the default picker. */
152
+ .version-picker-dropdown-small {
153
+ --dx-c-dropdown-option-font-size: var(--dx-g-text-base);
154
+ --dx-c-dropdown-option-padding: var(--dx-g-spacing-sm)
155
+ var(--dx-g-spacing-lg);
156
+ --dx-c-dropdown-option-label-color: var(--dx-g-gray-10);
157
+ --dx-c-dropdown-option-border-radius: 0;
158
+ --dx-c-popover-border: none;
159
+
160
+ /* Hug the labels (down to the shortest) but never wider than the ToC
161
+ content: 296px on desktop, and on the smallest breakpoint the sidebar
162
+ variable grows with the viewport up to 720px. Long labels wrap inside
163
+ that cap. */
164
+ --dx-c-dropdown-menu-max-width: var(--dx-version-picker-width, 296px);
165
+ --dx-c-dropdown-fit-white-space: normal;
166
+ --dx-c-dropdown-fit-overflow: hidden;
167
+ --dx-c-dropdown-fit-text-overflow: clip;
168
+ --dx-c-dropdown-option-overflow-wrap: anywhere;
169
+ }
170
+
171
+ .version-picker-button-small {
172
+ width: fit-content;
173
+ max-width: 104px;
174
+
175
+ --dx-c-button-font-size: var(--dx-g-text-xs);
176
+ --dx-c-button-font-weight: var(--dx-g-font-normal);
177
+ --dx-c-button-line-height: var(--dx-g-spacing-lg);
178
+ --dx-c-button-icon-gap: var(--dx-g-spacing-2xs);
179
+ }
180
+
181
+ /* min-width: 0 on each flex ancestor lets the label truncate: a flex item's
182
+ default min-width: auto refuses to shrink below its content and overrides the
183
+ host's max-width, so without this the trigger overflows instead. */
184
+ .version-picker-button-small::part(content) {
185
+ display: flex;
186
+ flex: 1;
187
+ min-width: 0;
188
+ width: auto;
189
+ overflow: hidden;
190
+ }
191
+
192
+ /* width:100% re-ties to the 104px-capped host (container is width:inherit,
193
+ which copies width but not max-width). Border/bg/states are shared above. */
194
+ .version-picker-button-small::part(container) {
195
+ width: 100%;
196
+ height: var(--dx-g-spacing-lg);
197
+ padding: 0 var(--dx-g-spacing-xs);
198
+ }
199
+
200
+ .selected-version-small {
201
+ flex: 1;
202
+ min-width: 0;
203
+ }
204
+
205
+ .version-picker-dot {
206
+ flex: 0 0 auto;
207
+ width: var(--dx-g-spacing-sm);
208
+ height: var(--dx-g-spacing-sm);
209
+ margin-right: var(--dx-g-spacing-xs);
210
+ border-radius: 50%;
211
+ }
212
+
213
+ .version-picker-dot-latest {
214
+ background: var(--dx-g-green-vibrant-60);
215
+ }
216
+
217
+ .version-picker-dot-not-latest {
218
+ background: var(--dx-g-yellow-vibrant-80);
219
+ }
220
+
221
+ /* inline-block (not flex) so the label can truncate; line-height centers it. */
222
+ .version-picker-readonly {
223
+ box-sizing: border-box;
224
+ display: inline-block;
225
+ width: var(--dx-version-picker-width, 296px);
226
+ height: var(--dx-g-spacing-xl);
227
+ margin: 0;
228
+ padding: 0 var(--dx-g-spacing-sm);
229
+ border: 1px solid var(--dx-g-gray-80);
230
+ border-radius: var(--dx-g-spacing-xs);
231
+ overflow: hidden;
232
+ color: var(--dx-g-gray-10);
233
+ text-overflow: ellipsis;
234
+ white-space: nowrap;
235
+ font: var(--dx-g-font-normal) var(--dx-g-text-sm) / var(--dx-g-spacing-xl)
236
+ var(--dx-g-font-sans),
237
+ sans-serif;
238
+ }
239
+
240
+ .version-picker-readonly-small {
241
+ width: fit-content;
242
+ max-width: 104px;
243
+ height: var(--dx-g-spacing-lg);
244
+ padding: 0 var(--dx-g-spacing-xs);
245
+ font: var(--dx-g-font-normal) var(--dx-g-text-xs) / var(--dx-g-spacing-lg)
246
+ var(--dx-g-font-sans),
247
+ sans-serif;
248
+ }
@@ -0,0 +1,73 @@
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
+
12
+ <dx-dropdown
13
+ lwc:else
14
+ class={dropdownClass}
15
+ options={versions}
16
+ analytics-event="custEv_docVersionSelect"
17
+ analytics-payload={analyticsPayload}
18
+ value={selectedVersion.id}
19
+ width-mode={dropdownWidthMode}
20
+ onchange={onVersionChange}
21
+ >
22
+ <dx-button
23
+ class={triggerClass}
24
+ variant="tertiary"
25
+ size="small"
26
+ font="sans"
27
+ icon-symbol="chevrondown"
28
+ icon-size={triggerIconSize}
29
+ aria-label={triggerAriaLabel}
30
+ >
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>
68
+ </template>
69
+ </div>
70
+ </dx-button>
71
+ </dx-dropdown>
72
+ </div>
73
+ </template>
@@ -0,0 +1,157 @@
1
+ import { LightningElement, api, track } from "lwc";
2
+ import cx from "classnames";
3
+
4
+ import { AnalyticsPayload, OptionWithNested } from "typings/custom";
5
+
6
+ import { toJson, normalizeBoolean } from "dxUtils/normalizers";
7
+
8
+ export default class VersionPicker extends LightningElement {
9
+ @api analyticsEvent?: string;
10
+ @api analyticsPayload?: AnalyticsPayload;
11
+
12
+ @track private _versions!: OptionWithNested[];
13
+ private _selectedVersion?: OptionWithNested;
14
+ private _latestVersion: boolean = false;
15
+ private _hideBadge: boolean = false;
16
+ private _small: boolean = false;
17
+ private _readOnly: boolean = false;
18
+
19
+ @api
20
+ get versions() {
21
+ return this._versions;
22
+ }
23
+
24
+ set versions(value: OptionWithNested[]) {
25
+ this._versions = toJson(value);
26
+ }
27
+
28
+ @api
29
+ get selectedVersion(): OptionWithNested | undefined {
30
+ return this._selectedVersion || this.versions[0];
31
+ }
32
+
33
+ set selectedVersion(value: OptionWithNested) {
34
+ if (value) {
35
+ this._selectedVersion = toJson(value);
36
+ }
37
+ }
38
+
39
+ @api
40
+ get latestVersion() {
41
+ return this._latestVersion;
42
+ }
43
+
44
+ set latestVersion(value) {
45
+ this._latestVersion = normalizeBoolean(value);
46
+ }
47
+
48
+ @api
49
+ get hideBadge() {
50
+ return this._hideBadge;
51
+ }
52
+
53
+ set hideBadge(value) {
54
+ this._hideBadge = normalizeBoolean(value);
55
+ }
56
+
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
+ private get showVersionPicker() {
79
+ return this._versions && this._versions.length !== 0;
80
+ }
81
+
82
+ private get containerClass(): string {
83
+ return this.small
84
+ ? "version-picker-container-small"
85
+ : "version-picker-container";
86
+ }
87
+
88
+ private get showLatestTag(): boolean {
89
+ return !this.hideBadge;
90
+ }
91
+
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
+ private onVersionChange(e: CustomEvent) {
155
+ this.dispatchEvent(new CustomEvent("change", { detail: e.detail }));
156
+ }
157
+ }
@@ -11,7 +11,7 @@
11
11
  --dx-c-sidebar-button-border-hover: var(--dx-g-blue-vibrant-50);
12
12
  --dx-c-sidebar-left-padding: var(--dx-g-global-header-padding-horizontal);
13
13
  --dx-c-sidebar-vertical-padding: var(--dx-g-spacing-md);
14
- --doc-version-picker-width: 296px;
14
+ --dx-version-picker-width: 296px;
15
15
  }
16
16
 
17
17
  .header {
@@ -172,7 +172,7 @@
172
172
 
173
173
  @media screen and (max-width: 768px) {
174
174
  :host {
175
- --doc-version-picker-width: calc(
175
+ --dx-version-picker-width: calc(
176
176
  100vw - var(--dx-c-sidebar-left-padding) - var(--dx-g-spacing-lg)
177
177
  );
178
178
  }
@@ -1,4 +1,9 @@
1
1
  export const TOPIC_VERSION_SESSION_KEY = "sfdocs-topic-version-selections";
2
+ export const DOCS_PATH_SEGMENT = "docs";
3
+ // Locale position in the path segments: `/docs/<locale>` for Docs,
4
+ // `/docs/<brand>/<locale>` for Guides/References.
5
+ export const DOCS_LOCALE_SEGMENT_INDEX = 1;
6
+ export const REFERENCES_LOCALE_SEGMENT_INDEX = 2;
2
7
 
3
8
  export type TopicVersionOption = {
4
9
  id: string;
@@ -9,6 +14,10 @@ export type TopicVersionOption = {
9
14
  href?: string;
10
15
  target?: string;
11
16
  };
17
+ /** Topics authored under this version. Absent when the client only rewrites hrefs. */
18
+ // VersionedNode also points back here through `versions`.
19
+ // eslint-disable-next-line no-use-before-define
20
+ children?: VersionedNode[];
12
21
  };
13
22
 
14
23
  export type VersionedNode = {
@@ -90,20 +99,48 @@ export function persistTopicVersion(
90
99
  );
91
100
  }
92
101
 
102
+ /**
103
+ * Normalizes the path, then removes `locale` when it sits in the docs or
104
+ * references locale position, so every locale shares one session key.
105
+ */
106
+ function stripLocaleFromPath(path: string, locale?: string): string {
107
+ const normalized = normalizePath(path);
108
+ const segments = normalized.split("/").filter(Boolean);
109
+ const normalizedLocale = locale?.toLowerCase();
110
+ if (!normalizedLocale || segments[0] !== DOCS_PATH_SEGMENT) {
111
+ return normalized;
112
+ }
113
+
114
+ const localeIndex = [
115
+ DOCS_LOCALE_SEGMENT_INDEX,
116
+ REFERENCES_LOCALE_SEGMENT_INDEX
117
+ ].find((index) => segments[index]?.toLowerCase() === normalizedLocale);
118
+ if (localeIndex === undefined) {
119
+ return normalized;
120
+ }
121
+
122
+ segments.splice(localeIndex, 1);
123
+ return `/${segments.join("/")}`;
124
+ }
125
+
93
126
  /**
94
127
  * Stable session key for a versioned topic. Parent-only sections have an empty
95
128
  * `name`/`href`, so fall back to the latest switching URL (then the label).
129
+ * The locale is stripped so a selection carries across locales.
96
130
  */
97
- export function getTopicVersionKey(node: VersionedNode | undefined): string {
131
+ export function getTopicVersionKey(
132
+ node: VersionedNode | undefined,
133
+ locale?: string
134
+ ): string {
98
135
  if (!node) {
99
136
  return "";
100
137
  }
101
138
  if (node.name) {
102
- return node.name;
139
+ return stripLocaleFromPath(node.name, locale);
103
140
  }
104
141
  const latestHref = node.versions?.[0]?.link?.href;
105
142
  if (latestHref) {
106
- return latestHref;
143
+ return stripLocaleFromPath(latestHref, locale);
107
144
  }
108
145
  return node.label || "";
109
146
  }
@@ -336,11 +373,18 @@ export function collectSubtreeCanonicalHrefs(
336
373
  if (includeSelf && own) {
337
374
  hrefs.push(own);
338
375
  }
339
- for (const child of node.children || []) {
340
- if (child.versions?.length || child.link?.target === "_blank") {
341
- continue;
376
+ const collectChildHrefs = (children?: VersionedNode[]) => {
377
+ for (const child of children || []) {
378
+ if (child.versions?.length || child.link?.target === "_blank") {
379
+ continue;
380
+ }
381
+ hrefs.push(...collectSubtreeCanonicalHrefs(child));
342
382
  }
343
- hrefs.push(...collectSubtreeCanonicalHrefs(child));
383
+ };
384
+ collectChildHrefs(node.children);
385
+ // Topics that exist only on an older version are not in `children`.
386
+ for (const version of node.versions || []) {
387
+ collectChildHrefs(version.children);
344
388
  }
345
389
  return hrefs;
346
390
  }
@@ -431,6 +475,9 @@ export function findTopicVersionForCurrentPage(
431
475
  match = { node, version };
432
476
  }
433
477
  walk(node.children);
478
+ for (const versionOption of node.versions || []) {
479
+ walk(versionOption.children);
480
+ }
434
481
  }
435
482
  };
436
483
  walk(nodes);
@@ -457,7 +504,8 @@ export function latestHrefForCurrentPage(
457
504
  }
458
505
 
459
506
  export function resolveSelectedVersion(
460
- node: VersionedNode | undefined
507
+ node: VersionedNode | undefined,
508
+ locale?: string
461
509
  ): TopicVersionOption | undefined {
462
510
  const versions = node?.versions;
463
511
  if (!node || !versions?.length) {
@@ -471,6 +519,7 @@ export function resolveSelectedVersion(
471
519
  return fromUrl;
472
520
  }
473
521
 
474
- const storedId = readTopicVersionSelections()[getTopicVersionKey(node)];
522
+ const storedId =
523
+ readTopicVersionSelections()[getTopicVersionKey(node, locale)];
475
524
  return versions.find((version) => version.id === storedId) || versions[0];
476
525
  }