@salesforcedevs/dx-components 1.29.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -44,5 +44,5 @@
44
44
  "luxon": "3.4.4",
45
45
  "msw": "^2.12.4"
46
46
  },
47
- "gitHead": "6b290fbf6e3032a9708592af2932b43d5e970bfc"
47
+ "gitHead": "4027270163b2eb9610b911a09bfa31f536061fe4"
48
48
  }
@@ -275,3 +275,175 @@ footer.signup-variant-no-signup {
275
275
  width: 100%;
276
276
  }
277
277
  }
278
+
279
+ /*
280
+ ================================
281
+ BEGIN OLD FOOTER STYLES -- CAN BE DELETED ONCE FOOTER MFE IS RE-DEPLOYED
282
+ ================================
283
+ */
284
+ .term-icon {
285
+ width: 35px;
286
+ margin: 0 var(--dx-g-spacing-xs);
287
+ }
288
+
289
+ .content-container_middle {
290
+ display: grid;
291
+ grid-gap: var(--dx-g-spacing-xl);
292
+ grid-template:
293
+ "logo general" auto
294
+ "socials general" max-content
295
+ "locales general" max-content / auto max-content;
296
+ background: var(--dx-footer-themed-background-color);
297
+ padding-top: var(--dx-g-spacing-4xl);
298
+ padding-bottom: var(--dx-g-spacing-3xl);
299
+ }
300
+
301
+ .logo {
302
+ grid-area: logo;
303
+ width: max-content;
304
+ height: var(--dx-g-spacing-4xl);
305
+ }
306
+
307
+ .logo > img {
308
+ height: 100%;
309
+ user-select: none;
310
+ -webkit-user-drag: none;
311
+ }
312
+
313
+ .general {
314
+ grid-area: general;
315
+ display: grid;
316
+ grid-template-columns: repeat(3, max-content);
317
+ grid-gap: var(--dx-g-spacing-4xl);
318
+ }
319
+
320
+ .locales {
321
+ grid-area: locales;
322
+ }
323
+
324
+ .socials {
325
+ grid-area: socials;
326
+ display: grid;
327
+ grid-template-columns: repeat(3, max-content);
328
+ grid-auto-rows: min-content;
329
+ grid-gap: var(--dx-g-spacing-sm);
330
+ }
331
+
332
+ .socials > a {
333
+ border-radius: 50%;
334
+ height: var(--dx-g-spacing-xl);
335
+ width: var(--dx-g-spacing-xl);
336
+ flex-shrink: 0;
337
+ display: flex;
338
+ justify-content: center;
339
+ align-items: center;
340
+ color: var(--dx-g-cloud-blue-vibrant-95);
341
+ }
342
+
343
+ .terms a {
344
+ color: var(--dx-g-blue-vibrant-70);
345
+ text-decoration: underline;
346
+ }
347
+
348
+ .terms > span,
349
+ .terms_links > a {
350
+ margin: var(--dx-g-spacing-sm) 0;
351
+ }
352
+
353
+ .terms > span > a {
354
+ display: inline;
355
+ }
356
+
357
+ .terms a:hover {
358
+ color: var(--dx-g-blue-vibrant-80);
359
+ }
360
+
361
+ .socials > a:hover {
362
+ background: var(--dx-g-blue-vibrant-80);
363
+ }
364
+
365
+ .socials > a > svg {
366
+ fill: var(--dx-footer-themed-svg-icon-color);
367
+ height: 100%;
368
+ }
369
+
370
+ /* BOTTOM */
371
+ .content-container_bottom {
372
+ display: flex;
373
+ background: var(--dx-footer-themed-terms-and-conditions-background-color);
374
+ }
375
+
376
+ .content-container_bottom a {
377
+ text-decoration: underline;
378
+ }
379
+
380
+ .terms {
381
+ width: 100%;
382
+ display: flex;
383
+ justify-content: center;
384
+ flex-wrap: wrap;
385
+ font-family: var(--dx-g-font-sans);
386
+ font-size: var(--dx-g-text-xs);
387
+ margin: var(--dx-g-spacing-md) 0;
388
+ }
389
+
390
+ .terms > span {
391
+ padding-right: var(--dx-g-spacing-lg);
392
+ color: white;
393
+ }
394
+
395
+ .terms > span > img {
396
+ margin-right: var(--dx-g-spacing-xs);
397
+ }
398
+
399
+ .terms_links {
400
+ display: flex;
401
+ flex-wrap: wrap;
402
+ }
403
+
404
+ .terms_links > *:not(:last-child) {
405
+ margin-right: var(--dx-g-spacing-lg);
406
+ }
407
+
408
+ @media screen and (max-width: 1024px) {
409
+ .content-container_middle {
410
+ grid-gap: var(--dx-g-spacing-lg);
411
+ padding-top: var(--dx-g-spacing-2xl);
412
+ padding-bottom: var(--dx-g-spacing-2xl);
413
+ }
414
+
415
+ .logo {
416
+ height: var(--dx-g-spacing-2xl);
417
+ }
418
+
419
+ .socials > a {
420
+ height: var(--dx-g-spacing-lg);
421
+ width: var(--dx-g-spacing-lg);
422
+ }
423
+
424
+ .socials > a > dx-icon {
425
+ --dx-c-icon-size: var(--dx-g-icon-size-xs);
426
+ }
427
+
428
+ .socials > a.github > dx-icon {
429
+ --dx-c-icon-size: var(--dx-g-spacing-lg);
430
+ }
431
+ }
432
+
433
+ @media screen and (max-width: 768px) {
434
+ .content-container_middle {
435
+ display: none;
436
+ }
437
+ }
438
+
439
+ @media screen and (max-width: 640px) {
440
+ .term-icon {
441
+ margin-left: 0;
442
+ }
443
+ }
444
+
445
+ /*
446
+ ================================
447
+ END OLD FOOTER STYLES -- CAN BE DELETED ONCE FOOTER MFE IS RE-DEPLOYED
448
+ ================================
449
+ */
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <footer class={className}>
3
3
  <div
