@salesforcedevs/arch-components 1.20.17-alpha13 → 1.20.17-alpha15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/assets/css/arch-variables.css +512 -0
- package/src/modules/arch/badge/badge.css +22 -0
- package/src/modules/arch/badge/badge.html +5 -0
- package/src/modules/arch/badge/badge.ts +9 -0
- package/src/modules/arch/button/button.css +1 -0
- package/src/modules/arch/button/button.html +20 -0
- package/src/modules/arch/button/button.ts +67 -0
- package/src/modules/arch/buttonLink/buttonLink.css +1 -0
- package/src/modules/arch/buttonLink/buttonLink.html +19 -0
- package/src/modules/arch/buttonLink/buttonLink.stories.js +24 -0
- package/src/modules/arch/buttonLink/buttonLink.ts +8 -0
- package/src/modules/arch/buttonStyles/buttonStyles.css +220 -0
- package/src/modules/arch/card/card.css +128 -0
- package/src/modules/arch/card/card.html +85 -0
- package/src/modules/arch/card/card.ts +277 -0
- package/src/modules/arch/cardBase/cardBase.css +11 -0
- package/src/modules/arch/cardBase/cardBase.html +2 -0
- package/src/modules/arch/cardGridA/cardGridA.css +11 -0
- package/src/modules/arch/cardGridA/cardGridA.html +21 -0
- package/src/modules/arch/cardGridA/cardGridA.stories.js +107 -0
- package/src/modules/arch/cardGridA/cardGridA.ts +24 -0
- package/src/modules/arch/cardGridC/cardGridC.css +24 -0
- package/src/modules/arch/cardGridC/cardGridC.html +22 -0
- package/src/modules/arch/cardGridC/cardGridC.stories.js +42 -0
- package/src/modules/arch/cardGridC/cardGridC.ts +11 -0
- package/src/modules/arch/cardGridD/cardGridD.css +17 -0
- package/src/modules/arch/cardGridD/cardGridD.html +20 -0
- package/src/modules/arch/cardGridD/cardGridD.stories.js +34 -0
- package/src/modules/arch/cardGridD/cardGridD.ts +7 -0
- package/src/modules/arch/cardNew/cardNew.css +31 -0
- package/src/modules/arch/cardNew/cardNew.html +32 -0
- package/src/modules/arch/cardNew/cardNew.ts +66 -0
- package/src/modules/arch/children/children.html +2 -0
- package/src/modules/arch/children/children.ts +31 -0
- package/src/modules/arch/color/color.ts +59 -0
- package/src/modules/arch/content/__fixtures__/index.ts +884 -0
- package/src/modules/arch/content/content.css +643 -0
- package/src/modules/arch/content/content.html +65 -0
- package/src/modules/arch/content/content.stories.js +14 -0
- package/src/modules/arch/content/content.ts +169 -0
- package/src/modules/arch/contentIcon/contentIcon.css +48 -0
- package/src/modules/arch/contentIcon/contentIcon.html +15 -0
- package/src/modules/arch/contentIcon/contentIcon.stories.js +110 -0
- package/src/modules/arch/contentIcon/contentIcon.ts +68 -0
- package/src/modules/arch/context/context.ts +21 -19
- package/src/modules/arch/contextAdapter/constants.ts +1 -0
- package/src/modules/arch/contextAdapter/contextAdapter.html +1 -0
- package/src/modules/arch/contextAdapter/contextAdapter.ts +54 -0
- package/src/modules/arch/debounce/debounce.ts +32 -0
- package/src/modules/arch/dialog/dialog.ts +154 -0
- package/src/modules/arch/dialogStyles/dialogStyles.css +90 -0
- package/src/modules/arch/effectAdapter/effectAdapter.ts +19 -9
- package/src/modules/arch/explorer/explorer.css +301 -0
- package/src/modules/arch/explorer/explorer.html +418 -0
- package/src/modules/arch/explorer/explorer.ts +718 -0
- package/src/modules/arch/explorer/types.d.ts +60 -0
- package/src/modules/arch/fetch/fetch.ts +55 -0
- package/src/modules/arch/footerMfe/footerMfe.html +3 -0
- package/src/modules/arch/footerMfe/footerMfe.ts +19 -0
- package/src/modules/arch/gallery/gallery.css +365 -0
- package/src/modules/arch/gallery/gallery.html +71 -0
- package/src/modules/arch/gallery/gallery.ts +366 -0
- package/src/modules/arch/gallery/types.d.ts +35 -0
- package/src/modules/arch/heading/heading.css +1 -0
- package/src/modules/arch/heading/heading.html +9 -0
- package/src/modules/arch/heading/heading.ts +36 -0
- package/src/modules/arch/helpers/helpers.ts +141 -0
- package/src/modules/arch/heroA/heroA.css +116 -0
- package/src/modules/arch/heroA/heroA.html +28 -0
- package/src/modules/arch/heroA/heroA.stories.js +49 -0
- package/src/modules/arch/heroA/heroA.ts +53 -0
- package/src/modules/arch/heroB/heroB.css +79 -0
- package/src/modules/arch/heroB/heroB.html +27 -0
- package/src/modules/arch/heroB/heroB.stories.js +44 -0
- package/src/modules/arch/heroB/heroB.ts +26 -0
- package/src/modules/arch/i18n/i18n.ts +78 -0
- package/src/modules/arch/icon/icon.css +28 -0
- package/src/modules/arch/icon/icon.html +17 -0
- package/src/modules/arch/icon/icon.stories.js +18 -0
- package/src/modules/arch/icon/icon.ts +92 -0
- package/src/modules/arch/instrumentation/instrumentation.css +1 -0
- package/src/modules/arch/instrumentation/instrumentation.html +1 -0
- package/src/modules/arch/instrumentation/instrumentation.ts +113 -0
- package/src/modules/arch/labels/helpers.ts +25 -0
- package/src/modules/arch/labels/pointHelpers.ts +47 -0
- package/src/modules/arch/labels/timeHelpers.ts +182 -0
- package/src/modules/arch/labels/types.d.ts +5 -0
- package/src/modules/arch/logger/logger.ts +33 -0
- package/src/modules/arch/menu/menu.ts +260 -0
- package/src/modules/arch/overflow/overflow.ts +71 -0
- package/src/modules/arch/page/page.css +3 -0
- package/src/modules/arch/page/page.html +3 -0
- package/src/modules/arch/page/page.stories.js +10 -0
- package/src/modules/arch/page/page.ts +3 -0
- package/src/modules/arch/pageHeaderA/pageHeaderA.css +82 -0
- package/src/modules/arch/pageHeaderA/pageHeaderA.html +24 -0
- package/src/modules/arch/pageHeaderA/pageHeaderA.stories.js +18 -0
- package/src/modules/arch/pageHeaderA/pageHeaderA.ts +51 -0
- package/src/modules/arch/pill/pill.css +70 -0
- package/src/modules/arch/pill/pill.html +17 -0
- package/src/modules/arch/pill/pill.ts +34 -0
- package/src/modules/arch/polling-request.ts +97 -0
- package/src/modules/arch/reflectedElement/reflectedElement.html +2 -0
- package/src/modules/arch/reflectedElement/reflectedElement.ts +5 -3
- package/src/modules/arch/reset/reset.css +39 -0
- package/src/modules/arch/searchList/searchList.css +120 -0
- package/src/modules/arch/searchList/searchList.html +46 -0
- package/src/modules/arch/searchList/searchList.ts +53 -0
- package/src/modules/arch/sectionA/sectionA.css +64 -0
- package/src/modules/arch/sectionA/sectionA.html +21 -0
- package/src/modules/arch/sectionA/sectionA.stories.js +18 -0
- package/src/modules/arch/sectionA/sectionA.ts +27 -0
- package/src/modules/arch/select/select.css +40 -0
- package/src/modules/arch/select/select.html +24 -0
- package/src/modules/arch/select/select.ts +64 -0
- package/src/modules/arch/socialShare/socialShare.css +50 -0
- package/src/modules/arch/socialShare/socialShare.html +56 -0
- package/src/modules/arch/socialShare/socialShare.ts +29 -0
- package/src/modules/arch/spinner/spinner.css +195 -0
- package/src/modules/arch/spinner/spinner.html +9 -0
- package/src/modules/arch/spinner/spinner.ts +15 -0
- package/src/modules/arch/styles/styles.css +24 -0
- package/src/modules/arch/summary/summary.css +134 -0
- package/src/modules/arch/summary/summary.html +71 -0
- package/src/modules/arch/summary/summary.stories.js +148 -0
- package/src/modules/arch/summary/summary.ts +96 -0
- package/src/modules/arch/tab/tab.css +3 -0
- package/src/modules/arch/tab/tab.html +5 -0
- package/src/modules/arch/tab/tab.ts +46 -0
- package/src/modules/arch/tabset/tabset.css +112 -0
- package/src/modules/arch/tabset/tabset.html +62 -0
- package/src/modules/arch/tabset/tabset.ts +244 -0
- package/src/modules/arch/testutils.ts +118 -0
- package/src/modules/arch/threeCardGrid/threeCardGrid.css +6 -0
- package/src/modules/arch/threeCardGrid/threeCardGrid.html +5 -0
- package/src/modules/arch/threeCardGrid/threeCardGrid.ts +3 -0
- package/src/modules/arch/track/track.ts +23 -0
- package/src/modules/arch/trailhead.ts +120 -0
- package/src/modules/arch/types.d.ts +1 -0
- package/src/modules/arch/useEffectAttr.ts +16 -0
- package/src/modules/arch/utils/utils.ts +20 -0
- package/src/modules/arch/withState.ts +21 -0
- package/src/modules/arch/xsfMfeEvents/xsfMfeEvents.html +1 -0
- package/src/modules/arch/xsfMfeEvents/xsfMfeEvents.ts +47 -0
- package/src/modules/arch/slot/slot.ts +0 -20
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { LightningElement, api, track } from "lwc";
|
|
2
|
+
import classnames from "classnames";
|
|
3
|
+
import { TabElement } from "./../tab/tab";
|
|
4
|
+
import { Menu } from "arch/menu";
|
|
5
|
+
import { calculateOverflow } from "arch/overflow";
|
|
6
|
+
|
|
7
|
+
export type Tab = TabElement & {
|
|
8
|
+
active: boolean;
|
|
9
|
+
className: string;
|
|
10
|
+
hidden: boolean;
|
|
11
|
+
labelId: string | null;
|
|
12
|
+
onselect: () => void;
|
|
13
|
+
onmenuselect: () => void;
|
|
14
|
+
overflow: boolean;
|
|
15
|
+
tabindex: number;
|
|
16
|
+
width: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default class extends LightningElement {
|
|
20
|
+
@track private tabs: Tab[] = [];
|
|
21
|
+
|
|
22
|
+
private get activeTab() {
|
|
23
|
+
return this.tabs.find((t) => t.active) || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private _activeTabValue: string | null = null;
|
|
27
|
+
|
|
28
|
+
@api
|
|
29
|
+
get activeTabValue() {
|
|
30
|
+
return this._activeTabValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
set activeTabValue(newValue) {
|
|
34
|
+
this.setActiveTabValueInternal(newValue);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private setActiveTabValueInternal(newValue: string | null) {
|
|
38
|
+
// Already selected, do nothing
|
|
39
|
+
if (!newValue || this._activeTabValue === newValue) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Unload previously active tab
|
|
44
|
+
if (this.activeTab) {
|
|
45
|
+
this.activeTab.unload();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Update tab properties
|
|
49
|
+
this._activeTabValue = newValue;
|
|
50
|
+
|
|
51
|
+
this.tabs = this.tabs.map((t) => ({
|
|
52
|
+
...t,
|
|
53
|
+
...this.calculatedTabProperties(t.value === newValue)
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// Load the new tab
|
|
57
|
+
if (this.activeTab) {
|
|
58
|
+
this.activeTab.load();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private hasOverflow: boolean = false;
|
|
63
|
+
|
|
64
|
+
private menu = new Menu({
|
|
65
|
+
onChange: () => {},
|
|
66
|
+
shadowRoot: this.template
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
connectedCallback() {
|
|
70
|
+
window.addEventListener("resize", this.handleWindowResize);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
renderedCallback() {
|
|
74
|
+
// Set the first tab as active by default
|
|
75
|
+
if (!this.activeTabValue && this.tabs.length) {
|
|
76
|
+
this.setActiveTabValueInternal(this.tabs[0].value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Repaint
|
|
80
|
+
requestAnimationFrame(this.detectOverflow.bind(this));
|
|
81
|
+
this.menu.renderedCallback();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
disconnectedCallback() {
|
|
85
|
+
window.removeEventListener("resize", this.handleWindowResize);
|
|
86
|
+
this.menu.disconnectedCallback();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// TODO i18n
|
|
90
|
+
private get labels() {
|
|
91
|
+
return {
|
|
92
|
+
overflowButton: "More"
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private get isOverflowHidden() {
|
|
97
|
+
return !this.hasOverflow;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private calculatedTabProperties(isActive: boolean) {
|
|
101
|
+
return {
|
|
102
|
+
active: isActive,
|
|
103
|
+
className: classnames("tab", {
|
|
104
|
+
"tab-active": isActive
|
|
105
|
+
}),
|
|
106
|
+
labelId: isActive ? "active-tab" : null,
|
|
107
|
+
hidden: !isActive,
|
|
108
|
+
tabindex: isActive ? 0 : -1
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Detect if tabs need to be placed in an overflow menu
|
|
113
|
+
private detectOverflow() {
|
|
114
|
+
const tabs = Array.from(
|
|
115
|
+
this.template.querySelectorAll('.tab[role="presentation"]')
|
|
116
|
+
) as HTMLElement[];
|
|
117
|
+
|
|
118
|
+
// Update tab.width
|
|
119
|
+
tabs.forEach((t) => {
|
|
120
|
+
const value = t.querySelector("a")!.getAttribute("data-value");
|
|
121
|
+
const tab = this.tabs.find((tabItem) => tabItem.value === value);
|
|
122
|
+
if (tab) {
|
|
123
|
+
const { paddingLeft, width, paddingRight } =
|
|
124
|
+
getComputedStyle(t);
|
|
125
|
+
tab.width =
|
|
126
|
+
parseInt(paddingLeft, 10) +
|
|
127
|
+
parseInt(width, 10) +
|
|
128
|
+
parseInt(paddingRight, 10);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Calculate which items need to be placed in overflow
|
|
133
|
+
const { overflowItems } = calculateOverflow(
|
|
134
|
+
this.tabs,
|
|
135
|
+
this.template.querySelector(".tablist")!.getBoundingClientRect()
|
|
136
|
+
.width,
|
|
137
|
+
this.template
|
|
138
|
+
.querySelector(".overflow-tab")!
|
|
139
|
+
.getBoundingClientRect().width
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
this.hasOverflow = !!overflowItems.length;
|
|
143
|
+
|
|
144
|
+
// Update tab.overflow
|
|
145
|
+
this.tabs.forEach((i) => {
|
|
146
|
+
i.overflow = !!overflowItems.find((o) => o.value === i.value);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private handleWindowResize = () => {
|
|
151
|
+
this.detectOverflow();
|
|
152
|
+
|
|
153
|
+
if (this.menu) {
|
|
154
|
+
this.menu.close();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
private handleTabRegister(e: { detail: TabElement }) {
|
|
159
|
+
const element = e.detail;
|
|
160
|
+
|
|
161
|
+
const tab: Tab = {
|
|
162
|
+
...this.calculatedTabProperties(
|
|
163
|
+
element.value === this.activeTabValue
|
|
164
|
+
),
|
|
165
|
+
...element,
|
|
166
|
+
onselect: () => {
|
|
167
|
+
this.setActiveTabValueInternal(element.value);
|
|
168
|
+
},
|
|
169
|
+
onmenuselect: () => {
|
|
170
|
+
this.setActiveTabValueInternal(element.value);
|
|
171
|
+
|
|
172
|
+
// Re-focus the menu button
|
|
173
|
+
this.template.querySelector('[data-menu="trigger"]')!.focus();
|
|
174
|
+
|
|
175
|
+
if (this.menu) {
|
|
176
|
+
this.menu.close();
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
overflow: false,
|
|
180
|
+
width: 0
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
this.tabs.push(tab);
|
|
184
|
+
|
|
185
|
+
// Load the tab if it's currently active
|
|
186
|
+
if (element.value === this.activeTabValue) {
|
|
187
|
+
tab.load();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private handleKeyDown(e: KeyboardEvent) {
|
|
192
|
+
switch (e.code) {
|
|
193
|
+
// Cycle through tabs
|
|
194
|
+
case "ArrowLeft":
|
|
195
|
+
case "ArrowRight": {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
e.stopPropagation();
|
|
198
|
+
|
|
199
|
+
const increment = e.code === "ArrowLeft" ? -1 : 1;
|
|
200
|
+
const visibleTabs = this.tabs.filter((t) => !t.overflow);
|
|
201
|
+
const currentFocusedIndex = visibleTabs.findIndex(
|
|
202
|
+
(tabItem) => tabItem.value === this.activeTabValue
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
let newIndex = currentFocusedIndex + increment;
|
|
206
|
+
if (newIndex < 0) {
|
|
207
|
+
newIndex = visibleTabs.length - 1;
|
|
208
|
+
}
|
|
209
|
+
if (newIndex + 1 > visibleTabs.length) {
|
|
210
|
+
newIndex = 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Set the active tabpanel and focus the tab
|
|
214
|
+
const newValue = visibleTabs[newIndex].value;
|
|
215
|
+
this.setActiveTabValueInternal(newValue);
|
|
216
|
+
this.template
|
|
217
|
+
.querySelector(`a[data-value=${newValue}]`)!
|
|
218
|
+
.focus();
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case "ArrowDown":
|
|
222
|
+
// Focus the tabpanel
|
|
223
|
+
this.template.querySelector('[role="tabpanel"]')!.focus();
|
|
224
|
+
break;
|
|
225
|
+
case "Enter":
|
|
226
|
+
case "Space": {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
e.stopPropagation();
|
|
229
|
+
|
|
230
|
+
const element = e.target as HTMLElement;
|
|
231
|
+
|
|
232
|
+
if (element && element.nodeName === "A") {
|
|
233
|
+
const newValue = element.getAttribute("data-value");
|
|
234
|
+
if (newValue) {
|
|
235
|
+
this.setActiveTabValueInternal(newValue);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
default:
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { html, render, TemplateResult } from 'lit-html';
|
|
2
|
+
import { querySelectorAll, querySelector } from 'kagekiri';
|
|
3
|
+
// @ts-ignore we are using a beta feature "setFeatureFlagForTest" that tsc doesn't like
|
|
4
|
+
import { createElement, LightningElement, setFeatureFlagForTest } from 'lwc';
|
|
5
|
+
|
|
6
|
+
export { html, render };
|
|
7
|
+
|
|
8
|
+
export function nextTick() {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
process.nextTick(resolve);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function $(selector: string, element: HTMLElement) {
|
|
15
|
+
return element.shadowRoot!.querySelectorAll(selector);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function $testid(value: string, element: HTMLElement) {
|
|
19
|
+
return $(`[data-testid="${value}"]`, element);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function renderIntoBody(template: TemplateResult) {
|
|
23
|
+
const element = document.createElement('div');
|
|
24
|
+
document.body.append(element);
|
|
25
|
+
render(template, element);
|
|
26
|
+
return element.children[0] as HTMLElement;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function renderIntoBodyAndWait(
|
|
30
|
+
template: TemplateResult,
|
|
31
|
+
ready:
|
|
32
|
+
| ((params: {
|
|
33
|
+
querySelector: typeof querySelector;
|
|
34
|
+
querySelectorAll: typeof querySelectorAll;
|
|
35
|
+
}) => boolean)
|
|
36
|
+
| string
|
|
37
|
+
) {
|
|
38
|
+
const element = renderIntoBody(template);
|
|
39
|
+
await new Promise((resolve) => {
|
|
40
|
+
const check = () => {
|
|
41
|
+
const isReady =
|
|
42
|
+
typeof ready === 'string'
|
|
43
|
+
? querySelector(ready, element) !== null
|
|
44
|
+
: ready({ querySelector, querySelectorAll });
|
|
45
|
+
if (isReady) {
|
|
46
|
+
resolve();
|
|
47
|
+
} else {
|
|
48
|
+
requestAnimationFrame(check);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
requestAnimationFrame(check);
|
|
52
|
+
});
|
|
53
|
+
return element;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// LWC Component Testing Utilities
|
|
57
|
+
export const renderLightningElement = (
|
|
58
|
+
element: LightningElement,
|
|
59
|
+
properties = {}
|
|
60
|
+
) => {
|
|
61
|
+
Object.assign(element, properties);
|
|
62
|
+
document.body.appendChild(element as unknown as Node);
|
|
63
|
+
return element;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const createRenderComponent = (
|
|
67
|
+
tag: string,
|
|
68
|
+
LWC: new () => LightningElement
|
|
69
|
+
) => {
|
|
70
|
+
setFeatureFlagForTest('ENABLE_LIGHT_DOM_COMPONENTS', true);
|
|
71
|
+
return (properties = {}) => {
|
|
72
|
+
const component = createElement(tag, {
|
|
73
|
+
is: LWC
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
renderLightningElement(component, properties);
|
|
77
|
+
return component;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const mockSlottedElement = <T extends HTMLElement>(
|
|
82
|
+
element: any,
|
|
83
|
+
elementType = 'button',
|
|
84
|
+
slotName?: string
|
|
85
|
+
): { slottedElement: T; slot: HTMLSlotElement } => {
|
|
86
|
+
const slot: HTMLSlotElement = element.shadowRoot.querySelector(
|
|
87
|
+
`slot${slotName ? `[name='${slotName}']` : ''}`
|
|
88
|
+
);
|
|
89
|
+
expect(slot).not.toBeNull();
|
|
90
|
+
|
|
91
|
+
const slottedElement = document.createElement(elementType) as T;
|
|
92
|
+
slot.assignedElements = jest.fn(() => [slottedElement]);
|
|
93
|
+
slot.dispatchEvent(new Event('slotchange'));
|
|
94
|
+
|
|
95
|
+
return { slottedElement, slot };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const wait = () => new Promise((res) => setTimeout(res, 0));
|
|
99
|
+
|
|
100
|
+
const renderAllShadowDoms = (element: {
|
|
101
|
+
shadowRoot: { children: HTMLElement };
|
|
102
|
+
}) => {
|
|
103
|
+
if (element.shadowRoot) {
|
|
104
|
+
return element.shadowRoot.children;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return element;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const toSnapshot = async <T extends Object>(
|
|
111
|
+
renderFn: Function,
|
|
112
|
+
props?: T
|
|
113
|
+
) => {
|
|
114
|
+
const element = renderFn(props);
|
|
115
|
+
await wait();
|
|
116
|
+
const doc = renderAllShadowDoms(element);
|
|
117
|
+
return expect(doc).toMatchSnapshot();
|
|
118
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type TrackingPayload = {
|
|
2
|
+
label?: string;
|
|
3
|
+
ctaType?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const track = (
|
|
7
|
+
element: HTMLElement,
|
|
8
|
+
eventName: string,
|
|
9
|
+
payload: TrackingPayload
|
|
10
|
+
) => {
|
|
11
|
+
const trackEvent = new CustomEvent('trailhead_track', {
|
|
12
|
+
bubbles: true,
|
|
13
|
+
composed: true,
|
|
14
|
+
detail: {
|
|
15
|
+
eventName,
|
|
16
|
+
payload: {
|
|
17
|
+
...payload,
|
|
18
|
+
...{ pageLocation: element.tagName }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
element.dispatchEvent(trackEvent);
|
|
23
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Utilites for interacting with the Trailhead platform.
|
|
3
|
+
*/
|
|
4
|
+
import { fetch, fetchJSON, postJSON, csrfHeader } from './fetch/fetch';
|
|
5
|
+
import { PollingRequest, PollingRequestInit } from './polling-request';
|
|
6
|
+
import logger from './logger/logger';
|
|
7
|
+
import { Org } from '../modules/th/orgPicker/types';
|
|
8
|
+
|
|
9
|
+
const CSRF_WARNING =
|
|
10
|
+
'A CSRF token required for this request. Set it using fromTrailhead.configure().';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_BASE_URL = '';
|
|
13
|
+
|
|
14
|
+
type TrailheadAPIClientInit = {
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
csrfToken?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Client for interacting with Trailhead via API requests
|
|
21
|
+
* @example
|
|
22
|
+
* const fromTrailhead = new TrailheadAPIClient({ csrfToken: "XXX" });
|
|
23
|
+
* fromTrailhead.get("/user_orgs");
|
|
24
|
+
*/
|
|
25
|
+
export class TrailheadAPIClient {
|
|
26
|
+
baseUrl: string = DEFAULT_BASE_URL;
|
|
27
|
+
csrfToken: string | undefined;
|
|
28
|
+
|
|
29
|
+
constructor(args: TrailheadAPIClientInit = {}) {
|
|
30
|
+
this.configure(args);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
configure({
|
|
34
|
+
baseUrl = process.env.API_HOST,
|
|
35
|
+
csrfToken
|
|
36
|
+
}: TrailheadAPIClientInit) {
|
|
37
|
+
this.baseUrl = baseUrl || DEFAULT_BASE_URL;
|
|
38
|
+
this.csrfToken = csrfToken;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async get<T>(path: string) {
|
|
42
|
+
return fetchJSON<T>(this.makeUrl(path));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async post<T>(path: string, body?: object) {
|
|
46
|
+
this.checkRequest();
|
|
47
|
+
return postJSON<T>(this.makeUrl(path), body, {
|
|
48
|
+
headers: { ...csrfHeader(this.csrfToken) }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async patch<T>(path: string, body?: object) {
|
|
53
|
+
this.checkRequest();
|
|
54
|
+
return postJSON<T>(this.makeUrl(path), body, {
|
|
55
|
+
method: 'PATCH',
|
|
56
|
+
headers: {
|
|
57
|
+
...csrfHeader(this.csrfToken)
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async delete<T>(path: string) {
|
|
63
|
+
this.checkRequest();
|
|
64
|
+
return fetchJSON<T>(this.makeUrl(path), {
|
|
65
|
+
method: 'DELETE',
|
|
66
|
+
headers: { ...csrfHeader(this.csrfToken) }
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async poll<T>(args: PollingRequestInit<T>) {
|
|
71
|
+
return new PollingRequest<T>(args);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
makeUrl(path: string) {
|
|
75
|
+
return [this.baseUrl, path]
|
|
76
|
+
.map((s) => s.replace(/^\/|\/$/g, ''))
|
|
77
|
+
.join('/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private checkRequest() {
|
|
81
|
+
if (!this.csrfToken) {
|
|
82
|
+
logger.warn(CSRF_WARNING);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Determine if an org is reachable (after creation) by
|
|
89
|
+
* requesting open endpoints. The org is reachable if the
|
|
90
|
+
* user's client resolves the freshly propagated DNS entries.
|
|
91
|
+
*/
|
|
92
|
+
export async function isOrgReachable(org: Org): Promise<boolean> {
|
|
93
|
+
const subdomain = org.instance_url.split('.')[0];
|
|
94
|
+
|
|
95
|
+
const responses = await Promise.all([
|
|
96
|
+
fetch(
|
|
97
|
+
`${subdomain}.my.salesforce.com/.well-known/openid-configuration`
|
|
98
|
+
),
|
|
99
|
+
fetch(
|
|
100
|
+
`${subdomain}--c.documentforce.com/.well-known/openid-configuration`,
|
|
101
|
+
{
|
|
102
|
+
mode: 'no-cors'
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
fetch(
|
|
106
|
+
`${subdomain}--th-con-app.visualforce.com/.well-known/openid-configuration`,
|
|
107
|
+
{
|
|
108
|
+
mode: 'no-cors'
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
fetch(
|
|
112
|
+
`${subdomain}.lightning.force.com/.well-known/openid-configuration`,
|
|
113
|
+
{
|
|
114
|
+
mode: 'no-cors'
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
]).catch(() => Promise.resolve([{ status: 404 }]));
|
|
118
|
+
|
|
119
|
+
return responses.every((r) => r.status === 200 || r.status === 0);
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type BooleanAttr = boolean | string | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function useEffectAttr(fn: Function, deps: Array<string>, context: any) {
|
|
2
|
+
const prevValues = new Map<string, any>();
|
|
3
|
+
return () => {
|
|
4
|
+
const prev = deps.map((key) => prevValues.get(key));
|
|
5
|
+
const next = deps.map((key) => context[key]);
|
|
6
|
+
for (const i in prev) {
|
|
7
|
+
if (next[i] !== prev[i]) {
|
|
8
|
+
fn.call(context);
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
for (const k of deps) {
|
|
13
|
+
prevValues.set(k, context[k]);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function waitForPageSettled() {
|
|
2
|
+
return new Promise((resolve) => {
|
|
3
|
+
let prevH: number = 0;
|
|
4
|
+
const prevY = window.scrollY;
|
|
5
|
+
function check() {
|
|
6
|
+
const nextH = window.innerHeight + document.body.scrollHeight;
|
|
7
|
+
const nextY = window.scrollY;
|
|
8
|
+
const settled = nextH === prevH;
|
|
9
|
+
if (settled) {
|
|
10
|
+
if (nextY === prevY) {
|
|
11
|
+
resolve();
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
prevH = nextH;
|
|
16
|
+
requestAnimationFrame(check);
|
|
17
|
+
}
|
|
18
|
+
requestAnimationFrame(check);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { directive, NodePart } from 'lit-html';
|
|
2
|
+
|
|
3
|
+
export type WithStateState = any;
|
|
4
|
+
export type WithStateSetState = (state: any) => void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* lit-html directive for creating thin, stateful renderings
|
|
8
|
+
* @example
|
|
9
|
+
* html`${withState((state, setState) => html`state.msg`, { msg: 'hello!' })}`
|
|
10
|
+
*/
|
|
11
|
+
export default directive((render, initialState = {}) => (part: NodePart) => {
|
|
12
|
+
let prevState = initialState;
|
|
13
|
+
|
|
14
|
+
function setState(nextState: WithStateState) {
|
|
15
|
+
part.setValue(render({ ...prevState, ...nextState }, setState));
|
|
16
|
+
part.commit();
|
|
17
|
+
prevState = { ...prevState, ...nextState };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
part.setValue(render(initialState, setState));
|
|
21
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<template></template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { LightningElement } from 'lwc';
|
|
2
|
+
import { track } from 'arch/instrumentation';
|
|
3
|
+
|
|
4
|
+
type EventHandler = (event: Event) => void;
|
|
5
|
+
|
|
6
|
+
export default class XsfMfeEvents extends LightningElement {
|
|
7
|
+
connectedCallback() {
|
|
8
|
+
document.addEventListener(
|
|
9
|
+
'www_spasearch',
|
|
10
|
+
this.handleSearch as EventHandler
|
|
11
|
+
);
|
|
12
|
+
document.addEventListener('www_track', this.handleTrack as EventHandler);
|
|
13
|
+
document.addEventListener('toggle_global_nav', this.handleToggleGlobalNav as EventHandler);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
disconnectedCallback() {
|
|
17
|
+
document.removeEventListener(
|
|
18
|
+
'www_spasearch',
|
|
19
|
+
this.handleSearch as EventHandler
|
|
20
|
+
);
|
|
21
|
+
document.removeEventListener('www_track', this.handleTrack as EventHandler);
|
|
22
|
+
document.removeEventListener('toggle_global_nav', this.handleToggleGlobalNav as EventHandler);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
handleSearch = ({ target, type, detail }: CustomEvent) => {
|
|
26
|
+
const searchTerm = detail?.searchTerm;
|
|
27
|
+
track(target, type, { searchTerm });
|
|
28
|
+
if (searchTerm) {
|
|
29
|
+
window.location.href = `/search/?keywords=${encodeURIComponent(
|
|
30
|
+
searchTerm
|
|
31
|
+
)}`;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
handleTrack = ({ target, detail }: CustomEvent) => {
|
|
36
|
+
const { event: eventName, ...payload } = detail;
|
|
37
|
+
track(target as EventTarget, eventName, payload);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
handleToggleGlobalNav = ({ detail }: CustomEvent) => {
|
|
41
|
+
const isVisible = detail;
|
|
42
|
+
document.documentElement?.style.setProperty(
|
|
43
|
+
'--dx-g-global-header-nav-row-count',
|
|
44
|
+
isVisible ? '2' : '1'
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export function assignedSlotNames(template: {
|
|
2
|
-
querySelectorAll(selector: string): NodeList;
|
|
3
|
-
}) {
|
|
4
|
-
let slots = Array.from(
|
|
5
|
-
template.querySelectorAll('slot')
|
|
6
|
-
) as HTMLSlotElement[];
|
|
7
|
-
return slots
|
|
8
|
-
.filter(isSlotAssigned)
|
|
9
|
-
.map((slot) => slot.getAttribute('name') || 'default');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function isSlotAssigned(slot: HTMLSlotElement): boolean {
|
|
13
|
-
let [element] = slot.assignedElements();
|
|
14
|
-
if (element) {
|
|
15
|
-
if (element.tagName === 'SLOT')
|
|
16
|
-
return isSlotAssigned(element as HTMLSlotElement);
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
return slot.children.length > 0;
|
|
20
|
-
}
|