@salesforcedevs/dx-components 1.3.246-canary.2 → 1.3.247

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/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2020, Salesforce.com, Inc.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "1.3.246-canary.2",
3
+ "version": "1.3.247",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -44,5 +44,6 @@
44
44
  },
45
45
  "volta": {
46
46
  "node": "16.19.1"
47
- }
47
+ },
48
+ "gitHead": "fdadc22dcca148e26f1589d4de64b8445899f39d"
48
49
  }
@@ -30,8 +30,10 @@
30
30
  top: 1px;
31
31
  }
32
32
 
33
- /* The "inner" container/main part of the player, inside the outer, colored border */
34
- /* Current design goal of this element and all other "inner" elements is to closely match native controls as they appear in Chrome */
33
+ /*
34
+ The "inner" container/main part of the player, inside the outer, colored border.
35
+ Current design goal of this element and all other "inner" elements is to closely match native controls as they appear in Chrome
36
+ */
35
37
  .player {
36
38
  --dx-c-popover-border-radius: 0;
37
39
  --dx-c-popover-padding: 0;
@@ -72,7 +74,6 @@
72
74
  border-radius: 16px;
73
75
  cursor: pointer;
74
76
  height: 4px;
75
- outline: none;
76
77
  width: 100%;
77
78
  }
78
79
 
@@ -123,24 +124,29 @@
123
124
  .player-volume-container {
124
125
  align-items: center;
125
126
  border-radius: var(--dx-g-spacing-xs);
127
+ height: 24px;
126
128
  display: flex;
127
- padding-left: 2px;
129
+ padding-left: 6px;
130
+ position: relative;
128
131
  transition: 0.2s ease-out;
129
132
  width: 24px;
130
133
  }
131
134
 
132
135
  .player-volume-container .player-volume-slider {
133
- margin: 0;
134
- visibility: hidden;
135
- }
136
-
137
- .player-volume-container:hover {
138
- background: var(--sds-g-gray-4);
139
- width: 100px;
136
+ /* HTML5 boilerplate .visuallyhidden equivalent, for hiding an element but leaving it focusable */
137
+ border: 0;
138
+ clip: rect(0 0 0 0);
139
+ height: 1px;
140
+ margin: -1px;
141
+ overflow: hidden;
142
+ padding: 0;
143
+ position: absolute;
144
+ width: 1px;
140
145
  }
141
146
 
142
- .player-volume-container:hover .player-volume-slider {
143
- visibility: visible;
147
+ .player-volume-container .player-volume-button {
148
+ position: absolute;
149
+ right: 0;
144
150
  }
145
151
 
146
152
  /* Three dot menu, for download and playback speed settings */
@@ -158,8 +164,8 @@
158
164
  }
159
165
 
160
166
  /* Speed selection items have a large left padding in Chrome, which we are mimicking */