4
- if:true={showSmallSignup}
4
+ lwc:if={showSmallSignup}
5
5
  class="content-container content-container_top"
6
6
  >
7
7
  <div class="graphic graphic-trees" alt=""></div>
@@ -14,7 +14,7 @@
14
14
  <div class="graphic graphic-trees-small" alt=""></div>
15
15
  </div>
16
16
  <div
17
- if:true={showLargeSignup}
17
+ lwc:if={showLargeSignup}
18
18
  class="content-container content-container_top_large"
19
19
  >
20
20
  <div class="graphic graphic-large" alt=""></div>
@@ -34,7 +34,7 @@
34
34
  </dx-button>
35
35
  </div>
36
36
  </div>
37
- <div if:true={showLargeNoSignup} class={largeNoSignupClassName}>
37
+ <div lwc:if={showLargeNoSignup} class={largeNoSignupClassName}>
38
38
  <div class="graphic graphic-large" alt=""></div>
39
39
  <slot
40
40
  class="large-signup-heading"
@@ -43,9 +43,90 @@
43
43
  ></slot>
44
44
  </div>
45
45
  <dx-footer-mfe
46
+ lwc:if={useMfe}
46
47
  legal-only={showLegalOnly}
47
48
  origin={mfeConfigOrigin}
48
49
  home-href={mfeHomeHref}
49
50
  ></dx-footer-mfe>
51
+ <!-- TODO: Probably just remove template below and always render the dx-footer-mfe above once footer MFE supports rendering in non-full-width contexts -->
52
+ <template lwc:else>
53
+ <div
54
+ lwc:if={showContainerMiddle}
55
+ class="content-container content-container_middle"
56
+ >
57
+ <a class="logo" href="/">
58
+ <img
59
+ src="https://a.sfdcstatic.com/developer-website/prod/images/salesforce-logo-corporate.svg"
60
+ alt="Salesforce logo"
61
+ />
62
+ </a>
63
+ <div class="socials">
64
+ <template for:each={socialLinks} for:item="link">
65
+ <a
66
+ aria-label={link.label}
67
+ href={link.href}
68
+ key={link.iconSymbol}
69
+ class={link.iconSymbol}
70
+ target="_blank"
71
+ rel="noopener"
72
+ >
73
+ <svg
74
+ xmlns="http://www.w3.org/2000/svg"
75
+ part="svg"
76
+ aria-hidden="true"
77
+ >
78
+ <use xlink:href={link.iconURL}></use>
79
+ </svg>
80
+ </a>
81
+ </template>
82
+ </div>
83
+ <div class="general">
84
+ <template for:each={generalLinks} for:item="item">
85
+ <div class="footer-group" key={item.id}>
86
+ <h2 class="subheading">{item.label}</h2>
87
+ <template for:each={item.options} for:item="option">
88
+ <dx-footer-option
89
+ key={option.id}
90
+ general-label={item.label}
91
+ label={option.label}
92
+ href={option.link.href}
93
+ target={option.link.target}
94
+ ></dx-footer-option>
95
+ </template>
96
+ </div>
97
+ </template>
98
+ </div>
99
+ </div>
100
+ <div class="content-container content-container_bottom">
101
+ <div class="terms">
102
+ <span class="copyright-notice" lwc:dom="manual"></span>
103
+ <div class="terms_links">
104
+ <template for:each={termsLinks} for:item="term">
105
+ <template lwc:if={term.onclick}>
106
+ <a
107
+ href={term.href}
108
+ key={term.label}
109
+ rel={term.rel}
110
+ onclick={term.onclick}
111
+ >
112
+ {term.label}
113
+ </a>
114
+ </template>
115
+ <template lwc:else>
116
+ <a href={term.href} key={term.label} rel={term.rel}>
117
+ <img
118
+ lwc:if={term.img}
119
+ class="term-icon"
120
+ src={term.img}
121
+ alt="Footer Term Icon"
122
+ />
123
+ {term.label}
124
+ </a>
125
+ </template>
126
+ </template>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </template>
50
131
  </footer>
