@internetarchive/elements 1.1.3 → 1.1.4-webdev-9274.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/dist/src/elements/ia-donation-thermometer/ia-donation-thermometer.d.ts +93 -0
- package/dist/src/elements/ia-donation-thermometer/ia-donation-thermometer.d.ts.map +1 -0
- package/dist/src/elements/ia-donation-thermometer/ia-donation-thermometer.js +369 -0
- package/dist/src/elements/ia-donation-thermometer/ia-donation-thermometer.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { LitElement, type CSSResultGroup, type TemplateResult } from 'lit';
|
|
2
|
+
/** What the text beside the bar says: the goal amount, a message, or nothing. */
|
|
3
|
+
export type GoalMessageMode = 'off' | 'amount' | 'message';
|
|
4
|
+
/** Whether the current amount is printed on the bar. */
|
|
5
|
+
export type CurrentAmountMode = 'on' | 'off';
|
|
6
|
+
/**
|
|
7
|
+
* A horizontal progress bar for a fundraising goal, as shown in the
|
|
8
|
+
* archive.org donation banner. The fill grows with `currentAmount`, the amount
|
|
9
|
+
* raised is printed on the fill when it fits and beside it when it doesn't,
|
|
10
|
+
* and the goal (or a goal message) sits to the right of the bar.
|
|
11
|
+
*/
|
|
12
|
+
export declare class IADonationThermometer extends LitElement {
|
|
13
|
+
goalMessageMode: GoalMessageMode;
|
|
14
|
+
goalNearMessage: string;
|
|
15
|
+
goalReachedMessage: string;
|
|
16
|
+
/** The accessible name of the progress bar, for screen readers. */
|
|
17
|
+
label: string;
|
|
18
|
+
goalAmount: number;
|
|
19
|
+
currentAmountMode: CurrentAmountMode;
|
|
20
|
+
currentAmount: number;
|
|
21
|
+
private thermometerValue?;
|
|
22
|
+
private thermometerFill?;
|
|
23
|
+
private thermometerValueWidth;
|
|
24
|
+
private thermometerFillWidth;
|
|
25
|
+
/**
|
|
26
|
+
* Watches the fill and the value label so the label can move to whichever
|
|
27
|
+
* side of the fill has room for it.
|
|
28
|
+
*/
|
|
29
|
+
private resizeObserver?;
|
|
30
|
+
/**
|
|
31
|
+
* The observed nodes are tracked so each is observed exactly once. Calling
|
|
32
|
+
* `observe` on an already-observed target re-reports its size, which costs a
|
|
33
|
+
* notification on every render. Lit also discards the value label when
|
|
34
|
+
* `currentAmountMode` turns it off, so a replacement has to be picked up.
|
|
35
|
+
*/
|
|
36
|
+
private observedFillElement;
|
|
37
|
+
private observedValueElement;
|
|
38
|
+
render(): TemplateResult;
|
|
39
|
+
private get thermometerValueTemplate();
|
|
40
|
+
/**
|
|
41
|
+
* Which side of the fill's end the value label sits on, as a class name for
|
|
42
|
+
* the CSS that places it. The label sits over the fill when the fill is
|
|
43
|
+
* wider than the label plus a little breathing room, and just past the
|
|
44
|
+
* fill's right edge otherwise.
|
|
45
|
+
*/
|
|
46
|
+
private get thermometerValuePosition();
|
|
47
|
+
connectedCallback(): void;
|
|
48
|
+
disconnectedCallback(): void;
|
|
49
|
+
updated(): void;
|
|
50
|
+
/** Observes the fill and the value label, picking up either if it is replaced. */
|
|
51
|
+
private observeParts;
|
|
52
|
+
/**
|
|
53
|
+
* Swaps `observed` for `current` on the observer and returns whichever is
|
|
54
|
+
* now being watched. A target that hasn't changed is left alone.
|
|
55
|
+
*/
|
|
56
|
+
private observeInPlaceOf;
|
|
57
|
+
private handleResize;
|
|
58
|
+
private get goalMessage();
|
|
59
|
+
private get currentAmountDisplayValue();
|
|
60
|
+
private get goalAmountDisplayValue();
|
|
61
|
+
/**
|
|
62
|
+
* Formats a dollar amount in millions, e.g. `$0.5MM` or `$35MM`. Amounts
|
|
63
|
+
* under ten million keep one decimal place, larger ones round to a whole
|
|
64
|
+
* number.
|
|
65
|
+
*/
|
|
66
|
+
private formatNumber;
|
|
67
|
+
private get currentGoalMessage();
|
|
68
|
+
/**
|
|
69
|
+
* The goal the bar measures against. A goal that isn't a positive, finite
|
|
70
|
+
* number leaves the bar empty, since there is no range to report progress
|
|
71
|
+
* against. Named clear of `ariaValueMax`, which `HTMLElement` declares as a
|
|
72
|
+
* public string.
|
|
73
|
+
*/
|
|
74
|
+
private get progressMax();
|
|
75
|
+
/**
|
|
76
|
+
* The progress reported to screen readers, clamped into the
|
|
77
|
+
* `aria-valuemin`/`aria-valuemax` range the spec requires.
|
|
78
|
+
* `aria-valuetext` carries the amount as it is displayed.
|
|
79
|
+
*/
|
|
80
|
+
private get progressValue();
|
|
81
|
+
/**
|
|
82
|
+
* How much of the bar is filled, as a percentage, clamped to `0`-`100`. An
|
|
83
|
+
* amount or goal that can't produce a usable percentage empties the bar.
|
|
84
|
+
*/
|
|
85
|
+
private get percentComplete();
|
|
86
|
+
static get styles(): CSSResultGroup;
|
|
87
|
+
}
|
|
88
|
+
declare global {
|
|
89
|
+
interface HTMLElementTagNameMap {
|
|
90
|
+
'ia-donation-thermometer': IADonationThermometer;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=ia-donation-thermometer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ia-donation-thermometer.d.ts","sourceRoot":"","sources":["../../../../src/elements/ia-donation-thermometer/ia-donation-thermometer.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,KAAK,CAAC;AAOb,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE3D,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,KAAK,CAAC;AAE7C;;;;;GAKG;AACH,qBACa,qBAAsB,SAAQ,UAAU;IACvB,eAAe,EAAE,eAAe,CAAY;IAE5C,eAAe,SAEzC;IAE0B,kBAAkB,SAE5C;IAEF,mEAAmE;IACvC,KAAK,SAA4B;IAEjC,UAAU,SAAa;IAEvB,iBAAiB,EAAE,iBAAiB,CAAQ;IAE5C,aAAa,SAAK;IAEjB,OAAO,CAAC,gBAAgB,CAAC,CAAiB;IAE3C,OAAO,CAAC,eAAe,CAAC,CAAiB;IAE5D,OAAO,CAAC,qBAAqB,CAAK;IAElC,OAAO,CAAC,oBAAoB,CAAK;IAE1C;;;OAGG;IACH,OAAO,CAAC,cAAc,CAAC,CAAiB;IAExC;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB,CAAwB;IAEnD,OAAO,CAAC,oBAAoB,CAAwB;IAEpD,MAAM,IAAI,cAAc;IA2BxB,OAAO,KAAK,wBAAwB,GAMnC;IAED;;;;;OAKG;IACH,OAAO,KAAK,wBAAwB,GAKnC;IAED,iBAAiB,IAAI,IAAI;IAUzB,oBAAoB,IAAI,IAAI;IAQ5B,OAAO,IAAI,IAAI;IAIf,kFAAkF;IAClF,OAAO,CAAC,YAAY;IAgBpB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,YAAY;IAYpB,OAAO,KAAK,WAAW,GAItB;IAED,OAAO,KAAK,yBAAyB,GAEpC;IAED,OAAO,KAAK,sBAAsB,GAEjC;IAED;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAYpB,OAAO,KAAK,kBAAkB,GAS7B;IAED;;;;;OAKG;IACH,OAAO,KAAK,WAAW,GAItB;IAED;;;;OAIG;IACH,OAAO,KAAK,aAAa,GAGxB;IAED;;;OAGG;IACH,OAAO,KAAK,eAAe,GAK1B;IAED,MAAM,KAAK,MAAM,IAAI,cAAc,CAmIlC;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,yBAAyB,EAAE,qBAAqB,CAAC;KAClD;CACF"}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { css, html, LitElement, nothing, } from 'lit';
|
|
3
|
+
import { property, query, state } from 'lit/decorators.js';
|
|
4
|
+
import { customElement } from '../../util/custom-element.js';
|
|
5
|
+
import { msg, str } from '@lit/localize';
|
|
6
|
+
import themeStyles from '../../themes/theme-styles.js';
|
|
7
|
+
/**
|
|
8
|
+
* A horizontal progress bar for a fundraising goal, as shown in the
|
|
9
|
+
* archive.org donation banner. The fill grows with `currentAmount`, the amount
|
|
10
|
+
* raised is printed on the fill when it fits and beside it when it doesn't,
|
|
11
|
+
* and the goal (or a goal message) sits to the right of the bar.
|
|
12
|
+
*/
|
|
13
|
+
let IADonationThermometer = class IADonationThermometer extends LitElement {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.goalMessageMode = 'amount';
|
|
17
|
+
this.goalNearMessage = msg('We’ve almost reached our goal!');
|
|
18
|
+
this.goalReachedMessage = msg('We’ve reached our goal!');
|
|
19
|
+
/** The accessible name of the progress bar, for screen readers. */
|
|
20
|
+
this.label = msg('Donation progress');
|
|
21
|
+
this.goalAmount = 7_500_000;
|
|
22
|
+
this.currentAmountMode = 'on';
|
|
23
|
+
this.currentAmount = 0;
|
|
24
|
+
this.thermometerValueWidth = 0;
|
|
25
|
+
this.thermometerFillWidth = 0;
|
|
26
|
+
/**
|
|
27
|
+
* The observed nodes are tracked so each is observed exactly once. Calling
|
|
28
|
+
* `observe` on an already-observed target re-reports its size, which costs a
|
|
29
|
+
* notification on every render. Lit also discards the value label when
|
|
30
|
+
* `currentAmountMode` turns it off, so a replacement has to be picked up.
|
|
31
|
+
*/
|
|
32
|
+
this.observedFillElement = null;
|
|
33
|
+
this.observedValueElement = null;
|
|
34
|
+
}
|
|
35
|
+
render() {
|
|
36
|
+
return html `
|
|
37
|
+
<div class="thermometer-message-container">
|
|
38
|
+
<div class="thermometer-container">
|
|
39
|
+
<div
|
|
40
|
+
class="thermometer-background ${this.thermometerValuePosition}"
|
|
41
|
+
style="--fill-end--: ${this.percentComplete}%"
|
|
42
|
+
role="progressbar"
|
|
43
|
+
aria-label="${this.label}"
|
|
44
|
+
aria-valuemin="0"
|
|
45
|
+
aria-valuemax="${this.progressMax}"
|
|
46
|
+
aria-valuenow="${this.progressValue}"
|
|
47
|
+
aria-valuetext="${this.currentAmountDisplayValue}"
|
|
48
|
+
>
|
|
49
|
+
<div class="thermometer-clip">
|
|
50
|
+
<div class="thermometer-fill"></div>
|
|
51
|
+
</div>
|
|
52
|
+
${this.thermometerValueTemplate}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
${this.goalMessageMode !== 'off'
|
|
56
|
+
? html `<div class="donate-goal">${this.currentGoalMessage}</div>`
|
|
57
|
+
: nothing}
|
|
58
|
+
</div>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
get thermometerValueTemplate() {
|
|
62
|
+
return this.currentAmountMode === 'off'
|
|
63
|
+
? nothing
|
|
64
|
+
: html `
|
|
65
|
+
<div class="thermometer-value">${this.currentAmountDisplayValue}</div>
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Which side of the fill's end the value label sits on, as a class name for
|
|
70
|
+
* the CSS that places it. The label sits over the fill when the fill is
|
|
71
|
+
* wider than the label plus a little breathing room, and just past the
|
|
72
|
+
* fill's right edge otherwise.
|
|
73
|
+
*/
|
|
74
|
+
get thermometerValuePosition() {
|
|
75
|
+
const buffer = 10;
|
|
76
|
+
return this.thermometerValueWidth + buffer < this.thermometerFillWidth
|
|
77
|
+
? 'value-left'
|
|
78
|
+
: 'value-right';
|
|
79
|
+
}
|
|
80
|
+
connectedCallback() {
|
|
81
|
+
super.connectedCallback();
|
|
82
|
+
this.resizeObserver = new ResizeObserver((entries) => this.handleResize(entries));
|
|
83
|
+
// On a re-connect the fill already exists; on first connect it doesn't
|
|
84
|
+
// until the first render, and `updated` picks it up then.
|
|
85
|
+
this.observeParts();
|
|
86
|
+
}
|
|
87
|
+
disconnectedCallback() {
|
|
88
|
+
super.disconnectedCallback();
|
|
89
|
+
this.resizeObserver?.disconnect();
|
|
90
|
+
this.resizeObserver = undefined;
|
|
91
|
+
this.observedFillElement = null;
|
|
92
|
+
this.observedValueElement = null;
|
|
93
|
+
}
|
|
94
|
+
updated() {
|
|
95
|
+
this.observeParts();
|
|
96
|
+
}
|
|
97
|
+
/** Observes the fill and the value label, picking up either if it is replaced. */
|
|
98
|
+
observeParts() {
|
|
99
|
+
const observer = this.resizeObserver;
|
|
100
|
+
if (!observer)
|
|
101
|
+
return;
|
|
102
|
+
this.observedFillElement = this.observeInPlaceOf(observer, this.observedFillElement, this.thermometerFill ?? null);
|
|
103
|
+
this.observedValueElement = this.observeInPlaceOf(observer, this.observedValueElement, this.thermometerValue ?? null);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Swaps `observed` for `current` on the observer and returns whichever is
|
|
107
|
+
* now being watched. A target that hasn't changed is left alone.
|
|
108
|
+
*/
|
|
109
|
+
observeInPlaceOf(observer, observed, current) {
|
|
110
|
+
if (current === observed)
|
|
111
|
+
return observed;
|
|
112
|
+
if (observed)
|
|
113
|
+
observer.unobserve(observed);
|
|
114
|
+
if (current)
|
|
115
|
+
observer.observe(current);
|
|
116
|
+
return current;
|
|
117
|
+
}
|
|
118
|
+
handleResize(entries) {
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
|
|
121
|
+
if (entry.target === this.observedFillElement) {
|
|
122
|
+
this.thermometerFillWidth = width;
|
|
123
|
+
}
|
|
124
|
+
else if (entry.target === this.observedValueElement) {
|
|
125
|
+
this.thermometerValueWidth = width;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
get goalMessage() {
|
|
130
|
+
return this.currentAmount >= this.goalAmount
|
|
131
|
+
? this.goalReachedMessage
|
|
132
|
+
: this.goalNearMessage;
|
|
133
|
+
}
|
|
134
|
+
get currentAmountDisplayValue() {
|
|
135
|
+
return this.formatNumber(this.currentAmount);
|
|
136
|
+
}
|
|
137
|
+
get goalAmountDisplayValue() {
|
|
138
|
+
return this.formatNumber(this.goalAmount);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Formats a dollar amount in millions, e.g. `$0.5MM` or `$35MM`. Amounts
|
|
142
|
+
* under ten million keep one decimal place, larger ones round to a whole
|
|
143
|
+
* number.
|
|
144
|
+
*/
|
|
145
|
+
formatNumber(number) {
|
|
146
|
+
if (!Number.isFinite(number) || number === 0)
|
|
147
|
+
return '$0';
|
|
148
|
+
const suffix = 'MM';
|
|
149
|
+
const divisor = 1_000_000;
|
|
150
|
+
const result = number / divisor;
|
|
151
|
+
const roundToOne = result < 10;
|
|
152
|
+
const rounded = roundToOne
|
|
153
|
+
? Math.round((result + Number.EPSILON) * 10) / 10
|
|
154
|
+
: Math.round(result);
|
|
155
|
+
return `$${rounded}${suffix}`;
|
|
156
|
+
}
|
|
157
|
+
get currentGoalMessage() {
|
|
158
|
+
switch (this.goalMessageMode) {
|
|
159
|
+
case 'amount':
|
|
160
|
+
return msg(str `${this.goalAmountDisplayValue} goal`);
|
|
161
|
+
case 'message':
|
|
162
|
+
return this.goalMessage;
|
|
163
|
+
case 'off':
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The goal the bar measures against. A goal that isn't a positive, finite
|
|
169
|
+
* number leaves the bar empty, since there is no range to report progress
|
|
170
|
+
* against. Named clear of `ariaValueMax`, which `HTMLElement` declares as a
|
|
171
|
+
* public string.
|
|
172
|
+
*/
|
|
173
|
+
get progressMax() {
|
|
174
|
+
return Number.isFinite(this.goalAmount) && this.goalAmount > 0
|
|
175
|
+
? this.goalAmount
|
|
176
|
+
: 0;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The progress reported to screen readers, clamped into the
|
|
180
|
+
* `aria-valuemin`/`aria-valuemax` range the spec requires.
|
|
181
|
+
* `aria-valuetext` carries the amount as it is displayed.
|
|
182
|
+
*/
|
|
183
|
+
get progressValue() {
|
|
184
|
+
if (!Number.isFinite(this.currentAmount))
|
|
185
|
+
return 0;
|
|
186
|
+
return Math.min(Math.max(this.currentAmount, 0), this.progressMax);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* How much of the bar is filled, as a percentage, clamped to `0`-`100`. An
|
|
190
|
+
* amount or goal that can't produce a usable percentage empties the bar.
|
|
191
|
+
*/
|
|
192
|
+
get percentComplete() {
|
|
193
|
+
const goal = this.progressMax;
|
|
194
|
+
if (goal === 0 || !Number.isFinite(this.currentAmount))
|
|
195
|
+
return 0;
|
|
196
|
+
const percent = (this.currentAmount / goal) * 100;
|
|
197
|
+
return Math.min(Math.max(percent, 0), 100);
|
|
198
|
+
}
|
|
199
|
+
static get styles() {
|
|
200
|
+
return [
|
|
201
|
+
themeStyles,
|
|
202
|
+
css `
|
|
203
|
+
:host {
|
|
204
|
+
--donation-thermometer-height--: var(
|
|
205
|
+
--ia-donation-thermometer-height,
|
|
206
|
+
20px
|
|
207
|
+
);
|
|
208
|
+
--donation-thermometer-fill-color--: var(
|
|
209
|
+
--ia-donation-thermometer-fill-color,
|
|
210
|
+
#23765d
|
|
211
|
+
);
|
|
212
|
+
--donation-thermometer-track-color--: var(
|
|
213
|
+
--ia-donation-thermometer-track-color,
|
|
214
|
+
#b8f5e2
|
|
215
|
+
);
|
|
216
|
+
--donation-thermometer-border--: var(
|
|
217
|
+
--ia-donation-thermometer-border,
|
|
218
|
+
1px solid var(--donation-thermometer-fill-color--)
|
|
219
|
+
);
|
|
220
|
+
/* Large enough to round any height into a pill */
|
|
221
|
+
--donation-thermometer-border-radius--: var(
|
|
222
|
+
--ia-donation-thermometer-border-radius,
|
|
223
|
+
9999px
|
|
224
|
+
);
|
|
225
|
+
--donation-thermometer-value-on-fill-color--: var(
|
|
226
|
+
--ia-donation-thermometer-value-on-fill-color,
|
|
227
|
+
var(--true-white)
|
|
228
|
+
);
|
|
229
|
+
--donation-thermometer-value-on-track-color--: var(
|
|
230
|
+
--ia-donation-thermometer-value-on-track-color,
|
|
231
|
+
var(--donation-thermometer-fill-color--)
|
|
232
|
+
);
|
|
233
|
+
--donation-thermometer-goal-color--: var(--primary-text-color);
|
|
234
|
+
--donation-thermometer-goal-padding--: var(
|
|
235
|
+
--ia-donation-thermometer-goal-padding,
|
|
236
|
+
0 10px
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
display: block;
|
|
240
|
+
height: var(--donation-thermometer-height--);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.thermometer-message-container {
|
|
244
|
+
height: 100%;
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.thermometer-container {
|
|
250
|
+
height: 100%;
|
|
251
|
+
flex: 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.thermometer-background {
|
|
255
|
+
box-sizing: border-box;
|
|
256
|
+
position: relative;
|
|
257
|
+
background-color: var(--donation-thermometer-track-color--);
|
|
258
|
+
height: 100%;
|
|
259
|
+
border-radius: var(--donation-thermometer-border-radius--);
|
|
260
|
+
border: var(--donation-thermometer-border--);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/*
|
|
264
|
+
* Clips the square-ended fill to the rounded track. It covers only the
|
|
265
|
+
* fill, so the label is never cut off by it.
|
|
266
|
+
*/
|
|
267
|
+
.thermometer-clip {
|
|
268
|
+
position: absolute;
|
|
269
|
+
inset: 0;
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
border-radius: var(--donation-thermometer-border-radius--);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.thermometer-fill {
|
|
275
|
+
background-color: var(--donation-thermometer-fill-color--);
|
|
276
|
+
height: 100%;
|
|
277
|
+
/*
|
|
278
|
+
* The fallback matters: without it a missing --fill-end-- makes the
|
|
279
|
+
* declaration invalid, width resolves to auto, and the bar paints as
|
|
280
|
+
* a fully funded fundraiser.
|
|
281
|
+
*/
|
|
282
|
+
width: var(--fill-end--, 0%);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/*
|
|
286
|
+
* The label is placed by offset from the end of the fill, so it is one
|
|
287
|
+
* node in one position in the DOM whichever side it shows on, and it
|
|
288
|
+
* never counts towards the width of the fill it is measured against.
|
|
289
|
+
*/
|
|
290
|
+
.thermometer-value {
|
|
291
|
+
position: absolute;
|
|
292
|
+
top: 0;
|
|
293
|
+
bottom: 0;
|
|
294
|
+
left: var(--fill-end--, 0%);
|
|
295
|
+
/*
|
|
296
|
+
* Without this, the left offset caps the shrink-to-fit width of a
|
|
297
|
+
* translated label and it collapses to its padding.
|
|
298
|
+
*/
|
|
299
|
+
width: max-content;
|
|
300
|
+
display: flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
font-weight: bold;
|
|
303
|
+
white-space: nowrap;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/*
|
|
307
|
+
* Both paddings total 1.5rem, so changing sides leaves the label the
|
|
308
|
+
* same width and cannot feed back into the side it is placed on.
|
|
309
|
+
*/
|
|
310
|
+
.value-left .thermometer-value {
|
|
311
|
+
transform: translateX(-100%);
|
|
312
|
+
color: var(--donation-thermometer-value-on-fill-color--);
|
|
313
|
+
padding: 0 0.5rem 0 1rem;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.value-right .thermometer-value {
|
|
317
|
+
color: var(--donation-thermometer-value-on-track-color--);
|
|
318
|
+
padding: 0 1rem 0 0.5rem;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.donate-goal {
|
|
322
|
+
text-align: left;
|
|
323
|
+
padding: var(--donation-thermometer-goal-padding--);
|
|
324
|
+
text-transform: uppercase;
|
|
325
|
+
font-weight: bold;
|
|
326
|
+
color: var(--donation-thermometer-goal-color--);
|
|
327
|
+
}
|
|
328
|
+
`,
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
__decorate([
|
|
333
|
+
property({ type: String })
|
|
334
|
+
], IADonationThermometer.prototype, "goalMessageMode", void 0);
|
|
335
|
+
__decorate([
|
|
336
|
+
property({ type: String })
|
|
337
|
+
], IADonationThermometer.prototype, "goalNearMessage", void 0);
|
|
338
|
+
__decorate([
|
|
339
|
+
property({ type: String })
|
|
340
|
+
], IADonationThermometer.prototype, "goalReachedMessage", void 0);
|
|
341
|
+
__decorate([
|
|
342
|
+
property({ type: String })
|
|
343
|
+
], IADonationThermometer.prototype, "label", void 0);
|
|
344
|
+
__decorate([
|
|
345
|
+
property({ type: Number })
|
|
346
|
+
], IADonationThermometer.prototype, "goalAmount", void 0);
|
|
347
|
+
__decorate([
|
|
348
|
+
property({ type: String })
|
|
349
|
+
], IADonationThermometer.prototype, "currentAmountMode", void 0);
|
|
350
|
+
__decorate([
|
|
351
|
+
property({ type: Number })
|
|
352
|
+
], IADonationThermometer.prototype, "currentAmount", void 0);
|
|
353
|
+
__decorate([
|
|
354
|
+
query('.thermometer-value')
|
|
355
|
+
], IADonationThermometer.prototype, "thermometerValue", void 0);
|
|
356
|
+
__decorate([
|
|
357
|
+
query('.thermometer-fill')
|
|
358
|
+
], IADonationThermometer.prototype, "thermometerFill", void 0);
|
|
359
|
+
__decorate([
|
|
360
|
+
state()
|
|
361
|
+
], IADonationThermometer.prototype, "thermometerValueWidth", void 0);
|
|
362
|
+
__decorate([
|
|
363
|
+
state()
|
|
364
|
+
], IADonationThermometer.prototype, "thermometerFillWidth", void 0);
|
|
365
|
+
IADonationThermometer = __decorate([
|
|
366
|
+
customElement('ia-donation-thermometer')
|
|
367
|
+
], IADonationThermometer);
|
|
368
|
+
export { IADonationThermometer };
|
|
369
|
+
//# sourceMappingURL=ia-donation-thermometer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ia-donation-thermometer.js","sourceRoot":"","sources":["../../../../src/elements/ia-donation-thermometer/ia-donation-thermometer.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n css,\n html,\n LitElement,\n nothing,\n type CSSResultGroup,\n type TemplateResult,\n} from 'lit';\nimport { property, query, state } from 'lit/decorators.js';\nimport { customElement } from '@src/util/custom-element';\nimport { msg, str } from '@lit/localize';\n\nimport themeStyles from '@src/themes/theme-styles';\n\n/** What the text beside the bar says: the goal amount, a message, or nothing. */\nexport type GoalMessageMode = 'off' | 'amount' | 'message';\n\n/** Whether the current amount is printed on the bar. */\nexport type CurrentAmountMode = 'on' | 'off';\n\n/**\n * A horizontal progress bar for a fundraising goal, as shown in the\n * archive.org donation banner. The fill grows with `currentAmount`, the amount\n * raised is printed on the fill when it fits and beside it when it doesn't,\n * and the goal (or a goal message) sits to the right of the bar.\n */\n@customElement('ia-donation-thermometer')\nexport class IADonationThermometer extends LitElement {\n @property({ type: String }) goalMessageMode: GoalMessageMode = 'amount';\n\n @property({ type: String }) goalNearMessage = msg(\n 'We’ve almost reached our goal!',\n );\n\n @property({ type: String }) goalReachedMessage = msg(\n 'We’ve reached our goal!',\n );\n\n /** The accessible name of the progress bar, for screen readers. */\n @property({ type: String }) label = msg('Donation progress');\n\n @property({ type: Number }) goalAmount = 7_500_000;\n\n @property({ type: String }) currentAmountMode: CurrentAmountMode = 'on';\n\n @property({ type: Number }) currentAmount = 0;\n\n @query('.thermometer-value') private thermometerValue?: HTMLDivElement;\n\n @query('.thermometer-fill') private thermometerFill?: HTMLDivElement;\n\n @state() private thermometerValueWidth = 0;\n\n @state() private thermometerFillWidth = 0;\n\n /**\n * Watches the fill and the value label so the label can move to whichever\n * side of the fill has room for it.\n */\n private resizeObserver?: ResizeObserver;\n\n /**\n * The observed nodes are tracked so each is observed exactly once. Calling\n * `observe` on an already-observed target re-reports its size, which costs a\n * notification on every render. Lit also discards the value label when\n * `currentAmountMode` turns it off, so a replacement has to be picked up.\n */\n private observedFillElement: Element | null = null;\n\n private observedValueElement: Element | null = null;\n\n render(): TemplateResult {\n return html`\n <div class=\"thermometer-message-container\">\n <div class=\"thermometer-container\">\n <div\n class=\"thermometer-background ${this.thermometerValuePosition}\"\n style=\"--fill-end--: ${this.percentComplete}%\"\n role=\"progressbar\"\n aria-label=\"${this.label}\"\n aria-valuemin=\"0\"\n aria-valuemax=\"${this.progressMax}\"\n aria-valuenow=\"${this.progressValue}\"\n aria-valuetext=\"${this.currentAmountDisplayValue}\"\n >\n <div class=\"thermometer-clip\">\n <div class=\"thermometer-fill\"></div>\n </div>\n ${this.thermometerValueTemplate}\n </div>\n </div>\n ${this.goalMessageMode !== 'off'\n ? html`<div class=\"donate-goal\">${this.currentGoalMessage}</div>`\n : nothing}\n </div>\n `;\n }\n\n private get thermometerValueTemplate(): TemplateResult | typeof nothing {\n return this.currentAmountMode === 'off'\n ? nothing\n : html`\n <div class=\"thermometer-value\">${this.currentAmountDisplayValue}</div>\n `;\n }\n\n /**\n * Which side of the fill's end the value label sits on, as a class name for\n * the CSS that places it. The label sits over the fill when the fill is\n * wider than the label plus a little breathing room, and just past the\n * fill's right edge otherwise.\n */\n private get thermometerValuePosition(): 'value-left' | 'value-right' {\n const buffer = 10;\n return this.thermometerValueWidth + buffer < this.thermometerFillWidth\n ? 'value-left'\n : 'value-right';\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.resizeObserver = new ResizeObserver((entries) =>\n this.handleResize(entries),\n );\n // On a re-connect the fill already exists; on first connect it doesn't\n // until the first render, and `updated` picks it up then.\n this.observeParts();\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n this.observedFillElement = null;\n this.observedValueElement = null;\n }\n\n updated(): void {\n this.observeParts();\n }\n\n /** Observes the fill and the value label, picking up either if it is replaced. */\n private observeParts(): void {\n const observer = this.resizeObserver;\n if (!observer) return;\n\n this.observedFillElement = this.observeInPlaceOf(\n observer,\n this.observedFillElement,\n this.thermometerFill ?? null,\n );\n this.observedValueElement = this.observeInPlaceOf(\n observer,\n this.observedValueElement,\n this.thermometerValue ?? null,\n );\n }\n\n /**\n * Swaps `observed` for `current` on the observer and returns whichever is\n * now being watched. A target that hasn't changed is left alone.\n */\n private observeInPlaceOf(\n observer: ResizeObserver,\n observed: Element | null,\n current: Element | null,\n ): Element | null {\n if (current === observed) return observed;\n if (observed) observer.unobserve(observed);\n if (current) observer.observe(current);\n return current;\n }\n\n private handleResize(entries: ResizeObserverEntry[]): void {\n for (const entry of entries) {\n const width =\n entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;\n if (entry.target === this.observedFillElement) {\n this.thermometerFillWidth = width;\n } else if (entry.target === this.observedValueElement) {\n this.thermometerValueWidth = width;\n }\n }\n }\n\n private get goalMessage(): string {\n return this.currentAmount >= this.goalAmount\n ? this.goalReachedMessage\n : this.goalNearMessage;\n }\n\n private get currentAmountDisplayValue(): string {\n return this.formatNumber(this.currentAmount);\n }\n\n private get goalAmountDisplayValue(): string {\n return this.formatNumber(this.goalAmount);\n }\n\n /**\n * Formats a dollar amount in millions, e.g. `$0.5MM` or `$35MM`. Amounts\n * under ten million keep one decimal place, larger ones round to a whole\n * number.\n */\n private formatNumber(number: number): string {\n if (!Number.isFinite(number) || number === 0) return '$0';\n const suffix = 'MM';\n const divisor = 1_000_000;\n const result = number / divisor;\n const roundToOne = result < 10;\n const rounded = roundToOne\n ? Math.round((result + Number.EPSILON) * 10) / 10\n : Math.round(result);\n return `$${rounded}${suffix}`;\n }\n\n private get currentGoalMessage(): string {\n switch (this.goalMessageMode) {\n case 'amount':\n return msg(str`${this.goalAmountDisplayValue} goal`);\n case 'message':\n return this.goalMessage;\n case 'off':\n return '';\n }\n }\n\n /**\n * The goal the bar measures against. A goal that isn't a positive, finite\n * number leaves the bar empty, since there is no range to report progress\n * against. Named clear of `ariaValueMax`, which `HTMLElement` declares as a\n * public string.\n */\n private get progressMax(): number {\n return Number.isFinite(this.goalAmount) && this.goalAmount > 0\n ? this.goalAmount\n : 0;\n }\n\n /**\n * The progress reported to screen readers, clamped into the\n * `aria-valuemin`/`aria-valuemax` range the spec requires.\n * `aria-valuetext` carries the amount as it is displayed.\n */\n private get progressValue(): number {\n if (!Number.isFinite(this.currentAmount)) return 0;\n return Math.min(Math.max(this.currentAmount, 0), this.progressMax);\n }\n\n /**\n * How much of the bar is filled, as a percentage, clamped to `0`-`100`. An\n * amount or goal that can't produce a usable percentage empties the bar.\n */\n private get percentComplete(): number {\n const goal = this.progressMax;\n if (goal === 0 || !Number.isFinite(this.currentAmount)) return 0;\n const percent = (this.currentAmount / goal) * 100;\n return Math.min(Math.max(percent, 0), 100);\n }\n\n static get styles(): CSSResultGroup {\n return [\n themeStyles,\n css`\n :host {\n --donation-thermometer-height--: var(\n --ia-donation-thermometer-height,\n 20px\n );\n --donation-thermometer-fill-color--: var(\n --ia-donation-thermometer-fill-color,\n #23765d\n );\n --donation-thermometer-track-color--: var(\n --ia-donation-thermometer-track-color,\n #b8f5e2\n );\n --donation-thermometer-border--: var(\n --ia-donation-thermometer-border,\n 1px solid var(--donation-thermometer-fill-color--)\n );\n /* Large enough to round any height into a pill */\n --donation-thermometer-border-radius--: var(\n --ia-donation-thermometer-border-radius,\n 9999px\n );\n --donation-thermometer-value-on-fill-color--: var(\n --ia-donation-thermometer-value-on-fill-color,\n var(--true-white)\n );\n --donation-thermometer-value-on-track-color--: var(\n --ia-donation-thermometer-value-on-track-color,\n var(--donation-thermometer-fill-color--)\n );\n --donation-thermometer-goal-color--: var(--primary-text-color);\n --donation-thermometer-goal-padding--: var(\n --ia-donation-thermometer-goal-padding,\n 0 10px\n );\n\n display: block;\n height: var(--donation-thermometer-height--);\n }\n\n .thermometer-message-container {\n height: 100%;\n display: flex;\n align-items: center;\n }\n\n .thermometer-container {\n height: 100%;\n flex: 1;\n }\n\n .thermometer-background {\n box-sizing: border-box;\n position: relative;\n background-color: var(--donation-thermometer-track-color--);\n height: 100%;\n border-radius: var(--donation-thermometer-border-radius--);\n border: var(--donation-thermometer-border--);\n }\n\n /*\n * Clips the square-ended fill to the rounded track. It covers only the\n * fill, so the label is never cut off by it.\n */\n .thermometer-clip {\n position: absolute;\n inset: 0;\n overflow: hidden;\n border-radius: var(--donation-thermometer-border-radius--);\n }\n\n .thermometer-fill {\n background-color: var(--donation-thermometer-fill-color--);\n height: 100%;\n /*\n * The fallback matters: without it a missing --fill-end-- makes the\n * declaration invalid, width resolves to auto, and the bar paints as\n * a fully funded fundraiser.\n */\n width: var(--fill-end--, 0%);\n }\n\n /*\n * The label is placed by offset from the end of the fill, so it is one\n * node in one position in the DOM whichever side it shows on, and it\n * never counts towards the width of the fill it is measured against.\n */\n .thermometer-value {\n position: absolute;\n top: 0;\n bottom: 0;\n left: var(--fill-end--, 0%);\n /*\n * Without this, the left offset caps the shrink-to-fit width of a\n * translated label and it collapses to its padding.\n */\n width: max-content;\n display: flex;\n align-items: center;\n font-weight: bold;\n white-space: nowrap;\n }\n\n /*\n * Both paddings total 1.5rem, so changing sides leaves the label the\n * same width and cannot feed back into the side it is placed on.\n */\n .value-left .thermometer-value {\n transform: translateX(-100%);\n color: var(--donation-thermometer-value-on-fill-color--);\n padding: 0 0.5rem 0 1rem;\n }\n\n .value-right .thermometer-value {\n color: var(--donation-thermometer-value-on-track-color--);\n padding: 0 1rem 0 0.5rem;\n }\n\n .donate-goal {\n text-align: left;\n padding: var(--donation-thermometer-goal-padding--);\n text-transform: uppercase;\n font-weight: bold;\n color: var(--donation-thermometer-goal-color--);\n }\n `,\n ];\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ia-donation-thermometer': IADonationThermometer;\n }\n}\n"]}
|