161
- .player-threedot-menu li.player-speed-item {
162
- padding-left: calc(var(--dx-c-threedot-menu-item-padding) * 2);
167
+ .player-threedot-menu .player-speed-item {
168
+ padding-left: calc(var(--dx-c-threedot-menu-item-padding) * 3);
163
169
  }
164
170
 
165
171
  .player-threedot-menu a {
@@ -189,8 +195,10 @@
189
195
  transform: translateY(-50%);
190
196
  }
191
197
 
192
- /* All clickable elements inside of the three dot menu are buttons, except for the download link */
193
- /* dx-button is not used here because these things barely look like buttons */
198
+ /*
199
+ All clickable elements inside of the three dot menu are buttons, except for the download link
200
+ dx-button is not used here because these things barely look like buttons
201
+ */
194
202
  .player-threedot-menu button {
195
203
  background: transparent;
196
204
  border: 0;
@@ -223,16 +231,27 @@
223
231
  opacity: 1;
224
232
  }
225
233
 
234
+ .player-volume-container.focused-by-keyboard,
226
235
  .player-volume-container:hover {
227
236
  background: var(--sds-g-gray-4);
228
- width: 100px;
237
+ width: max(12%, 150px);
229
238
  }
230
239
 
240
+ .player-volume-container.focused-by-keyboard .player-volume-slider,
231
241
  .player-volume-container:hover .player-volume-slider {
232
- visibility: visible;
242
+ /* Undo .visuallyhidden */
243
+ clip: unset;
244
+ height: 4px;
245
+ margin-right: 24px; /* leave room for the button */
246
+ overflow: visible;
247
+ position: relative;
248
+ width: 100%;
233
249
  }
234
250
 
235
- .player-threedot-menu li:hover {
251
+ .player-threedot-menu a:focus,
252
+ .player-threedot-menu a:hover,
253
+ .player-threedot-menu button:focus,
254
+ .player-threedot-menu button:hover {
236
255
  background: var(--sds-g-gray-4);
237
256
  }
238
257
  }
@@ -1,5 +1,11 @@
1
1
  <template>
2
- <div class="custom-audio-player">
2
+ <div
3
+ aria-label={playerAriaLabel}
4
+ class="custom-audio-player"
5
+ lwc:ref="container"
6
+ onkeyup={handleContainerKeyUp}
7
+ tabindex="0"
8
+ >
3
9
  <dx-icon
4
10
  class="listen-icon"
5
11
  size="xsmall"
@@ -8,24 +14,13 @@
8
14
  ></dx-icon>
9
15
  <span class="listen-text">Listen to this article</span>
10
16
  <div class="player">
11
- <template lwc:if={isPlaying}>
12
- <dx-button
13
- aria-label="Pause"
14
- class="player-pause-button"
15
- icon-symbol="pause"
16
- variant="custom"
17
- onclick={handleAudioPause}
18
- ></dx-button>
19
- </template>
20
- <template lwc:else>
21
- <dx-button
22
- aria-label="Play"
23
- class="player-play-button"
24
- icon-symbol="play"
25
- variant="custom"
26
- onclick={handleAudioPlay}
27
- ></dx-button>
28
- </template>
17
+ <dx-button
18
+ aria-label={mainControlAriaLabel}
19
+ class="player-play-pause-button"
20
+ icon-symbol={mainControlIconSymbol}
21
+ variant="custom"
22
+ onclick={handleMainControlClick}
23
+ ></dx-button>
29
24
  <div class="player-time">
30
25
  <span class="player-current-time">{formattedCurrentTime}</span>
31
26
  &nbsp;/&nbsp;
@@ -41,13 +36,14 @@
41
36
  onchange={handleSeekChange}
42
37
  oninput={handleSeekInput}
43
38
  />
44
- <div class="player-volume-container">
39
+ <div class="player-volume-container" lwc:ref="playerVolumeContainer" onfocusin={handleVolumeFocusIn} onfocusout={handleVolumeFocusOut}>
45
40
  <input
46
41
  aria-label="Volume Level"
47
42
  class="player-volume-slider"
48
43
  type="range"
49
44
  max="100"
50
45
  lwc:ref="playerVolumeSlider"
46
+ tabindex="0"
51
47
  value={volume}
52
48
  oninput={handleVolumeInput}
53
49
  />
@@ -70,7 +66,7 @@
70
66
  slot="control"
71
67
  variant="custom"
72
68
  ></dx-button>
73
- <div class="player-threedot-menu" slot="content">
69
+ <div class="player-threedot-menu" lwc:ref="playbackSpeedMenu" slot="content" tabindex="0">
74
70
  <template lwc:if={isSettingPlaybackSpeed}>
75
71
  <div>
76
72
  <ul>
@@ -91,9 +87,11 @@
91
87
  >
92
88
  <li
93
89
  key={playbackSpeedDatum.value}
94
- class="player-speed-item"
95
90
  >
96
- <button onclick={handleSetPlaybackSpeed}>
91
+ <button
92
+ class="player-speed-item"
93
+ onclick={handleSetPlaybackSpeed}
94
+ >
97
95
  {playbackSpeedDatum.value}
98
96
  </button>
99
97
  <dx-icon
@@ -113,6 +111,7 @@
113
111
  <a
114
112
  href={audioSrc}
115
113
  download
114
+ tabindex="0"
116
115
  target="_blank"
117
116
  rel="noopener"
118
117
  >
@@ -9,6 +9,7 @@ type TrackColors = {
9
9
  buffer?: string;
10
10
  after: string;
11
11
  };
12
+ // eslint-disable-next-line no-use-before-define
12
13
  type PlaybackSpeed = (typeof formattedPlaybackSpeeds)[number];
13
14
 
14
15
  /*
@@ -46,12 +47,15 @@ const formattedPlaybackSpeeds = [
46
47
 
47
48
  export default class Audio extends LightningElementWithTypedRefs<{
48
49
  audioElement: HTMLAudioElement;
50
+ container: HTMLDivElement;
51
+ playbackSpeedMenu: HTMLDivElement;
49
52
  playbackSpeedPopover: Popover;
50
53
  playerSeekSlider: HTMLInputElement;
51
54
  playerVolumeSlider: HTMLInputElement;
55
+ playerVolumeContainer: HTMLDivElement;
52
56
  }> {
53
57
  @api audioSrc!: string;
54
- @api postName!: string;
58
+ @api audioTitle!: string;
55
59
 
56
60
  private _bufferedTimeRanges?: TimeRanges;
57
61
  private _currentTimeSeconds = 0;
@@ -60,6 +64,7 @@ export default class Audio extends LightningElementWithTypedRefs<{
60
64
  private animationFrameId: number | null = null; // controls movement of the timeline when audio is playing
61
65
  private currentPlaybackSpeed: PlaybackSpeed = "Normal";
62
66
  private didRender = false;
67
+ private isAnimating = false;
63
68
  private isPlaying = false;
64
69
  private isSettingPlaybackSpeed = false; // popover menu has two panes; this manages that state
65
70
  // `playbackSpeedData` tracks which value is selected, in a way useable by LWC for:each without requiring a separate sub-component
@@ -78,6 +83,18 @@ export default class Audio extends LightningElementWithTypedRefs<{
78
83
  return this.getFormatted(this.durationSeconds);
79
84
  }
80
85
 
86
+ private get mainControlAriaLabel() {
87
+ return this.isPlaying ? "Pause" : "Play";
88
+ }
89
+
90
+ private get mainControlIconSymbol() {
91
+ return this.isPlaying ? "pause" : "play";
92
+ }
93
+
94
+ private get playerAriaLabel() {
95
+ return `Audio: ${this.audioTitle}`;
96
+ }
97
+
81
98
  // NOTE that values with getters and setters here have side effects that update the UI elements.
82
99
  // This essentially makes these items the source of truth for the UI.
83
100
  private get bufferedTimeRanges() {
@@ -176,6 +193,14 @@ export default class Audio extends LightningElementWithTypedRefs<{
176
193
  this.bufferedTimeRanges = this.typedRefs.audioElement.buffered;
177
194
  };
178
195
 
196
+ handleMainControlClick = (event: Event) => {
197
+ if (this.isPlaying) {
198
+ this.handleAudioPause(event);
199
+ } else {
200
+ this.handleAudioPlay(event);
201
+ }
202
+ };
203
+
179
204
  handleAudioPlay = (event: Event) => {
180
205
  this.typedRefs.audioElement.play();
181
206
  this.syncTimeWithAudio();
@@ -253,9 +278,34 @@ export default class Audio extends LightningElementWithTypedRefs<{
253
278
  }
254
279
  };
255
280
 
281
+ handleVolumeFocusIn = () => {
282
+ this.typedRefs.playerVolumeContainer.classList.add(
283
+ "focused-by-keyboard"
284
+ );
285
+ };
286
+
287
+ handleVolumeFocusOut = (event: FocusEvent) => {
288
+ const { playerVolumeContainer } = this.typedRefs;
289
+ const isFocusingChildOfVolumeContainer = Array.from(
290
+ playerVolumeContainer.children
291
+ ).some((childElement) => event.relatedTarget === childElement);
292
+
293
+ if (!isFocusingChildOfVolumeContainer) {
294
+ this.typedRefs.playerVolumeContainer.classList.remove(
295
+ "focused-by-keyboard"
296
+ );
297
+ }
298
+ };
299
+
256
300
  // Set "pane" state for playback popover menu
257
301
  handlePlaybackSpeedClick = () => {
258
302
  this.isSettingPlaybackSpeed = true;
303
+ this.refocusPlaybackSpeedMenu();
304
+ };
305
+
306
+ // Sets "pane" state for popover back to the default
307
+ resetIsSettingPlaybackSpeed = () => {
308
+ this.isSettingPlaybackSpeed = false;
259
309
  };
260
310
 
261
311
  // Handle selection of a playback speed in the "three dot" menu
@@ -281,13 +331,23 @@ export default class Audio extends LightningElementWithTypedRefs<{
281
331
  : parseFloat(selectedPlaybackSpeed);
282
332
  };
283
333
 
284
- // Sets "pane" state for popover back to the default
285
- resetIsSettingPlaybackSpeed = () => {
286
- this.isSettingPlaybackSpeed = false;
334
+ handleContainerKeyUp = (event: KeyboardEvent) => {
335
+ if (event.key === " " && event.target === this.typedRefs.container) {
336
+ if (this.isPlaying) {
337
+ this.handleAudioPause(event);
338
+ } else {
339
+ this.handleAudioPlay(event);
340
+ }
341
+ }
287
342
  };
288
343
 
289
344
  /* END event handlers, BEGIN private utility methods */
290
345
 
346
+ // Called when menu's inner pane changes, ensuring focus is on the newly-displayed menu
347
+ private refocusPlaybackSpeedMenu = () => {
348
+ Promise.resolve().then(() => this.typedRefs.playbackSpeedMenu.focus());
349
+ };
350
+
291
351
  // Animates the seek slider (timeline) each frame so that it is N*Sync with current play
292
352
  // time. We do this using requestAnimationFrame so that we can temporarily cancel the
293
353
  // animating whenever the user is interacting with the slider.
@@ -378,7 +438,7 @@ export default class Audio extends LightningElementWithTypedRefs<{
378
438
  const payload = {
379
439
  event: eventType,
380
440
  media_action: action,
381
- media_name: this.postName,
441
+ media_name: this.audioTitle,
382
442
  media_percentage_played:
383
443
  this.durationSeconds === 0
384
444
  ? this.durationSeconds
@@ -14,6 +14,7 @@ export default class CardBlogPost extends LightningElement {
14
14
  @api title!: string;
15
15
  @api authors?: Array<any> | null = null;
16
16
  @api origin?: string = "wordpress";
17
+ @api clickEvent?: () => void;
17
18
 
18
19
  // LWC is being compiled so that datetime is being interpreted as date-time
19
20
  // ONLY from from the implementation in dx-card-blog-post-provider
@@ -22,6 +23,10 @@ export default class CardBlogPost extends LightningElement {
22
23
  }
23
24
 
24
25
  private onLinkClick(event: Event) {
26
+ if (this.clickEvent) {
27
+ this.clickEvent();
28
+ }
29
+
25
30
  const payload = {
26
31
  click_text: this.title,
27
32
  click_url: `${window.location.origin}${this.href}`,
@@ -23,6 +23,7 @@
23
23
  key={recommendation.id}
24
24
  title={recommendation.yoast_head_json.title}
25
25
  origin="coveo"
26
+ click-event={recommendation.clickEvent}
26
27
  ></dx-card-blog-post>
27
28
  </template>
28
29
  </dx-grid>
@@ -1,11 +1,25 @@
1
1
  import { LightningElement, api } from "lwc";
2
2
 
3
3
  const SEARCH_HUB = "developerWebsiteBlogs";
4
+
5
+ interface Recommendation {
6
+ originalCoveoData: {
7
+ uri: string;
8
+ title: string;
9
+ searchUid: string;
10
+ raw: {
11
+ permanentId: string;
12
+ };
13
+ "@source": string;
14
+ };
15
+ clickEvent: () => void;
16
+ }
17
+
4
18
  export default class CoveoRecommendations extends LightningElement {
5
19
  @api coveoAuthToken!: string;
6
20
  @api coveoOrganizationId!: string;
7
21
 
8
- _recommendations = [] as any[];
22
+ _recommendations = [] as Recommendation[];
9
23
 
10
24
  private get recommendations(): any[] {
11
25
  return this._recommendations || [];
@@ -46,10 +60,17 @@ export default class CoveoRecommendations extends LightningElement {
46
60
 
47
61
  const blogDataLoadTasks = results
48
62
  .slice(0, 3)
49
- .map(async (rec: any) => {
63
+ .map(async (rec: any, index: number) => {
50
64
  const slug = rec.uri.split("/").pop();
51
65
  const blogDataUrl = `https://developer.salesforce.com/blogs/wp-json/wp/v2/posts?slug=${slug}&state=publish&_embed=wp:featuredmedia`;
52
- return (await (await fetch(blogDataUrl)).json())[0];
66
+ const wordPressData = (
67
+ await (await fetch(blogDataUrl)).json()
68
+ )[0];
69
+ wordPressData.originalCoveoData = rec;
70
+ wordPressData.originalCoveoData.searchUid =
71
+ json.searchUid;
72
+ wordPressData.clickEvent =
73
+ this.buildClickAnalyticsLogger(index);
53
74
  });
54
75
  this._recommendations = await Promise.all(
55
76
  blogDataLoadTasks
@@ -92,4 +113,38 @@ export default class CoveoRecommendations extends LightningElement {
92
113
  }
93
114
  );
94
115
  };
116
+
117
+ buildClickAnalyticsLogger = (index: number) => () => {
118
+ const payload = {
119
+ anonymous: true,
120
+ documentPosition: index,
121
+ documentTitle: this._recommendations[index].originalCoveoData.title,
122
+ documentUrl: this._recommendations[index].originalCoveoData.uri,
123
+ language: "en",
124
+ originLevel1: SEARCH_HUB,
125
+ originLevel2: SEARCH_HUB,
126
+ actionCause: "recommendationOpen",
127
+ queryText: "", // This has to be included, but is empty, because recommendations use the 'search' endpoint with an empty query string
128
+ searchQueryUid: this._recommendations[index].originalCoveoData.uri,
129
+ sourceName:
130
+ this._recommendations[index].originalCoveoData["@source"],
131
+ customData: {
132
+ contentIDKey: "permanentId",
133
+ contentIDValue:
134
+ this._recommendations[index].originalCoveoData.raw
135
+ .permanentId
136
+ }
137
+ };
138
+ fetch(
139
+ `https://${this.coveoOrganizationId}.analytics.org.coveo.com/rest/ua/v15/analytics/click`,
140
+ {
141
+ headers: {
142
+ Authorization: `Bearer ${this.coveoAuthToken}`,
143
+ "Content-Type": "application/json"
144
+ },
145
+ method: "POST",
146
+ body: JSON.stringify(payload)
147
+ }
148
+ );
149
+ };
95
150
  }
@@ -6,6 +6,7 @@ import {
6
6
  } from "typings/custom";
7
7
 
8
8
  import {
9
+ autoUpdate,
9
10
  computePosition,
10
11
  flip,
11
12
  size,
@@ -31,6 +32,8 @@ const isEventOutsideElements = (
31
32
  );
32
33
 
33
34
  export default class Popover extends LightningElement {
35
+ private autoUpdateCleanup?: () => void;
36
+
34
37
  @api offset?: "small" | "medium";
35
38
  @api pagePadding?: number = 16; // padding between dropdown and the edge of the page
36
39
  @api placement?: PopperPlacement = "bottom-start";
@@ -76,6 +79,10 @@ export default class Popover extends LightningElement {
76
79
  this._open = true;
77
80
  this.control.setAttribute("aria-expanded", "true");
78
81
 
82
+ if (this.popover) {
83
+ this.autoUpdateCleanup = autoUpdate(this.control, this.popover, this.setPosition);
84
+ }
85
+
79
86
  this.dispatchEvent(new CustomEvent("open"));
80
87
 
81
88
  setTimeout(() => {
@@ -88,6 +95,7 @@ export default class Popover extends LightningElement {
88
95
  @api
89
96
  closePopover(focusControl: boolean = false) {
90
97
  this._open = false;
98
+ this.autoUpdateCleanup?.();
91
99
  if (focusControl && this.control && this.control.focus) {
92
100
  this.control.focus();
93
101
  }