51
132
  </template>
@@ -4,7 +4,7 @@ import { FooterVariant, LightningSlotElement } from "typings/custom";
4
4
  import { track } from "dxUtils/analytics";
5
5
  import { isSlotEmpty } from "dxUtils/slot";
6
6
 
7
- export default class Footer extends LightningElement {
7
+ class Footer extends LightningElement {
8
8
  private _variant: FooterVariant = "small-signup";
9
9
  private isSlotEmpty = true;
10
10
  private signupUrl =
@@ -16,6 +16,9 @@ export default class Footer extends LightningElement {
16
16
  @api
17
17
  mfeConfigOrigin: string = `${window.location.origin}/developer/en-us/wp-json`;
18
18
 
19
+ @api
20
+ useMfe: boolean = false; // TODO: Probably just remove this once the footer MFE supports rendering in non-full-width contexts
21
+
19
22
  @api
20
23
  set variant(value: FooterVariant) {
21
24
  if (!this.isFooterVariant(value)) {
@@ -96,3 +99,208 @@ export default class Footer extends LightningElement {
96
99
  });
97
100
  }
98
101
  }
102
+
103
+ // This encapsulates the old non-MFE footer functionality that should be removed once the footer MFE supports rendering in non-full-width contexts.
104
+ // It's encapsulated like this solely for ease of deletion later.
105
+ function augmentWithNonMFEFooterFunctionality(FooterClass: typeof Footer) {
106
+ const baseSocialIconUrl = `/assets/icons/brand-sprite/svg/symbols.svg`;
107
+
108
+ return class NonMFEFooter extends FooterClass {
109
+ private didRequestFooterConfig = false;
110
+ private footerConfig: any = []; // NOTE: See mockProps.ts for the expected format of the footer config
111
+ private configItemTitleToItemLookup: Map<string, any> = new Map();
112
+ private configItemParentToChildrenLookup: Map<number, any[]> =
113
+ new Map();
114
+ private generalLinks: any = [];
115
+ private socialLinks: any = [];
116
+ private termsLinks: any = [];
117
+
118
+ async renderedCallback() {
119
+ super.renderedCallback?.();
120
+
121
+ if (this.didRequestFooterConfig || this.useMfe) {
122
+ return;
123
+ }
124
+
125
+ this.didRequestFooterConfig = true;
126
+ this.footerConfig = await this.requestFooterConfig();
127
+ this.buildFooterConfigLookupTables(this.footerConfig);
128
+ this.setSocialLinks();
129
+ this.setGeneralLinks();
130
+ this.setLegalFooter();
131
+ }
132
+
133
+ get showContainerMiddle() {
134
+ return !this.showLegalOnly;
135
+ }
136
+
137
+ private async requestFooterConfig() {
138
+ let config: any = [];
139
+ try {
140
+ const response = await fetch(
141
+ `${this.mfeConfigOrigin}/c360/experience/v1/navigation`
142
+ );
143
+ const data = await response.json();
144
+ config = data["footer-navigation"] || [];
145
+ } catch (error) {
146
+ console.error("Error requesting footer config:", error);
147
+ }
148
+ return config;
149
+ }
150
+
151
+ private buildFooterConfigLookupTables(config: any[]) {
152
+ config.forEach((item: any) => {
153
+ this.configItemTitleToItemLookup.set(item.title, item);
154
+ if (item.menu_item_parent) {
155
+ const parentId = parseInt(item.menu_item_parent, 10);
156
+ const children =
157
+ this.configItemParentToChildrenLookup.get(parentId) ||
158
+ [];
159
+ children.push(item);
160
+ this.configItemParentToChildrenLookup.set(
161
+ parentId,
162
+ children
163
+ );
164
+ }
165
+ });
166
+ }
167
+
168
+ private setSocialLinks() {
169
+ const socialLinksParentItem =
170
+ this.configItemTitleToItemLookup.get("social-icons");
171
+
172
+ if (!socialLinksParentItem || !socialLinksParentItem.ID) {
173
+ console.error(
174
+ "Social links parent item not found in footer config"
175
+ );
176
+ return;
177
+ }
178
+
179
+ const socialLinksItems = this.configItemParentToChildrenLookup.get(
180
+ socialLinksParentItem.ID
181
+ );
182
+
183
+ if (!socialLinksItems) {
184
+ console.error(
185
+ "Social links children items not found in footer config"
186
+ );
187
+ return;
188
+ }
189
+
190
+ this.socialLinks =
191
+ socialLinksItems.map((child: any) => {
192
+ const iconSymbol =
193
+ child.title === "LinkedIn"
194
+ ? "linked-in"
195
+ : child.title.toLowerCase();
196
+ const iconUrlHash =
197
+ iconSymbol === "twitter"
198
+ ? "#twitter-x"
199
+ : `#themed-${iconSymbol}`;
200
+
201
+ return {
202
+ href: child.url,
203
+ iconSprite: "brand",
204
+ iconURL: `${baseSocialIconUrl}${iconUrlHash}`,
205
+ label: child.title,
206
+ iconSymbol
207
+ };
208
+ });
209
+ }
210
+
211
+ private setGeneralLinks() {
212
+ const topFooterItem =
213
+ this.configItemTitleToItemLookup.get("top-footer");
214
+ const mediaItem = this.configItemTitleToItemLookup.get("media");
215
+
216
+ if (!topFooterItem || !topFooterItem.ID) {
217
+ console.error("Top footer item not found in footer config");
218
+ return;
219
+ }
220
+
221
+ const generalLinksHeadingsItems =
222
+ this.configItemParentToChildrenLookup
223
+ .get(topFooterItem.ID)
224
+ ?.filter((item: any) => item.ID !== mediaItem?.ID);
225
+
226
+ if (!generalLinksHeadingsItems) {
227
+ console.error(
228
+ "General links children items not found in footer config"
229
+ );
230
+ return;
231
+ }
232
+
233
+ const generalLinks: any[] = [];
234
+ generalLinksHeadingsItems.forEach((item: any) => {
235
+ const childrenItems = this.configItemParentToChildrenLookup.get(item.ID);
236
+
237
+ if (!childrenItems) {
238
+ console.error(`General links children items not found for item with title "${item.title}"`);
239
+ return;
240
+ }
241
+
242
+ generalLinks.push({
243
+ id: item.title,
244
+ label: item.title,
245
+ options: childrenItems.map((option: any) => {
246
+ return {
247
+ id: option.title,
248
+ label: option.title,
249
+ link: {
250
+ href: option.url,
251
+ target: option.target
252
+ }
253
+ };
254
+ })
255
+ });
256
+ });
257
+ this.generalLinks = generalLinks;
258
+ }
259
+
260
+ private setLegalFooter() {
261
+ const copyrightNoticeItem = this.configItemTitleToItemLookup.get("All rights reserved");
262
+
263
+ if (!copyrightNoticeItem) {
264
+ console.error("All rights reserved item not found in footer config");
265
+ return;
266
+ }
267
+
268
+ const { url, description } = copyrightNoticeItem;
269
+ const copyrightNoticeInnerHtml = description.replace("{{All rights reserved}}", `<a href="${url}">All rights reserved</a>`);
270
+ this.template.querySelector(".copyright-notice")!.innerHTML = copyrightNoticeInnerHtml;
271
+
272
+ const legalLinksItems = this.configItemParentToChildrenLookup.get(copyrightNoticeItem.ID);
273
+
274
+ if (!legalLinksItems) {
275
+ console.error("Legal links children items not found in footer config");
276
+ return;
277
+ }
278
+
279
+ this.termsLinks = legalLinksItems.map((item: any) => {
280
+ const link: any = {
281
+ label: item.title,
282
+ href: item.url,
283
+ target: item.target,
284
+ rel: item.rel
285
+ };
286
+
287
+ if (item.title === "Cookie Preferences") {
288
+ link.href = "#";
289
+ link.onclick = this.openOneTrustInfoDisplay;
290
+ } else if (item.title === "Your Privacy Choices") {
291
+ link.img = "https://a.sfdcstatic.com/developer-website/prod/images/privacyoptions.svg";
292
+ }
293
+
294
+ return link;
295
+ });
296
+ }
297
+
298
+ private openOneTrustInfoDisplay() {
299
+ if ((window as any).OneTrust && (window as any).OneTrust.ToggleInfoDisplay) {
300
+ (window as any).OneTrust.ToggleInfoDisplay();
301
+ }
302
+ }
303
+ };
304
+ }
305
+
306
+ export default augmentWithNonMFEFooterFunctionality(Footer);
@@ -15,6 +15,7 @@ export default class FooterMfe extends LightningElement {
15
15
  this.footerElement = document.createElement("hgf-footer");
16
16
  this.footerElement.setAttribute("origin", this.origin);
17
17
  this.footerElement.setAttribute("hide-language-selector", "true");
18
+ this.footerElement.setAttribute("home-href", this.homeHref);
18
19
  if (this.legalOnly) {
19
20
  this.footerElement.setAttribute("legal-only", "true");
20
21
  }