@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 +0 -1
- package/package.json +2 -3
- package/src/modules/dx/audio/audio.css +12 -253
- package/src/modules/dx/audio/audio.html +1 -140
- package/src/modules/dx/audio/audio.ts +40 -444
- package/src/modules/dx/popover/popover.css +1 -1
- package/src/modules/dx/popover/popover.ts +0 -8
- package/src/modules/dxUtils/withTypedRefs/withTypedRefs.ts +0 -26
package/lwc.config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforcedevs/dx-components",
|
|
3
|
-
"version": "1.3.
|
|
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": "
|
|
47
|
+
"gitHead": "c7d155ee843224d12db375fde4f54840d0317385"
|
|
49
48
|
}
|
|
@@ -1,257 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
5
|
+
/* TODO: we need mobile breakpoints for this, it looks bad on mobile */
|
|
6
|
+
.audio {
|
|
7
|
+
width: 350px;
|
|
174
8
|
display: block;
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
/
|
|
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
|
|
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 {
|
|
2
|
+
import { LightningElement, api } from "lwc";
|
|
6
3
|
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
8
|
+
renderedCallback() {
|
|
9
|
+
const audioElement = this.template.querySelector("audio");
|
|
10
|
+
audioElement?.addEventListener(
|
|
11
|
+
"play",
|
|
12
|
+
this.trackPlay.bind(this),
|
|
13
|
+
false
|
|
162
14
|
);
|
|
163
|
-
|
|
15
|
+
audioElement?.addEventListener(
|
|
164
16
|
"ended",
|
|
165
|
-
this.
|
|
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
|
-
|
|
179
|
-
"
|
|
180
|
-
this.
|
|
20
|
+
audioElement?.addEventListener(
|
|
21
|
+
"pause",
|
|
22
|
+
this.trackPause.bind(this),
|
|
23
|
+
false
|
|
181
24
|
);
|
|
182
25
|
}
|
|
183
26
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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.
|
|
442
|
-
media_percentage_played:
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
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
|
-
|
|
465
|
-
|
|
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,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
|
-
}
|