@salesforcedevs/dx-components 1.3.248 → 1.3.249

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
@@ -121,7 +121,6 @@
121
121
  "dxUtils/normalizers",
122
122
  "dxUtils/queryCoordinator",
123
123
  "dxUtils/recentSearches",
124
- "dxUtils/withTypedRefs",
125
124
  "dxUtils/wordpress"
126
125
  ]
127
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "1.3.248",
3
+ "version": "1.3.249",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -23,7 +23,6 @@
23
23
  "lodash.defaults": "^4.2.0",
24
24
  "lodash.get": "^4.4.2",
25
25
  "lodash.kebabcase": "^4.1.1",
26
- "memoize-one": "^6.0.0",
27
26
  "microtip": "0.2.2",
28
27
  "salesforce-oauth2": "^0.2.0",
29
28
  "throttle-debounce": "^5.0.0",
@@ -45,5 +44,5 @@
45
44
  "volta": {
46
45
  "node": "16.19.1"
47
46
  },
48
- "gitHead": "411db405210df8682a3a38ab89e521df252e6838"
47
+ "gitHead": "c7d155ee843224d12db375fde4f54840d0317385"
49
48
  }
@@ -1,257 +1,16 @@
1
- :host {
2
- --dx-c-track-before-color: var(--dx-g-indigo-vibrant-30);
3
- --dx-c-track-custom-dark-gray: rgba(62 62 62 / 100%);
4
- --dx-c-track-custom-medium-gray: rgba(195 195 195 / 100%);
5
- --dx-c-track-custom-light-gray: rgba(235 235 236 / 100%);
6
- --dx-c-track-thumb-size: 10px;
7
- --dx-c-threedot-menu-item-padding: 14px;
8
- }
9
-
10
- /* Outermost container/border */
11
- .custom-audio-player {
12
- background-color: var(--dx-g-indigo-vibrant-90, #e0e5f8);
13
- border-radius: 12px;
14
- padding: 6px 13px 13px;
15
- }
16
-
17
- .listen-icon {
18
- display: inline-block;
19
- padding: 0 6px 0 4px;
20
- }
21
-
22
- .listen-text {
23
- display: inline-block;
24
- font-family: var(--dx-g-font-display);
25
- font-size: var(--dx-g-text-2xs, 11px);
26
- font-weight: var(--dx-g-font-bold, bold);
27
- letter-spacing: 0.6px;
28
- position: relative;
29
- text-transform: uppercase;
30
- top: 1px;
31
- }
32
-
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
- */
37
- .player {
38
- --dx-c-popover-border-radius: 0;
39
- --dx-c-popover-padding: 0;
40
-
41
- align-items: center;
42
- background-color: var(--sds-g-gray-1, #fff);
43
- border-radius: 5px;
44
- display: flex;
45
- margin-top: 8px;
46
- padding: 3px 4px;
47
- }
48
-
49
- /* Player button controls */
50
- .player dx-button::part(container) {
51
- height: 24px;
52
- width: 24px;
53
- }
54
-
55
- .player-time,
56
- .player-volume-slider,
57
- .player-seek-slider {
58
- position: relative;
59
- top: 1px;
60
- }
61
-
62
- .player-time {
63
- display: inline-block;
64
- font-family: var(--dx-g-font-sans);
65
- font-size: var(--dx-g-text-2xs, 11px);
66
- margin-left: 4px;
67
- margin-right: 8px;
68
- }
69
-
70
- /* Sliders and "thumb" controls */
71
- .player-volume-slider,
72
- .player-seek-slider {
73
- appearance: none;
74
- border-radius: 16px;
75
- cursor: pointer;
76
- height: 4px;
77
- width: 100%;
78
- }
79
-
80
- .player-seek-slider {
81
- background: var(--dx-c-track-custom-light-gray);
82
- }
83
-
84
- .player-volume-slider {
85
- background: var(--sds-g-gray-7);
86
- }
87
-
88
- /* Create a large, solid "shadow" to the left of the "thumb" on the progress bar, simulating filled in space */
89
- .player-volume-slider::-webkit-slider-thumb,
90
- .player-seek-slider::-webkit-slider-thumb {
91
- appearance: none;
92
- background-color: var(--dx-g-indigo-vibrant-30);
93
- border: none;
94
- border-radius: 50%;
95
- height: var(--dx-c-track-thumb-size);
96
- opacity: 0;
97
- transition: opacity 0.3s ease-in-out;
98
- width: var(--dx-c-track-thumb-size);
99
- }
100
-
101
- .player-volume-slider::-moz-range-thumb,
102
- .player-seek-slider::-moz-range-thumb {
103
- background-color: var(--dx-g-indigo-vibrant-30);
104
- border: none;
105
- border-radius: 50%;
106
- height: var(--dx-c-track-thumb-size);
107
- opacity: 0;
108
- transition: opacity 0.3s ease-in-out;
109
- width: var(--dx-c-track-thumb-size);
110
- }
111
-
112
- /* NOTE: Even though the 'active' CSS rules for sliders on webkit and mozilla are the same here, they CANNOT be combined or the slider breaks */
113
- .player-volume-slider:active::-webkit-slider-thumb,
114
- .player-seek-slider:active::-webkit-slider-thumb {
115
- opacity: 1;
116
- }
117
-
118
- .player-volume-slider:active::-moz-range-thumb,
119
- .player-seek-slider:active::-moz-range-thumb {
120
- opacity: 1;
121
- }
122
-
123
- /* Container for the volume slider, which transitions in and out on hover */
124
- .player-volume-container {
125
- align-items: center;
126
- border-radius: var(--dx-g-spacing-xs);
127
- height: 24px;
128
- display: flex;
129
- padding-left: 6px;
130
- position: relative;
131
- transition: 0.2s ease-out;
132
- width: 24px;
133
- }
134
-
135
- .player-volume-container .player-volume-slider {
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;
145
- }
146
-
147
- .player-volume-container .player-volume-button {
148
- position: absolute;
149
- right: 0;
150
- }
151
-
152
- /* Three dot menu, for download and playback speed settings */
153
- .player-threedot-menu ul {
154
- font-family: var(--dx-g-font-sans);
155
- font-size: var(--dx-g-text-2xs, 11px);
156
- list-style: none;
157
- margin: 0;
158
- padding: 0;
159
- }
160
-
161
- .player-threedot-menu li {
162
- cursor: pointer;
163
- position: relative;
164
- }
165
-
166
- /* Speed selection items have a large left padding in Chrome, which we are mimicking */
167
- .player-threedot-menu .player-speed-item {
168
- padding-left: calc(var(--dx-c-threedot-menu-item-padding) * 3);
1
+ .audio-container {
2
+ width: 350px;
169
3
  }
170
4
 
171
- .player-threedot-menu a {
172
- color: initial;
173
- cursor: pointer;
5
+ /* TODO: we need mobile breakpoints for this, it looks bad on mobile */
6
+ .audio {
7
+ width: 350px;
174
8
  display: block;
175
- padding: var(--dx-c-threedot-menu-item-padding);
176
- text-decoration: none;
9
+ margin-bottom: var(--dx-g-spacing-3xl);
10
+ border: 5px solid
11
+ var(
12
+ --dx-c-featured-content-header-background-color,
13
+ var(--dx-g-indigo-vibrant-90)
14
+ );
15
+ border-radius: 2em;
177
16
  }
178
-
179
- .player-threedot-menu dx-icon {
180
- display: inline-block;
181
- }
182
-
183
- .player-threedot-menu dx-icon:not(.player-selected-speed) {
184
- margin-right: var(--dx-c-threedot-menu-item-padding);
185
- position: relative;
186
- top: -1px; /* fix icon vertical position */
187
- }
188
-
189
- /* The selected speed has a check mark icon to the right side */
190
- .player-threedot-menu dx-icon.player-selected-speed {
191
- color: var(--dx-g-indigo-vibrant-40);
192
- position: absolute;
193
- right: 40px;
194
- top: 50%;
195
- transform: translateY(-50%);
196
- }
197
-
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
- */
202
- .player-threedot-menu button {
203
- background: transparent;
204
- border: 0;
205
- color: initial; /* only necessary on iOS Safari to remove blue text color */
206
- cursor: pointer;
207
- font-family: var(--dx-g-font-sans);
208
- font-size: var(--dx-g-text-2xs, 11px);
209
- padding: var(--dx-c-threedot-menu-item-padding);
210
- text-align: left;
211
- width: 100%;
212
- }
213
-
214
- .audio-container {
215
- display: none;
216
- }
217
-
218
- @media (hover: hover) {
219
- .player dx-button::part(container):hover {
220
- background: var(--sds-g-gray-4);
221
- }
222
-
223
- /* NOTE: Even though the 'hover' CSS rules for sliders on webkit and mozilla are the same here, they CANNOT be combined or the slider breaks */
224
- .player-volume-slider:hover::-webkit-slider-thumb,
225
- .player-seek-slider:hover::-webkit-slider-thumb {
226
- opacity: 1;
227
- }
228
-
229
- .player-volume-slider:hover::-moz-range-thumb,
230
- .player-seek-slider:hover::-moz-range-thumb {
231
- opacity: 1;
232
- }
233
-
234
- .player-volume-container.focused-by-keyboard,
235
- .player-volume-container:hover {
236
- background: var(--sds-g-gray-4);
237
- width: max(12%, 150px);
238
- }
239
-
240
- .player-volume-container.focused-by-keyboard .player-volume-slider,
241
- .player-volume-container:hover .player-volume-slider {
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%;
249
- }
250
-
251
- .player-threedot-menu a:focus,
252
- .player-threedot-menu a:hover,
253
- .player-threedot-menu button:focus,
254
- .player-threedot-menu button:hover {
255
- background: var(--sds-g-gray-4);
256
- }
257
- }
@@ -1,145 +1,6 @@
1
1
  <template>
2
- <div
3
- aria-label={playerAriaLabel}
4
- class="custom-audio-player"
5
- lwc:ref="container"
6
- onkeyup={handleContainerKeyUp}
7
- tabindex="0"
8
- >
9
- <dx-icon
10
- class="listen-icon"
11
- size="xsmall"
12
- sprite="general"
13
- symbol="headphones"
14
- ></dx-icon>
15
- <span class="listen-text">Listen to this article</span>
16
- <div class="player">
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>
24
- <div class="player-time">
25
- <span class="player-current-time">{formattedCurrentTime}</span>
26
- &nbsp;/&nbsp;
27
- <span class="player-duration">{formattedDuration}</span>
28
- </div>
29
- <input
30
- aria-label="Current Time"
31
- class="player-seek-slider"
32
- type="range"
33
- max={durationSeconds}
34
- lwc:ref="playerSeekSlider"
35
- value={currentTimeSeconds}
36
- onchange={handleSeekChange}
37
- oninput={handleSeekInput}
38
- />
39
- <div class="player-volume-container" lwc:ref="playerVolumeContainer" onfocusin={handleVolumeFocusIn} onfocusout={handleVolumeFocusOut}>
40
- <input
41
- aria-label="Volume Level"
42
- class="player-volume-slider"
43
- type="range"
44
- max="100"
45
- lwc:ref="playerVolumeSlider"
46
- tabindex="0"
47
- value={volume}
48
- oninput={handleVolumeInput}
49
- />
50
- <dx-button
51
- aria-label="Volume"
52
- class="player-volume-button"
53
- icon-symbol={volumeIcon}
54
- onclick={handleVolumeClick}
55
- variant="custom"
56
- ></dx-button>
57
- </div>
58
- <dx-popover
59
- lwc:ref="playbackSpeedPopover"
60
- width="200px"
61
- onclose={resetIsSettingPlaybackSpeed}
62
- >
63
- <dx-button
64
- aria-label="More"
65
- icon-symbol="threedots_vertical"
66
- slot="control"
67
- variant="custom"
68
- ></dx-button>
69
- <div class="player-threedot-menu" lwc:ref="playbackSpeedMenu" slot="content" tabindex="0">
70
- <template lwc:if={isSettingPlaybackSpeed}>
71
- <div>
72
- <ul>
73
- <li>
74
- <button
75
- onclick={resetIsSettingPlaybackSpeed}
76
- >
77
- <dx-icon
78
- size="small"
79
- symbol="back"
80
- ></dx-icon>
81
- Options
82
- </button>
83
- </li>
84
- <template
85
- for:each={playbackSpeedData}
86
- for:item="playbackSpeedDatum"
87
- >
88
- <li
89
- key={playbackSpeedDatum.value}
90
- >
91
- <button
92
- class="player-speed-item"
93
- onclick={handleSetPlaybackSpeed}
94
- >
95
- {playbackSpeedDatum.value}
96
- </button>
97
- <dx-icon
98
- class="player-selected-speed"
99
- lwc:if={playbackSpeedDatum.selected}
100
- size="small"
101
- symbol="check"
102
- ></dx-icon>
103
- </li>
104
- </template>
105
- </ul>
106
- </div>
107
- </template>
108
- <template lwc:else>
109
- <ul>
110
- <li>
111
- <a
112
- href={audioSrc}
113
- download
114
- tabindex="0"
115
- target="_blank"
116
- rel="noopener"
117
- >
118
- <dx-icon
119
- size="small"
120
- symbol="download"
121
- ></dx-icon>
122
- Download
123
- </a>
124
- </li>
125
- <li>
126
- <button onclick={handlePlaybackSpeedClick}>
127
- <dx-icon
128
- size="small"
129
- sprite="general"
130
- symbol="tachometer-alt"
131
- ></dx-icon>
132
- Playback Speed
133
- </button>
134
- </li>
135
- </ul>
136
- </template>
137
- </div>
138
- </dx-popover>
139
- </div>
140
- </div>
141
2
  <div class="audio-container">
142
- <audio lwc:ref="audioElement" class="audio" controls>
3
+ <audio class="audio" controls>
143
4
  <source src={audioSrc} type="audio/mpeg" />
144
5
  Your browser does not support the audio element.
145
6
  </audio>
@@ -1,467 +1,63 @@
1
- import { api } from "lwc";
2
- import memoize from "memoize-one";
3
- import Popover from "dx/popover";
4
1
  import { track } from "dxUtils/analytics";
5
- import { LightningElementWithTypedRefs } from "dxUtils/withTypedRefs";
2
+ import { LightningElement, api } from "lwc";
6
3
 
7
- type TrackColors = {
8
- before: string;
9
- buffer?: string;
10
- after: string;
11
- };
12
- // eslint-disable-next-line no-use-before-define
13
- type PlaybackSpeed = (typeof formattedPlaybackSpeeds)[number];
14
-
15
- /*
16
- * Color settings for the "track" of the slider, which dynamically updates based on amount buffered,
17
- * amount played, and amount remaining. The "before" color is what goes _before_ the "thumb" on the
18
- * slider, and so on.
19
- */
20
- const uiConfig = {
21
- seekSlider: {
22
- trackColors: {
23
- before: "var(--dx-g-indigo-vibrant-30)",
24
- buffer: "var(--dx-c-track-custom-medium-gray)",
25
- after: "var(--dx-c-track-custom-light-gray)"
26
- } as TrackColors
27
- },
28
- volumeSlider: {
29
- trackColors: {
30
- before: "var(--dx-g-indigo-vibrant-30)",
31
- after: "var(--dx-c-track-custom-medium-gray)"
32
- } as TrackColors
33
- }
34
- };
35
-
36
- // Standard playback speeds, though the "formatted" version for 1 is "Normal" (mimicking Chrome)
37
- const formattedPlaybackSpeeds = [
38
- "0.25",
39
- "0.5",
40
- "0.75",
41
- "Normal",
42
- "1.25",
43
- "1.5",
44
- "1.75",
45
- "2"
46
- ] as const;
47
-
48
- export default class Audio extends LightningElementWithTypedRefs<{
49
- audioElement: HTMLAudioElement;
50
- container: HTMLDivElement;
51
- playbackSpeedMenu: HTMLDivElement;
52
- playbackSpeedPopover: Popover;
53
- playerSeekSlider: HTMLInputElement;
54
- playerVolumeSlider: HTMLInputElement;
55
- playerVolumeContainer: HTMLDivElement;
56
- }> {
4
+ export default class Audio extends LightningElement {
57
5
  @api audioSrc!: string;
58
- @api audioTitle!: string;
59
-
60
- private _bufferedTimeRanges?: TimeRanges;
61
- private _currentTimeSeconds = 0;
62
- private _durationSeconds = 0;
63
- private _volume = 100; // 100 is browser default (and max), at least in Chrome, for audio elements
64
- private animationFrameId: number | null = null; // controls movement of the timeline when audio is playing
65
- private currentPlaybackSpeed: PlaybackSpeed = "Normal";
66
- private didRender = false;
67
- private isAnimating = false;
68
- private isPlaying = false;
69
- private isSettingPlaybackSpeed = false; // popover menu has two panes; this manages that state
70
- // `playbackSpeedData` tracks which value is selected, in a way useable by LWC for:each without requiring a separate sub-component
71
- private playbackSpeedData = formattedPlaybackSpeeds.map((value) => ({
72
- selected: value === "Normal",
73
- value
74
- }));
75
- private prevVolume = this._volume; // previous volume is tracked so that it can be restored on unmute
76
- private volumeIcon: "volume_high" | "volume_off" = "volume_high"; // built-in browser UI doesn't distinguish between high/low, just "on"/"off" (we use the "high" icon for "on")
77
-
78
- private get formattedCurrentTime() {
79
- return this.getFormatted(this.currentTimeSeconds);
80
- }
81
-
82
- private get formattedDuration() {
83
- return this.getFormatted(this.durationSeconds);
84
- }
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
-
98
- // NOTE that values with getters and setters here have side effects that update the UI elements.
99
- // This essentially makes these items the source of truth for the UI.
100
- private get bufferedTimeRanges() {
101
- return this._bufferedTimeRanges;
102
- }
103
- private set bufferedTimeRanges(value: TimeRanges | undefined) {
104
- this._bufferedTimeRanges = value;
105
- this.updateRangeInputStyles(
106
- this.typedRefs.playerSeekSlider,
107
- this.currentTimeSeconds
108
- );
109
- }
110
-
111
- private get currentTimeSeconds() {
112
- return this._currentTimeSeconds;
113
- }
114
- private set currentTimeSeconds(value: number) {
115
- this._currentTimeSeconds = value;
116
- this.updateRangeInputStyles(this.typedRefs.playerSeekSlider, value);
117
- }
118
-
119
- private get durationSeconds() {
120
- return this._durationSeconds;
121
- }
122
- private set durationSeconds(value: number) {
123
- this._durationSeconds = value;
124
- this.updateRangeInputStyles(
125
- this.typedRefs.playerSeekSlider,
126
- this.currentTimeSeconds
127
- );
128
- }
129
-
130
- private get volume() {
131
- return this._volume;
132
- }
133
- private set volume(value: number) {
134
- this.prevVolume = this.volume;
135
- this._volume = value;
136
- this.updateRangeInputStyles(this.typedRefs.playerVolumeSlider, value);
137
- }
138
-
139
- renderedCallback(): void {
140
- if (this.didRender) {
141
- return;
142
- }
143
-
144
- this.didRender = true;
145
- this.volume = this.typedRefs.audioElement.volume * 100;
146
-
147
- // We only need to listen for the metadata event if the metadata isn't already loaded;
148
- // sometimes, browsers can load it _before_ the event handler can even be added.
149
- if (this.typedRefs.audioElement.readyState > 0) {
150
- this.handleAudioLoadedMetadata();
151
- } else {
152
- this.typedRefs.audioElement.addEventListener(
153
- "loadedmetadata",
154
- this.handleAudioLoadedMetadata
155
- );
156
- }
6
+ @api postName!: string;
157
7
 
158
- // "progress" handles buffering of audio data, lets us update the UI for the buffered amount
159
- this.typedRefs.audioElement.addEventListener(
160
- "progress",
161
- this.handleAudioProgress
8
+ renderedCallback() {
9
+ const audioElement = this.template.querySelector("audio");
10
+ audioElement?.addEventListener(
11
+ "play",
12
+ this.trackPlay.bind(this),
13
+ false
162
14
  );
163
- this.typedRefs.audioElement.addEventListener(
15
+ audioElement?.addEventListener(
164
16
  "ended",
165
- this.handleAudioEnded
166
- );
167
- }
168
-
169
- disconnectedCallback(): void {
170
- this.typedRefs.audioElement.removeEventListener(
171
- "loadedmetadata",
172
- this.handleAudioLoadedMetadata
173
- );
174
- this.typedRefs.audioElement.removeEventListener(
175
- "progress",
176
- this.handleAudioProgress
17
+ this.trackEnded.bind(this),
18
+ false
177
19
  );
178
- this.typedRefs.audioElement.removeEventListener(
179
- "ended",
180
- this.handleAudioEnded
20
+ audioElement?.addEventListener(
21
+ "pause",
22
+ this.trackPause.bind(this),
23
+ false
181
24
  );
182
25
  }
183
26
 
184
- /* BEGIN event handlers */
185
-
186
- handleAudioLoadedMetadata = () => {
187
- // `loadedmetadata` is the first spot at which duration is available.
188
- this.durationSeconds = this.typedRefs.audioElement.duration;
189
- };
190
-
191
- handleAudioProgress = () => {
192
- // New buffered amount received, store the buffered time ranges.
193
- this.bufferedTimeRanges = this.typedRefs.audioElement.buffered;
194
- };
195
-
196
- handleMainControlClick = (event: Event) => {
197
- if (this.isPlaying) {
198
- this.handleAudioPause(event);
199
- } else {
200
- this.handleAudioPlay(event);
201
- }
202
- };
203
-
204
- handleAudioPlay = (event: Event) => {
205
- this.typedRefs.audioElement.play();
206
- this.syncTimeWithAudio();
207
- this.isPlaying = true;
208
- this.trackPlay(event);
209
- };
210
-
211
- handleAudioPause = (event: Event) => {
212
- this.typedRefs.audioElement.pause();
213
- cancelAnimationFrame(this.animationFrameId as number);
214
- this.isPlaying = false;
215
- this.trackPause(event);
216
- };
217
-
218
- handleAudioEnded = (event: Event) => {
219
- cancelAnimationFrame(this.animationFrameId as number);
220
- this.isPlaying = false;
221
- this.trackEnded(event);
222
- };
223
-
224
- // Handles any user-driven movement on the "seek" slider (timeline)
225
- handleSeekInput = () => {
226
- if (!this.durationSeconds) {
227
- // No moving the "thumb" when metadata hasn't even loaded.
228
- return;
229
- }
27
+ private trackClick(e: Event, eventType: string, action: string) {
28
+ const audioElement = this.template.querySelector(
29
+ "audio"
30
+ ) as HTMLMediaElement;
230
31
 
231
- if (this.isPlaying) {
232
- // To allow the user to interact with the slider, we temporarily have to stop moving the
233
- // "thumb," but leave the player in the "isPlaying" state so that the play button
234
- // remains and audio picks back up once the user is done interacting (the last bit is
235
- // handled in the `handleSeekChange` handler, when the user actually commits the change)
236
- cancelAnimationFrame(this.animationFrameId as number);
237
- }
238
-
239
- this.currentTimeSeconds = parseInt(
240
- this.typedRefs.playerSeekSlider.value,
241
- 10
242
- );
243
- };
244
-
245
- // `change` is only fired once the user *commits* to a value (e.g., by releasing the mouse click), unlike `input` above
246
- handleSeekChange = () => {
247
- if (this.isPlaying) {
248
- // Resume playing after user finishes interacting with seek slider, if the audio was
249
- // already playing (see note in `handleSeekInput`)
250
- this.syncTimeWithAudio();
251
- }
252
-
253
- this.currentTimeSeconds = parseInt(
254
- this.typedRefs.playerSeekSlider.value,
255
- 10
256
- );
257
- this.typedRefs.audioElement!.currentTime = this.currentTimeSeconds;
258
- };
259
-
260
- // Handles any user-driven movement on the volume slider
261
- handleVolumeInput = ({
262
- currentTarget
263
- }: InputEvent & { currentTarget: HTMLInputElement }) => {
264
- this.volume = parseInt(currentTarget.value, 10);
265
- this.typedRefs.audioElement.volume = this.volume / 100;
266
- };
267
-
268
- // Direct click on the volume button is mute/unmute
269
- handleVolumeClick = () => {
270
- if (this.typedRefs.audioElement.muted) {
271
- this.typedRefs.audioElement.muted = false;
272
- this.volumeIcon = "volume_high";
273
- this.volume = this.prevVolume; // restore to previous volume level
274
- } else {
275
- this.typedRefs.audioElement.muted = true;
276
- this.volumeIcon = "volume_off";
277
- this.volume = 0;
278
- }
279
- };
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
-
300
- // Set "pane" state for playback popover menu
301
- handlePlaybackSpeedClick = () => {
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;
309
- };
310
-
311
- // Handle selection of a playback speed in the "three dot" menu
312
- handleSetPlaybackSpeed = (event: Event) => {
313
- const currentTarget = event.currentTarget as Node;
314
- const selectedPlaybackSpeed =
315
- currentTarget.textContent as PlaybackSpeed;
316
-
317
- if (selectedPlaybackSpeed === this.currentPlaybackSpeed) {
318
- return;
319
- }
320
-
321
- this.playbackSpeedData = formattedPlaybackSpeeds.map((value) => ({
322
- selected: value === selectedPlaybackSpeed,
323
- value
324
- }));
325
- this.currentPlaybackSpeed = selectedPlaybackSpeed;
326
- this.typedRefs.playbackSpeedPopover.closePopover();
327
- this.resetIsSettingPlaybackSpeed();
328
- this.typedRefs.audioElement.playbackRate =
329
- selectedPlaybackSpeed === "Normal"
330
- ? 1
331
- : parseFloat(selectedPlaybackSpeed);
332
- };
333
-
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
- }
342
- };
343
-
344
- /* END event handlers, BEGIN private utility methods */
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
-
351
- // Animates the seek slider (timeline) each frame so that it is N*Sync with current play
352
- // time. We do this using requestAnimationFrame so that we can temporarily cancel the
353
- // animating whenever the user is interacting with the slider.
354
- private syncTimeWithAudio = () => {
355
- this.currentTimeSeconds = Math.floor(
356
- this.typedRefs.audioElement.currentTime
357
- );
358
- this.animationFrameId = requestAnimationFrame(this.syncTimeWithAudio);
359
- };
360
-
361
- // Convert raw numerical seconds to strings formatted as "X:XX"
362
- private getFormatted(totalSeconds: number) {
363
- const minutes = Math.floor(totalSeconds / 60);
364
- const seconds = Math.floor(totalSeconds % 60);
365
- const doubleDigitSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
366
- return `${minutes}:${doubleDigitSeconds}`;
367
- }
368
-
369
- // Find the end of the buffered amount of audio, if any, that overlaps with the current
370
- // position. We care about the overlap because buffered time ranges can have gaps.
371
- // Memoized because it is called whenever input styles are updated, and bufferedTimeRanges
372
- // rarely changes.
373
- private getOverlappingBufferEnd = memoize(
374
- (currentTimeSeconds: number, bufferedTimeRanges?: TimeRanges) => {
375
- let nearestBufferEnd = 0;
376
-
377
- if (bufferedTimeRanges?.length) {
378
- for (let i = 0; i < bufferedTimeRanges.length; i++) {
379
- if (
380
- bufferedTimeRanges.start(i) <= currentTimeSeconds &&
381
- bufferedTimeRanges.end(i) > currentTimeSeconds
382
- ) {
383
- nearestBufferEnd = bufferedTimeRanges.end(i);
384
- break;
385
- }
386
- }
387
- }
388
-
389
- return nearestBufferEnd;
390
- }
391
- );
392
-
393
- // Visually updates sliders (timeline or volume) so that the amount "covered" (before the
394
- // "thumb") is visually different than the amount remaining; also handles visually showing the
395
- // buffered amount if there is buffer data and the element is the "seek"/timeline slider
396
- private updateRangeInputStyles(
397
- target: HTMLInputElement | null,
398
- currentValue: number
399
- ) {
400
- if (
401
- !target ||
402
- (target === this.typedRefs.playerSeekSlider &&
403
- !this.durationSeconds)
404
- ) {
405
- return;
406
- }
407
-
408
- const maximumValue =
409
- target === this.typedRefs.playerVolumeSlider
410
- ? 100
411
- : this.durationSeconds;
412
- const trackColors =
413
- target === this.typedRefs.playerVolumeSlider
414
- ? uiConfig.volumeSlider.trackColors
415
- : uiConfig.seekSlider.trackColors;
416
- const percentageCovered =
417
- maximumValue === 100
418
- ? currentValue
419
- : (currentValue / maximumValue) * 100;
420
- const overlappingBufferEnd = this.getOverlappingBufferEnd(
421
- this.currentTimeSeconds,
422
- this.bufferedTimeRanges
423
- );
424
-
425
- if (trackColors.buffer && overlappingBufferEnd) {
426
- // Slider with three colors: the before amount, the buffered amount, and the full
427
- // "remaining" slider color.
428
- const bufferPercentage =
429
- (overlappingBufferEnd / maximumValue) * 100;
430
- target.style.background = `linear-gradient(to right, ${trackColors.before} ${percentageCovered}%, ${trackColors.buffer} ${percentageCovered}% ${bufferPercentage}%, ${trackColors.after} ${bufferPercentage}%)`;
431
- } else {
432
- // "Basic" slider, with only two colors, no buffering.
433
- target.style.background = `linear-gradient(to right, ${trackColors.before} ${percentageCovered}%, ${trackColors.after} ${percentageCovered}%)`;
434
- }
435
- }
436
-
437
- private trackClick = (e: Event, eventType: string, action: string) => {
438
32
  const payload = {
439
33
  event: eventType,
440
34
  media_action: action,
441
- media_name: this.audioTitle,
442
- media_percentage_played:
443
- this.durationSeconds === 0
444
- ? this.durationSeconds
445
- : (
446
- (this.currentTimeSeconds * 100) /
447
- this.durationSeconds
448
- ).toFixed(0),
449
- media_seconds_played: this.currentTimeSeconds,
35
+ media_name: this.postName,
36
+ media_percentage_played: (
37
+ (audioElement!.currentTime * 100) /
38
+ audioElement!.duration
39
+ ).toFixed(0),
40
+ media_seconds_played: audioElement!.currentTime,
450
41
  media_type: "blog audio"
451
42
  };
452
43
 
453
44
  track(e.currentTarget!, "custEv_blogAudioPlay", payload);
454
- };
45
+ }
455
46
 
456
- private trackPlay = (e: Event) => {
47
+ private trackPlay(e: Event) {
457
48
  this.trackClick(e, "custEv_blogAudioPlay", "play");
458
- };
459
-
460
- private trackEnded = (e: Event) => {
49
+ }
50
+ private trackEnded(e: Event) {
461
51
  this.trackClick(e, "custEv_blogAudioComplete", "complete");
462
- };
52
+ }
53
+ private trackPause(e: Event) {
54
+ const audioElement = this.template.querySelector(
55
+ "audio"
56
+ ) as HTMLMediaElement;
463
57
 
464
- private trackPause = (e: Event) => {
465
- this.trackClick(e, "custEv_blogAudioPause", "pause");
466
- };
58
+ //suppress 'pause' events that happen in the last 99% of the duration
59
+ if ((audioElement!.currentTime * 99) / audioElement!.duration < 99) {
60
+ this.trackClick(e, "custEv_blogAudioPause", "pause");
61
+ }
62
+ }
467
63
  }
@@ -6,7 +6,7 @@
6
6
  var(--dx-g-spacing-sm)
7
7
  );
8
8
  --popover-border: var(--dx-c-popover-border, 1px solid var(--dx-g-gray-90));
9
- --popover-padding: var(--dx-c-popover-padding, --dx-g-spacing-sm);
9
+ --popover-padding: var(--dx-g-spacing-sm);
10
10
  }
11
11
 
12
12
  .popover-container {
@@ -6,7 +6,6 @@ import {
6
6
  } from "typings/custom";
7
7
 
8
8
  import {
9
- autoUpdate,
10
9
  computePosition,
11
10
  flip,
12
11
  size,
@@ -32,8 +31,6 @@ const isEventOutsideElements = (
32
31
  );
33
32
 
34
33
  export default class Popover extends LightningElement {
35
- private autoUpdateCleanup?: () => void;
36
-
37
34
  @api offset?: "small" | "medium";
38
35
  @api pagePadding?: number = 16; // padding between dropdown and the edge of the page
39
36
  @api placement?: PopperPlacement = "bottom-start";
@@ -79,10 +76,6 @@ export default class Popover extends LightningElement {
79
76
  this._open = true;
80
77
  this.control.setAttribute("aria-expanded", "true");
81
78
 
82
- if (this.popover) {
83
- this.autoUpdateCleanup = autoUpdate(this.control, this.popover, this.setPosition);
84
- }
85
-
86
79
  this.dispatchEvent(new CustomEvent("open"));
87
80
 
88
81
  setTimeout(() => {
@@ -95,7 +88,6 @@ export default class Popover extends LightningElement {
95
88
  @api
96
89
  closePopover(focusControl: boolean = false) {
97
90
  this._open = false;
98
- this.autoUpdateCleanup?.();
99
91
  if (focusControl && this.control && this.control.focus) {
100
92
  this.control.focus();
101
93
  }
@@ -1,26 +0,0 @@
1
- import { LightningElement } from "lwc";
2
-
3
- // mixin version for more flexibility
4
- export const withTypedRefs = <
5
- RefsType extends LightningElement["refs"] = LightningElement["refs"],
6
- BaseClass extends new (...args: any[]) => any = new (
7
- ...args: any[]
8
- ) => LightningElement
9
- >(
10
- BaseClass: BaseClass
11
- ) => {
12
- return class extends BaseClass {
13
- get typedRefs() {
14
- return this.refs as RefsType;
15
- }
16
- };
17
- };
18
-
19
- // class version if you just want a lightning element with typed refs and nothing else
20
- export class LightningElementWithTypedRefs<
21
- RefsType extends LightningElement["refs"]
22
- > extends LightningElement {
23
- get typedRefs() {
24
- return this.refs as RefsType;
25
- }
26
- }