@salesforcedevs/dx-components 1.40.4 → 1.40.5-final-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 +1 -0
- package/package.json +46 -47
- package/src/modules/dx/button/button.css +2 -2
- package/src/modules/dx/dropdown/dropdown.css +29 -1
- package/src/modules/dx/dropdown/dropdown.html +1 -1
- package/src/modules/dx/dropdown/dropdown.ts +16 -1
- package/src/modules/dx/dropdownOption/dropdownOption.css +32 -6
- package/src/modules/dx/popover/popover.ts +119 -7
- package/src/modules/dx/tree/treeHandler.ts +82 -10
- package/src/modules/dx/treeItem/treeItem.css +30 -0
- package/src/modules/dx/treeItem/treeItem.html +16 -2
- package/src/modules/dx/treeItem/treeItem.ts +99 -10
- package/src/modules/dx/treeTile/treeTile.css +4 -0
- package/src/modules/dx/treeTile/treeTile.ts +12 -3
- package/src/modules/dx/typeBadge/typeBadge.ts +23 -2
- package/src/modules/dxBaseElements/headerBase/headerBase.ts +2 -1
- package/src/modules/dxConstants/brands/brands.ts +2 -1
- package/src/modules/dxHelpers/commonSidebar/commonSidebar.css +1 -1
- package/src/modules/dxHelpers/commonTreeItem/commonTreeItem.css +4 -0
- package/src/modules/dxHelpers/typeBadge/typeBadge.css +22 -0
- package/src/modules/dxUtils/topicVersion/topicVersion.ts +467 -0
- package/LICENSE +0 -12
package/lwc.config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,48 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
2
|
+
"name": "@salesforcedevs/dx-components",
|
|
3
|
+
"version": "1.40.5-final-alpha",
|
|
4
|
+
"description": "DX Lightning web components",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "22.x"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@coveo/headless": "3.22.2",
|
|
14
|
+
"@floating-ui/dom": "1.5.1",
|
|
15
|
+
"@optimizely/optimizely-sdk": "5.3.0",
|
|
16
|
+
"@shikijs/colorized-brackets": "^3.12.2",
|
|
17
|
+
"@types/throttle-debounce": "5.0.2",
|
|
18
|
+
"@vimeo/player": "2.22.0",
|
|
19
|
+
"classnames": "2.5.1",
|
|
20
|
+
"composed-offset-position": "0.0.4",
|
|
21
|
+
"coveo-search-ui": "2.10113.0",
|
|
22
|
+
"debounce": "1.2.1",
|
|
23
|
+
"js-cookie": "3.0.5",
|
|
24
|
+
"lodash.defaults": "4.2.0",
|
|
25
|
+
"lodash.get": "4.4.2",
|
|
26
|
+
"lodash.kebabcase": "4.1.1",
|
|
27
|
+
"memoize-one": "6.0.0",
|
|
28
|
+
"microtip": "0.2.2",
|
|
29
|
+
"shiki": "^3.12.2",
|
|
30
|
+
"throttle-debounce": "5.0.0",
|
|
31
|
+
"uuid": "9.0.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/classnames": "2.3.1",
|
|
35
|
+
"@types/debounce": "1.2.4",
|
|
36
|
+
"@types/js-cookie": "3.0.6",
|
|
37
|
+
"@types/lodash.defaults": "4.2.9",
|
|
38
|
+
"@types/lodash.get": "4.4.9",
|
|
39
|
+
"@types/lodash.kebabcase": "4.1.9",
|
|
40
|
+
"@types/luxon": "3.4.2",
|
|
41
|
+
"@types/uuid": "8.3.4",
|
|
42
|
+
"@types/vimeo__player": "2.18.3",
|
|
43
|
+
"eventsourcemock": "2.0.0",
|
|
44
|
+
"luxon": "3.4.4",
|
|
45
|
+
"msw": "^2.12.4"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -86,11 +86,11 @@
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
.button.icon-right:not(.slot-empty) > *:nth-child(2) {
|
|
89
|
-
margin-left: var(--dx-g-spacing-sm);
|
|
89
|
+
margin-left: var(--dx-c-button-icon-gap, var(--dx-g-spacing-sm));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
.button.icon-left:not(.slot-empty) > *:nth-child(2) {
|
|
93
|
-
margin-right: var(--dx-g-spacing-sm);
|
|
93
|
+
margin-right: var(--dx-c-button-icon-gap, var(--dx-g-spacing-sm));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
.button.style-icon.slot-empty {
|
|
@@ -14,12 +14,40 @@
|
|
|
14
14
|
display: none;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/* Opt-in fit-content menu: hugs the labels (so it can be as narrow as the
|
|
18
|
+
shortest one) and stops at --dx-c-dropdown-menu-max-width. Defaults still
|
|
19
|
+
truncate; set --dx-c-dropdown-fit-white-space: normal to wrap instead. */
|
|
20
|
+
.menu-fit-content::part(content) {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
width: max-content;
|
|
23
|
+
max-width: var(
|
|
24
|
+
--dx-c-dropdown-menu-max-width,
|
|
25
|
+
calc(100vw - var(--dx-g-spacing-lg))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
.menu-fit-content {
|
|
30
|
+
--dx-c-dropdown-option-min-width: 0;
|
|
31
|
+
--dx-c-dropdown-option-label-display: block;
|
|
32
|
+
--dx-c-dropdown-option-white-space: var(
|
|
33
|
+
--dx-c-dropdown-fit-white-space,
|
|
34
|
+
nowrap
|
|
35
|
+
);
|
|
36
|
+
--dx-c-dropdown-option-overflow: var(--dx-c-dropdown-fit-overflow, hidden);
|
|
37
|
+
--dx-c-dropdown-option-text-overflow: var(
|
|
38
|
+
--dx-c-dropdown-fit-text-overflow,
|
|
39
|
+
ellipsis
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Nested fit-content menus (content action toolbar). Flat fit-content menus,
|
|
44
|
+
including the version picker, keep the option rules above. */
|
|
45
|
+
.menu-fit-content.menu-nested {
|
|
18
46
|
--dx-c-popover-width: max-content;
|
|
19
47
|
--dx-c-popover-padding: var(--dx-g-spacing-sm) 0;
|
|
20
48
|
}
|
|
21
49
|
|
|
22
|
-
.menu-fit-content .menu_list {
|
|
50
|
+
.menu-fit-content.menu-nested .menu_list {
|
|
23
51
|
width: 100%;
|
|
24
52
|
align-items: flex-start;
|
|
25
53
|
gap: var(--dx-g-spacing-sm);
|
|
@@ -82,20 +82,35 @@ export default class Dropdown extends LightningElement {
|
|
|
82
82
|
return this.areOptionsEmpty ? null : "menu";
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// "fit-end" is fit-content, aligned to the trigger's end edge.
|
|
86
|
+
// "full" matches the trigger width. "fixed" and "fit-content" are unchanged.
|
|
85
87
|
private get isFitContent() {
|
|
86
|
-
return this.widthMode === "fit-content";
|
|
88
|
+
return this.widthMode === "fit-content" || this.widthMode === "fit-end";
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
private get effectivePlacement(): PopperPlacement | undefined {
|
|
92
|
+
return this.widthMode === "fit-end" ? "bottom-end" : this.placement;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// fit-content takes precedence; the misuse is surfaced in className() below.
|
|
89
96
|
private get effectiveWidth() {
|
|
90
97
|
return this.isFitContent ? null : this.width;
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
private get effectiveFullWidth() {
|
|
101
|
+
if (this.widthMode === "full") {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
94
104
|
return this.isFitContent ? false : this.fullWidth;
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
private get className() {
|
|
98
108
|
const fitContent = this.isFitContent;
|
|
109
|
+
if (fitContent && (this.width || this.fullWidth)) {
|
|
110
|
+
console.error(
|
|
111
|
+
'The "width-mode=fit-content" and "width"/"full-width" properties cannot be provided simultaneously. "width-mode=fit-content" is prioritized; "width"/"full-width" are ignored.'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
99
114
|
return cx(
|
|
100
115
|
this.isNested && "menu-nested",
|
|
101
116
|
this.isNested &&
|
|
@@ -25,8 +25,14 @@
|
|
|
25
25
|
display: flex;
|
|
26
26
|
font-family: var(--dx-g-font-sans);
|
|
27
27
|
width: 100%;
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
|
|
29
|
+
/* Opt-in hooks; defaults preserve today's look. */
|
|
30
|
+
padding: var(--dx-c-dropdown-option-padding, var(--dx-g-spacing-sm));
|
|
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
|
+
);
|
|
30
36
|
transition: var(--dx-g-transition-hue-1x);
|
|
31
37
|
align-items: center;
|
|
32
38
|
outline: none !important;
|
|
@@ -47,7 +53,10 @@
|
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
.option-active {
|
|
50
|
-
--dx-c-dropdown-option-label-color: var(
|
|
56
|
+
--dx-c-dropdown-option-label-color: var(
|
|
57
|
+
--dx-c-dropdown-option-active-label-color,
|
|
58
|
+
var(--dx-g-blue-vibrant-50)
|
|
59
|
+
);
|
|
51
60
|
|
|
52
61
|
background: var(--dx-g-cloud-blue-vibrant-95);
|
|
53
62
|
}
|
|
@@ -57,8 +66,12 @@
|
|
|
57
66
|
flex-direction: column;
|
|
58
67
|
justify-content: center;
|
|
59
68
|
font-family: var(--dx-g-font-sans);
|
|
60
|
-
font-size: var(--dx-g-text-sm);
|
|
69
|
+
font-size: var(--dx-c-dropdown-option-font-size, var(--dx-g-text-sm));
|
|
70
|
+
color: var(--dx-c-dropdown-option-label-color, inherit);
|
|
61
71
|
text-align: left;
|
|
72
|
+
|
|
73
|
+
/* Opt-in: set to 0 to let a long label truncate instead of overflow. */
|
|
74
|
+
min-width: var(--dx-c-dropdown-option-min-width, auto);
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
.option-indented .option_details {
|
|
@@ -68,10 +81,20 @@
|
|
|
68
81
|
|
|
69
82
|
.option_label {
|
|
70
83
|
color: var(--dx-c-dropdown-option-label-color, var(--dx-g-blue-vibrant-50));
|
|
71
|
-
|
|
84
|
+
|
|
85
|
+
/* Opt-in: set to block so text-overflow:ellipsis applies (ignored on flex). */
|
|
86
|
+
display: var(--dx-c-dropdown-option-label-display, flex);
|
|
72
87
|
align-items: center;
|
|
73
88
|
font-weight: var(--dx-c-dropdown-option-font-weight, var(--dx-g-font-bold));
|
|
74
89
|
font-size: var(--dx-c-dropdown-option-font-size, var(--dx-g-text-base));
|
|
90
|
+
|
|
91
|
+
/* Opt-in truncation: consumers set --dx-c-dropdown-option-white-space:
|
|
92
|
+
nowrap to truncate labels with an ellipsis instead of wrapping.
|
|
93
|
+
Defaults preserve today's wrapping behavior. */
|
|
94
|
+
white-space: var(--dx-c-dropdown-option-white-space, normal);
|
|
95
|
+
overflow: var(--dx-c-dropdown-option-overflow, visible);
|
|
96
|
+
overflow-wrap: var(--dx-c-dropdown-option-overflow-wrap, normal);
|
|
97
|
+
text-overflow: var(--dx-c-dropdown-option-text-overflow, clip);
|
|
75
98
|
}
|
|
76
99
|
|
|
77
100
|
.option-indented .option_label {
|
|
@@ -130,7 +153,10 @@
|
|
|
130
153
|
}
|
|
131
154
|
|
|
132
155
|
.option-fit-content.option-active .option_label {
|
|
133
|
-
color: var(
|
|
156
|
+
color: var(
|
|
157
|
+
--dx-c-dropdown-option-active-label-color,
|
|
158
|
+
var(--dx-g-blue-vibrant-50)
|
|
159
|
+
);
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
.option-fit-content .option_icon {
|
|
@@ -89,7 +89,15 @@ export default class Popover extends LightningElement {
|
|
|
89
89
|
|
|
90
90
|
this.dispatchEvent(new CustomEvent("open"));
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
this.clearOpenListenersTimer();
|
|
93
|
+
// Defer so the opening click is not treated as an outside click.
|
|
94
|
+
// Drop the callback if the popover closes before it runs, or the
|
|
95
|
+
// document listeners attach with nothing left to remove them.
|
|
96
|
+
this.openListenersTimer = setTimeout(() => {
|
|
97
|
+
this.openListenersTimer = undefined;
|
|
98
|
+
if (!this._open) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
93
101
|
this.setPosition();
|
|
94
102
|
this.attachListenersTopopover();
|
|
95
103
|
});
|
|
@@ -98,7 +106,15 @@ export default class Popover extends LightningElement {
|
|
|
98
106
|
|
|
99
107
|
@api
|
|
100
108
|
closePopover(focusControl: boolean = false) {
|
|
109
|
+
this.clearOpenListenersTimer();
|
|
110
|
+
if (!this._open) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
101
113
|
this._open = false;
|
|
114
|
+
// Hide before aria-expanded changes. :has([aria-expanded]) / :focus-within
|
|
115
|
+
// stacking rules would otherwise drop position:relative while this menu
|
|
116
|
+
// is still visible, flashing it against a different offset parent.
|
|
117
|
+
this.popover?.classList.remove("popover-container_open");
|
|
102
118
|
this.autoUpdateCleanup?.();
|
|
103
119
|
if (focusControl && this.control && this.control.focus) {
|
|
104
120
|
this.control.focus();
|
|
@@ -106,20 +122,28 @@ export default class Popover extends LightningElement {
|
|
|
106
122
|
if (this.control) {
|
|
107
123
|
this.control.setAttribute("aria-expanded", "false");
|
|
108
124
|
}
|
|
109
|
-
document.removeEventListener(
|
|
125
|
+
document.removeEventListener(
|
|
126
|
+
"click",
|
|
127
|
+
this.interactOutsideListener,
|
|
128
|
+
true
|
|
129
|
+
);
|
|
110
130
|
document.removeEventListener("focusout", this.interactOutsideListener);
|
|
131
|
+
document.removeEventListener("keydown", this.onDocumentKeydown, true);
|
|
111
132
|
if (this.openOnHover) {
|
|
112
133
|
document.removeEventListener(
|
|
113
134
|
"mousemove",
|
|
114
135
|
this.interactOutsideListener
|
|
115
136
|
);
|
|
116
137
|
}
|
|
138
|
+
this.pointerOpened = false;
|
|
117
139
|
this.dispatchEvent(new CustomEvent("close"));
|
|
118
140
|
}
|
|
119
141
|
|
|
120
142
|
// PRIVATE VARS
|
|
121
143
|
|
|
122
144
|
private controlled: boolean = false;
|
|
145
|
+
private pointerOpened: boolean = false;
|
|
146
|
+
private openListenersTimer?: ReturnType<typeof setTimeout>;
|
|
123
147
|
private _open: boolean = false;
|
|
124
148
|
private _rendered: boolean = isPrerender();
|
|
125
149
|
private _role: string | null = null;
|
|
@@ -132,9 +156,15 @@ export default class Popover extends LightningElement {
|
|
|
132
156
|
// LIFECYCLE METHODS
|
|
133
157
|
|
|
134
158
|
disconnectedCallback() {
|
|
135
|
-
|
|
159
|
+
this.clearOpenListenersTimer();
|
|
160
|
+
document.removeEventListener(
|
|
161
|
+
"click",
|
|
162
|
+
this.interactOutsideListener,
|
|
163
|
+
true
|
|
164
|
+
);
|
|
136
165
|
document.removeEventListener("mousemove", this.interactOutsideListener);
|
|
137
166
|
document.removeEventListener("focusout", this.interactOutsideListener);
|
|
167
|
+
document.removeEventListener("keydown", this.onDocumentKeydown, true);
|
|
138
168
|
this.removeControlListeners();
|
|
139
169
|
}
|
|
140
170
|
|
|
@@ -226,17 +256,91 @@ export default class Popover extends LightningElement {
|
|
|
226
256
|
switch (e.key) {
|
|
227
257
|
case "ArrowDown": {
|
|
228
258
|
e.preventDefault();
|
|
259
|
+
this.pointerOpened = false;
|
|
229
260
|
if (!this.open) {
|
|
230
261
|
this.openPopover();
|
|
231
262
|
}
|
|
232
263
|
this.dispatchEvent(new CustomEvent("controlarrowdown"));
|
|
233
264
|
break;
|
|
234
265
|
}
|
|
266
|
+
case "Esc":
|
|
267
|
+
case "Escape":
|
|
268
|
+
this.closeOnEscape(e);
|
|
269
|
+
break;
|
|
235
270
|
default:
|
|
236
271
|
}
|
|
237
272
|
};
|
|
238
273
|
|
|
239
|
-
|
|
274
|
+
// Capture on document: click-open leaves focus on the trigger, and slotted
|
|
275
|
+
// menu options live outside this shadow tree, so the panel's onkeydown
|
|
276
|
+
// never sees Escape. Only the innermost popover in the event path handles
|
|
277
|
+
// it, so Escape in another control does not close this menu or cancel that
|
|
278
|
+
// control's own Escape behavior.
|
|
279
|
+
private onDocumentKeydown = (e: KeyboardEvent) => {
|
|
280
|
+
if ((e.key !== "Escape" && e.key !== "Esc") || !this.handlesEscape(e)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.closeOnEscape(e);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
private handlesEscape(e: KeyboardEvent): boolean {
|
|
287
|
+
if (!this._open || e.defaultPrevented) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const host = this.template.host;
|
|
291
|
+
const path = e.composedPath();
|
|
292
|
+
for (const node of path) {
|
|
293
|
+
if (node instanceof Element && node.localName === "dx-popover") {
|
|
294
|
+
return node === host;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// A non-composed event fired in this shadow or on the trigger does
|
|
298
|
+
// not list the host. The panel and control listeners still apply.
|
|
299
|
+
return (
|
|
300
|
+
(this.popover !== null && path.includes(this.popover)) ||
|
|
301
|
+
(this.control !== null && path.includes(this.control))
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private clearOpenListenersTimer() {
|
|
306
|
+
if (this.openListenersTimer !== undefined) {
|
|
307
|
+
clearTimeout(this.openListenersTimer);
|
|
308
|
+
this.openListenersTimer = undefined;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private closeOnEscape(e: KeyboardEvent) {
|
|
313
|
+
if (!this.open) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
e.preventDefault();
|
|
317
|
+
this.dispatchRequestCloseEvent("keypress-escape");
|
|
318
|
+
// Click-open + Escape upgrades :focus-visible on the trigger. Blur so
|
|
319
|
+
// the closed picker shows hover (pointer still over it) or default,
|
|
320
|
+
// not the keyboard halo. Keyboard-opened menus still restore focus.
|
|
321
|
+
const openedWithPointer = this.pointerOpened;
|
|
322
|
+
this.closePopover(!openedWithPointer);
|
|
323
|
+
if (openedWithPointer) {
|
|
324
|
+
this.blurControl();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private blurControl() {
|
|
329
|
+
const host = this.control;
|
|
330
|
+
if (!host) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const inner = host.shadowRoot?.activeElement as HTMLElement | null;
|
|
334
|
+
if (inner && typeof inner.blur === "function") {
|
|
335
|
+
inner.blur();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (typeof host.blur === "function") {
|
|
339
|
+
host.blur();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private onControlClick = (e: MouseEvent) => {
|
|
240
344
|
if (this.controlled && this.open) {
|
|
241
345
|
this.dispatchRequestCloseEvent("click-control");
|
|
242
346
|
return;
|
|
@@ -257,6 +361,10 @@ export default class Popover extends LightningElement {
|
|
|
257
361
|
return;
|
|
258
362
|
}
|
|
259
363
|
|
|
364
|
+
// Enter/Space on a button synthesizes a click with detail 0. Only a
|
|
365
|
+
// pointer click should blur the trigger on Escape; keyboard activation
|
|
366
|
+
// must leave focus on the control.
|
|
367
|
+
this.pointerOpened = e.detail > 0;
|
|
260
368
|
this.openPopover();
|
|
261
369
|
};
|
|
262
370
|
|
|
@@ -287,16 +395,20 @@ export default class Popover extends LightningElement {
|
|
|
287
395
|
switch (e.key) {
|
|
288
396
|
case "Esc":
|
|
289
397
|
case "Escape":
|
|
290
|
-
this.
|
|
291
|
-
this.closePopover(true);
|
|
398
|
+
this.closeOnEscape(e);
|
|
292
399
|
break;
|
|
293
400
|
default:
|
|
294
401
|
}
|
|
295
402
|
}
|
|
296
403
|
|
|
297
404
|
private attachListenersTopopover() {
|
|
298
|
-
|
|
405
|
+
// Capture so a click still counts as outside when a descendant
|
|
406
|
+
// calls stopPropagation (topic version pickers do, so the tree row
|
|
407
|
+
// does not navigate). Without this, opening a second picker leaves
|
|
408
|
+
// the first menu open.
|
|
409
|
+
document.addEventListener("click", this.interactOutsideListener, true);
|
|
299
410
|
document.addEventListener("focusout", this.interactOutsideListener);
|
|
411
|
+
document.addEventListener("keydown", this.onDocumentKeydown, true);
|
|
300
412
|
if (this.openOnHover) {
|
|
301
413
|
document.addEventListener(
|
|
302
414
|
"mousemove",
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { InternalTreeNode, TreeNode } from "typings/custom";
|
|
2
|
+
import type { TopicVersionOption } from "dxUtils/topicVersion";
|
|
3
|
+
import {
|
|
4
|
+
applyVersionToHref,
|
|
5
|
+
normalizePath,
|
|
6
|
+
resolveSelectedVersion
|
|
7
|
+
} from "dxUtils/topicVersion";
|
|
8
|
+
|
|
9
|
+
type VersionedTreeNode = TreeNode & { versions?: TopicVersionOption[] };
|
|
10
|
+
type VersionedInternalTreeNode = InternalTreeNode & {
|
|
11
|
+
versions?: TopicVersionOption[];
|
|
12
|
+
};
|
|
13
|
+
|
|
2
14
|
type MapItem = {
|
|
3
|
-
node:
|
|
15
|
+
node: VersionedInternalTreeNode;
|
|
4
16
|
parent?: MapItem;
|
|
5
17
|
setExpanded?: Function;
|
|
6
18
|
};
|
|
@@ -21,7 +33,10 @@ export class TreeHandler {
|
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
getKeyByName(name: string): string | undefined {
|
|
24
|
-
|
|
36
|
+
if (!name) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return this._nameKeyMap[name] || this._nameKeyMap[normalizePath(name)];
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
initialize(treeRoot: TreeNode): InternalTreeNode {
|
|
@@ -48,12 +63,41 @@ export class TreeHandler {
|
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
private convert(
|
|
51
|
-
node:
|
|
66
|
+
node: VersionedTreeNode,
|
|
52
67
|
level: number = 0,
|
|
53
68
|
parent?: MapItem,
|
|
54
|
-
position?: number
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
position?: number,
|
|
70
|
+
ancestorVersion?: {
|
|
71
|
+
selected: TopicVersionOption;
|
|
72
|
+
versions: TopicVersionOption[];
|
|
73
|
+
}
|
|
74
|
+
): VersionedInternalTreeNode {
|
|
75
|
+
const ownSelected = node.versions?.length
|
|
76
|
+
? resolveSelectedVersion(node)
|
|
77
|
+
: undefined;
|
|
78
|
+
const versionContext =
|
|
79
|
+
ownSelected && node.versions
|
|
80
|
+
? { selected: ownSelected, versions: node.versions }
|
|
81
|
+
: ancestorVersion;
|
|
82
|
+
|
|
83
|
+
let link = node.link;
|
|
84
|
+
if (
|
|
85
|
+
versionContext &&
|
|
86
|
+
!node.versions?.length &&
|
|
87
|
+
link?.href &&
|
|
88
|
+
link.target !== "_blank"
|
|
89
|
+
) {
|
|
90
|
+
const rewritten = applyVersionToHref(
|
|
91
|
+
link.href,
|
|
92
|
+
versionContext.selected,
|
|
93
|
+
versionContext.versions
|
|
94
|
+
);
|
|
95
|
+
if (rewritten && rewritten !== link.href) {
|
|
96
|
+
link = { ...link, href: rewritten };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const convertedNode: VersionedInternalTreeNode = {
|
|
57
101
|
label: node.label,
|
|
58
102
|
name: node.name,
|
|
59
103
|
key: this.calculateKey(
|
|
@@ -63,24 +107,52 @@ export class TreeHandler {
|
|
|
63
107
|
level: level,
|
|
64
108
|
isExpanded: node.isExpanded || false,
|
|
65
109
|
isChildrenLoading: node.isChildrenLoading || false,
|
|
66
|
-
link
|
|
110
|
+
link,
|
|
67
111
|
method: node.method,
|
|
68
112
|
showForwardArrow: node.showForwardArrow,
|
|
69
|
-
redirectToChild: node.redirectToChild
|
|
113
|
+
redirectToChild: node.redirectToChild,
|
|
114
|
+
versions: node.versions
|
|
70
115
|
};
|
|
71
116
|
|
|
72
117
|
const mapItem = { node: convertedNode, parent };
|
|
73
118
|
if (node.children && node.children.length > 0) {
|
|
74
119
|
convertedNode.children = node.children.map((child, index) =>
|
|
75
|
-
this.convert(child, level + 1, mapItem, index)
|
|
120
|
+
this.convert(child, level + 1, mapItem, index, versionContext)
|
|
76
121
|
);
|
|
77
122
|
}
|
|
78
123
|
|
|
79
124
|
this._nodeMap[convertedNode.key] = mapItem;
|
|
80
|
-
this.
|
|
125
|
+
this.indexName(convertedNode.name, convertedNode.key);
|
|
126
|
+
// A redirect parent can carry a child's href. This index runs after
|
|
127
|
+
// the child, so recording it would make the parent win selection.
|
|
128
|
+
if (!convertedNode.redirectToChild) {
|
|
129
|
+
this.indexName(convertedNode.link?.href, convertedNode.key);
|
|
130
|
+
}
|
|
131
|
+
// Index this topic's own versioned URLs only. Parent-only switching
|
|
132
|
+
// URLs point at a child with content and must not steal that child's
|
|
133
|
+
// selection.
|
|
134
|
+
if (node.name && node.versions) {
|
|
135
|
+
for (const version of node.versions) {
|
|
136
|
+
this.indexName(
|
|
137
|
+
applyVersionToHref(node.name, version, node.versions),
|
|
138
|
+
convertedNode.key
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
81
142
|
return convertedNode;
|
|
82
143
|
}
|
|
83
144
|
|
|
145
|
+
private indexName(name: string | undefined, key: string): void {
|
|
146
|
+
if (!name) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this._nameKeyMap[name] = key;
|
|
150
|
+
const normalized = normalizePath(name);
|
|
151
|
+
if (normalized && normalized !== name) {
|
|
152
|
+
this._nameKeyMap[normalized] = key;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
84
156
|
private calculateKey(parentKey: string, position?: number): string {
|
|
85
157
|
if (position === undefined) {
|
|
86
158
|
return parentKey;
|
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
:host {
|
|
4
4
|
display: block;
|
|
5
|
+
|
|
6
|
+
/* Keep a stable containing block; z-index still lifts the open menu. */
|
|
7
|
+
position: relative;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Lift this row above later sibling pickers while its menu is open. */
|
|
11
|
+
:host(:focus-within) {
|
|
12
|
+
z-index: 2;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.tree-item-row {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
min-width: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.tree-item-row dx-tree-tile {
|
|
22
|
+
flex: 1;
|
|
23
|
+
min-width: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tree-item-version-picker {
|
|
27
|
+
flex: 0 0 auto;
|
|
28
|
+
margin-right: var(--dx-g-spacing-lg);
|
|
29
|
+
overflow: visible;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.tree-item-row-selected {
|
|
33
|
+
background: var(--dx-g-cloud-blue-vibrant-95);
|
|
34
|
+
box-shadow: inset var(--dx-g-spacing-xs) 0 0 0 var(--dx-g-blue-vibrant-40);
|
|
5
35
|
}
|
|
6
36
|
|
|
7
37
|
button {
|