@internetarchive/histogram-date-range 1.3.2 → 1.4.0-alpha-webdev7756.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.
Files changed (31) hide show
  1. package/demo/index.html +9 -2
  2. package/demo/js/{app-root.ts → lit-histogram-wrapper.ts} +20 -4
  3. package/dist/demo/js/app-root.d.ts +2 -1
  4. package/dist/demo/js/app-root.js +20 -19
  5. package/dist/demo/js/app-root.js.map +1 -1
  6. package/dist/demo/js/list-histogram-wrapper.d.ts +20 -0
  7. package/dist/demo/js/list-histogram-wrapper.js +59 -0
  8. package/dist/demo/js/list-histogram-wrapper.js.map +1 -0
  9. package/dist/demo/js/lit-histogram-wrapper.d.ts +21 -0
  10. package/dist/demo/js/lit-histogram-wrapper.js +73 -0
  11. package/dist/demo/js/lit-histogram-wrapper.js.map +1 -0
  12. package/dist/src/histogram-date-range.d.ts +18 -0
  13. package/dist/src/histogram-date-range.js +282 -242
  14. package/dist/src/histogram-date-range.js.map +1 -1
  15. package/dist/test/histogram-date-range.test.js +202 -178
  16. package/dist/test/histogram-date-range.test.js.map +1 -1
  17. package/docs/demo/index.html +9 -2
  18. package/docs/dist/demo/js/{app-root.js → lit-histogram-wrapper.js} +22 -7
  19. package/docs/dist/src/histogram-date-range.js +34 -11
  20. package/package.json +1 -1
  21. package/src/histogram-date-range.ts +1152 -1110
  22. package/test/histogram-date-range.test.ts +966 -935
  23. package/dist/src/dayjs/fix-first-century-years.d.ts +0 -2
  24. package/dist/src/dayjs/fix-first-century-years.js +0 -25
  25. package/dist/src/dayjs/fix-first-century-years.js.map +0 -1
  26. package/dist/src/dayjs/fix-two-digit-dates.d.ts +0 -2
  27. package/dist/src/dayjs/fix-two-digit-dates.js +0 -25
  28. package/dist/src/dayjs/fix-two-digit-dates.js.map +0 -1
  29. package/dist/src/histogram-date-range copy.d.ts +0 -214
  30. package/dist/src/histogram-date-range copy.js +0 -1018
  31. package/dist/src/histogram-date-range copy.js.map +0 -1
@@ -1,935 +1,966 @@
1
- /* eslint-disable @typescript-eslint/no-unused-expressions */
2
- import { html, fixture, expect, oneEvent, aTimeout } from '@open-wc/testing';
3
-
4
- import { HistogramDateRange } from '../src/histogram-date-range';
5
- import '../src/histogram-date-range';
6
-
7
- const SLIDER_WIDTH = 10;
8
- const WIDTH = 200;
9
-
10
- const subject = html`
11
- <histogram-date-range
12
- width="${WIDTH}"
13
- tooltipWidth="140"
14
- height="50"
15
- dateFormat="M/D/YYYY"
16
- minDate="1900"
17
- maxDate="12/4/2020"
18
- bins="[33, 1, 100]"
19
- >
20
- </histogram-date-range>
21
- `;
22
-
23
- async function createCustomElementInHTMLContainer(): Promise<HistogramDateRange> {
24
- document.head.insertAdjacentHTML(
25
- 'beforeend',
26
- `<style>
27
- html {
28
- font-size:10px;
29
- }
30
- .container {
31
- width: 400px;
32
- height: 400px;
33
- display: flex;
34
- background: #FFF6E1;
35
- justify-content: center;
36
- align-items: center;
37
- }
38
- </style>`
39
- );
40
- // https://open-wc.org/docs/testing/helpers/#customize-the-fixture-container
41
- const parentNode = document.createElement('div');
42
- parentNode.setAttribute('class', 'container');
43
- return fixture<HistogramDateRange>(subject, { parentNode });
44
- }
45
-
46
- describe('HistogramDateRange', () => {
47
- it('shows scaled histogram bars when provided with data', async () => {
48
- const el = await createCustomElementInHTMLContainer();
49
- const bars = el.shadowRoot?.querySelectorAll(
50
- '.bar'
51
- ) as unknown as SVGRectElement[];
52
- const heights = Array.from(bars).map(b => b.height.baseVal.value);
53
-
54
- expect(heights).to.eql([38, 7, 50]);
55
- });
56
-
57
- it('changes the position of the sliders and standardizes date format when dates are entered', async () => {
58
- const el = await createCustomElementInHTMLContainer();
59
-
60
- /* -------------------------- minimum (left) slider ------------------------- */
61
- expect(el.minSliderX).to.eq(SLIDER_WIDTH);
62
- const minDateInput = el.shadowRoot?.querySelector(
63
- '#date-min'
64
- ) as HTMLInputElement;
65
-
66
- const pressEnterEvent = new KeyboardEvent('keyup', {
67
- key: 'Enter',
68
- });
69
-
70
- // valid min date
71
- minDateInput.value = '1950';
72
- minDateInput.dispatchEvent(pressEnterEvent);
73
-
74
- expect(Math.floor(el.minSliderX)).to.eq(84);
75
- expect(el.minSelectedDate).to.eq('1/1/1950'); // set to correct format
76
-
77
- // attempt to set date earlier than first item
78
- minDateInput.value = '10/1/1850';
79
- minDateInput.dispatchEvent(new Event('blur'));
80
-
81
- expect(Math.floor(el.minSliderX)).to.eq(SLIDER_WIDTH); // leftmost valid position
82
- // allow date value less than slider range
83
- expect(el.minSelectedDate).to.eq('10/1/1850');
84
-
85
- /* -------------------------- maximum (right) slider ------------------------- */
86
- expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH);
87
- const maxDateInput = el.shadowRoot?.querySelector(
88
- '#date-max'
89
- ) as HTMLInputElement;
90
-
91
- // set valid max date
92
- maxDateInput.value = '3/12/1975';
93
- maxDateInput.dispatchEvent(pressEnterEvent);
94
-
95
- expect(Math.floor(el.maxSliderX)).to.eq(121);
96
- expect(maxDateInput.value).to.eq('3/12/1975');
97
-
98
- // attempt to set date later than last item
99
- maxDateInput.value = '12/31/2199';
100
- maxDateInput.dispatchEvent(new Event('blur'));
101
- await el.updateComplete;
102
-
103
- expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH); // rightmost valid position
104
- // allow date value greater than slider range
105
- expect(maxDateInput.value).to.eq('12/31/2199');
106
- });
107
-
108
- it('when updateWhileFocused option is true, updates are fired upon changing input focus', async () => {
109
- const el = await createCustomElementInHTMLContainer();
110
- el.updateWhileFocused = true;
111
- await el.updateComplete;
112
-
113
- let updateEventFired = false;
114
- el.addEventListener(
115
- 'histogramDateRangeUpdated',
116
- () => (updateEventFired = true)
117
- );
118
-
119
- /* -------------------------- minimum (left) slider ------------------------- */
120
- const minDateInput = el.shadowRoot?.querySelector(
121
- '#date-min'
122
- ) as HTMLInputElement;
123
-
124
- /* -------------------------- maximum (right) slider ------------------------- */
125
- const maxDateInput = el.shadowRoot?.querySelector(
126
- '#date-max'
127
- ) as HTMLInputElement;
128
-
129
- minDateInput.focus();
130
-
131
- // set valid min date, but don't hit Enter -- just switch focus to the max date input
132
- minDateInput.value = '1950';
133
- maxDateInput.focus();
134
- await el.updateComplete;
135
- await aTimeout(0);
136
-
137
- // update event should have fired, setting the minSelectedDate prop & slider position accordingly
138
- expect(updateEventFired).to.be.true;
139
- expect(Math.floor(el.minSliderX)).to.eq(84);
140
- expect(el.minSelectedDate).to.eq('1/1/1950');
141
-
142
- updateEventFired = false;
143
- await el.updateComplete;
144
-
145
- // set valid max date, but don't hit Enter -- just switch focus to the min date input
146
- maxDateInput.value = '3/12/1975';
147
- minDateInput.focus();
148
- await el.updateComplete;
149
- await aTimeout(0);
150
-
151
- // update event should have fired, setting the maxSelectedDate prop & slider position accordingly
152
- expect(updateEventFired).to.be.true;
153
- expect(Math.floor(el.maxSliderX)).to.eq(121);
154
- expect(el.maxSelectedDate).to.eq('3/12/1975');
155
- });
156
-
157
- it('when updateWhileFocused option is false (default), updates are not fired while one of the inputs remains focused', async () => {
158
- const el = await createCustomElementInHTMLContainer();
159
-
160
- let updateEventFired = false;
161
- el.addEventListener(
162
- 'histogramDateRangeUpdated',
163
- () => (updateEventFired = true)
164
- );
165
-
166
- /* -------------------------- minimum (left) slider ------------------------- */
167
- const minDateInput = el.shadowRoot?.querySelector(
168
- '#date-min'
169
- ) as HTMLInputElement;
170
-
171
- /* -------------------------- maximum (right) slider ------------------------- */
172
- const maxDateInput = el.shadowRoot?.querySelector(
173
- '#date-max'
174
- ) as HTMLInputElement;
175
-
176
- minDateInput.focus();
177
-
178
- // set valid min date, but don't hit Enter -- just switch focus to the max date input
179
- minDateInput.value = '1950';
180
- maxDateInput.focus();
181
- await el.updateComplete;
182
- await aTimeout(0);
183
-
184
- // update event should NOT have fired, because focus remains within the inputs
185
- expect(updateEventFired).to.be.false;
186
-
187
- // set valid max date, but don't hit Enter -- just switch focus to the min date input
188
- maxDateInput.value = '3/12/1975';
189
- minDateInput.focus();
190
- await el.updateComplete;
191
- await aTimeout(0);
192
-
193
- // update event should NOT have fired, because focus remains within the inputs
194
- expect(updateEventFired).to.be.false;
195
- });
196
-
197
- it('handles invalid date inputs', async () => {
198
- const el = await createCustomElementInHTMLContainer();
199
-
200
- /* -------------------------- minimum (left) slider ------------------------- */
201
- const minDateInput = el.shadowRoot?.querySelector(
202
- '#date-min'
203
- ) as HTMLInputElement;
204
-
205
- minDateInput.value = '5/17/1961';
206
- minDateInput.dispatchEvent(new Event('blur'));
207
- await el.updateComplete;
208
-
209
- expect(Math.floor(el.minSliderX)).to.eq(101);
210
- expect(minDateInput.value).to.eq('5/17/1961');
211
-
212
- // enter invalid value
213
- minDateInput.value = 'invalid';
214
- minDateInput.dispatchEvent(new Event('blur'));
215
- await el.updateComplete;
216
-
217
- expect(Math.floor(el.minSliderX)).to.eq(101); // does not move
218
- expect(minDateInput.value).to.eq('5/17/1961'); // resets back to previous date
219
-
220
- /* -------------------------- maximum (right) slider ------------------------- */
221
- const maxDateInput = el.shadowRoot?.querySelector(
222
- '#date-max'
223
- ) as HTMLInputElement;
224
-
225
- // initial values
226
- expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH);
227
- expect(maxDateInput.value).to.eq('12/4/2020');
228
-
229
- // enter invalid value
230
- maxDateInput.value = 'Abc 12, 1YYY';
231
- maxDateInput.dispatchEvent(new Event('blur'));
232
- await el.updateComplete;
233
-
234
- expect(Math.floor(el.maxSliderX)).to.eq(WIDTH - SLIDER_WIDTH); // does not move
235
- expect(maxDateInput.value).to.eq('12/4/2020'); // resets back to previous date
236
- });
237
-
238
- it('updates the date inputs when the sliders are moved', async () => {
239
- const el = await createCustomElementInHTMLContainer();
240
-
241
- /* -------------------------- minimum (left) slider ------------------------- */
242
- const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
243
- const container = el.shadowRoot?.querySelector(
244
- '#container'
245
- ) as HTMLDivElement;
246
- const minDateInput = el.shadowRoot?.querySelector(
247
- '#date-min'
248
- ) as HTMLInputElement;
249
-
250
- // initial state
251
- expect(minSlider.getBoundingClientRect().x).to.eq(108);
252
- expect(minSlider.classList.contains('draggable')).to.be.true;
253
-
254
- // pointer down
255
- minSlider.dispatchEvent(new PointerEvent('pointerdown'));
256
- await el.updateComplete;
257
-
258
- // cursor changes to 'grab'
259
- const classList = minSlider.classList;
260
- expect(classList.contains('draggable')).to.be.true;
261
- expect(classList.contains('dragging')).to.be.true;
262
-
263
- // slide to right
264
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 60 }));
265
- await el.updateComplete;
266
-
267
- // slider has moved
268
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(168);
269
- // min date is updated
270
- expect(minDateInput.value).to.eq('4/23/1940');
271
-
272
- // stop dragging
273
- window.dispatchEvent(new PointerEvent('pointerup'));
274
- await el.updateComplete;
275
-
276
- // cursor returns to normal
277
- expect(Array.from(container.classList)).not.to.include('dragging');
278
-
279
- /* -------------------------- maximum (right) slider ------------------------- */
280
- const maxSlider = el.shadowRoot?.querySelector('#slider-max') as SVGElement;
281
- const maxDateInput = el.shadowRoot?.querySelector(
282
- '#date-max'
283
- ) as HTMLInputElement;
284
-
285
- // initial state
286
- expect(maxSlider.getBoundingClientRect().x).to.eq(298);
287
-
288
- // slide to left
289
- maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 195 }));
290
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 165 }));
291
- await el.updateComplete;
292
-
293
- // slider has moved
294
- expect(Math.round(maxSlider.getBoundingClientRect().x)).to.eq(268);
295
- // max date is updated
296
- expect(maxDateInput.value).to.eq('10/8/2000');
297
- await el.updateComplete;
298
-
299
- // try to slide min slider past max slider
300
- minSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 62 }));
301
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 190 }));
302
- await el.updateComplete;
303
-
304
- // slider moves all the way to meet the right slider
305
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(258);
306
-
307
- // try to slide max slider past min slider
308
- maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 120 }));
309
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 50 }));
310
- await el.updateComplete;
311
- expect(Math.round(maxSlider.getBoundingClientRect().x)).to.eq(268); // max slider didn't move
312
-
313
- // try to slide max slider off the right edge
314
- maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 120 }));
315
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 300 }));
316
- await el.updateComplete;
317
- expect(maxSlider.getBoundingClientRect().x).to.eq(298); // back to its initial position
318
- expect(el.maxSelectedDate).to.equal('12/4/2020');
319
- });
320
-
321
- it('does not permit sliders to select dates outside the allowed range', async () => {
322
- const el = await createCustomElementInHTMLContainer();
323
- el.binSnapping = 'month';
324
- el.dateFormat = 'YYYY-MM';
325
- el.minDate = '2020-01';
326
- el.maxDate = '2021-12';
327
- el.minSelectedDate = '2020-01';
328
- el.maxSelectedDate = '2021-12';
329
- el.bins = [10, 20, 30, 40, 50, 60, 70, 80];
330
- await el.updateComplete;
331
-
332
- const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
333
- const maxSlider = el.shadowRoot?.querySelector('#slider-max') as SVGElement;
334
-
335
- const minDateInput = el.shadowRoot?.querySelector(
336
- '#date-min'
337
- ) as HTMLInputElement;
338
- const maxDateInput = el.shadowRoot?.querySelector(
339
- '#date-max'
340
- ) as HTMLInputElement;
341
-
342
- // initial state
343
- expect(minSlider.getBoundingClientRect().x).to.eq(108, 'initial');
344
- expect(maxSlider.getBoundingClientRect().x).to.eq(298, 'initial');
345
- expect(minDateInput.value).to.eq('2020-01', 'initial');
346
- expect(maxDateInput.value).to.eq('2021-12', 'initial');
347
-
348
- // try dragging the min slider too far to the left
349
- minSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 0 }));
350
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: -50 }));
351
- await el.updateComplete;
352
- expect(minSlider.getBoundingClientRect().x).to.eq(108); // slider didn't move
353
- expect(minDateInput.value).to.eq('2020-01'); // value unchanged
354
-
355
- // try dragging the max slider too far to the right
356
- maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 195 }));
357
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 250 }));
358
- await el.updateComplete;
359
- expect(maxSlider.getBoundingClientRect().x).to.eq(298); // slider didn't move
360
- expect(maxDateInput.value).to.eq('2021-12'); // value unchanged
361
- });
362
-
363
- it("emits a custom event when the element's date range changes", async () => {
364
- const el = await createCustomElementInHTMLContainer();
365
- el.updateDelay = 30; // set debounce delay of 30ms
366
-
367
- const minDateInput = el.shadowRoot?.querySelector(
368
- '#date-min'
369
- ) as HTMLInputElement;
370
- const updateEventPromise = oneEvent(el, 'histogramDateRangeUpdated');
371
-
372
- // simulate typing a new value into input
373
- minDateInput.value = '1955';
374
- minDateInput.dispatchEvent(new Event('blur'));
375
-
376
- // will wait longer than debounce delay
377
- const { detail } = await updateEventPromise;
378
- // verify that event is emitted
379
- expect(detail.minDate).to.equal('1/1/1955');
380
- expect(detail.maxDate).to.equal('12/4/2020');
381
-
382
- let eventCount = 0;
383
- el.addEventListener('histogramDateRangeUpdated', () => (eventCount += 1));
384
-
385
- // events are not sent if no change since the last event that was sent
386
- minDateInput.value = '1955';
387
- minDateInput.dispatchEvent(new Event('blur'));
388
- await aTimeout(60); // wait longer than debounce delay
389
- expect(eventCount).to.equal(0);
390
-
391
- const updateEventPromise2 = oneEvent(el, 'histogramDateRangeUpdated');
392
-
393
- // with the debounce, multiple quick changes only result in one event sent
394
- minDateInput.value = '1965';
395
- minDateInput.dispatchEvent(new Event('blur'));
396
- await aTimeout(10); // wait less than the debounce delay
397
-
398
- minDateInput.dispatchEvent(new Event('focus'));
399
- minDateInput.value = '1975';
400
- minDateInput.dispatchEvent(new Event('blur'));
401
- await aTimeout(10);
402
-
403
- minDateInput.dispatchEvent(new Event('focus'));
404
- minDateInput.value = '1985';
405
- minDateInput.dispatchEvent(new Event('blur'));
406
- await aTimeout(10);
407
-
408
- const event2 = await updateEventPromise2;
409
- expect(event2.detail.minDate).to.equal('1/1/1985');
410
- expect(eventCount).to.equal(1); // only one event was fired
411
- });
412
-
413
- it('shows/hides tooltip when hovering over (or pointing at) a bar', async () => {
414
- const el = await createCustomElementInHTMLContainer();
415
- // include a number which will require commas (1,000,000)
416
- el.bins = [1000000, 1, 100];
417
- await aTimeout(10);
418
- const bars = el.shadowRoot?.querySelectorAll(
419
- '.bar'
420
- ) as unknown as SVGRectElement[];
421
- const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
422
- expect(tooltip.innerText).to.eq('');
423
-
424
- // hover
425
- bars[0].dispatchEvent(new PointerEvent('pointerenter'));
426
- await el.updateComplete;
427
- expect(tooltip.innerText).to.match(
428
- /^1,000,000 items\n1\/1\/1900 - 4\/23\/1940/
429
- );
430
- expect(getComputedStyle(tooltip).display).to.eq('block');
431
-
432
- // leave
433
- bars[0].dispatchEvent(new PointerEvent('pointerleave'));
434
- await el.updateComplete;
435
- expect(getComputedStyle(tooltip).display).to.eq('none');
436
- expect(tooltip.innerText).to.eq('');
437
-
438
- // ensure singular item is not pluralized
439
- bars[1].dispatchEvent(new PointerEvent('pointerenter'));
440
- await el.updateComplete;
441
- expect(tooltip.innerText).to.match(/^1 item\n4\/23\/1940 - 8\/13\/1980/);
442
- });
443
-
444
- it('does not show tooltip while dragging', async () => {
445
- const el = await createCustomElementInHTMLContainer();
446
- const bars = el.shadowRoot?.querySelectorAll(
447
- '.bar'
448
- ) as unknown as SVGRectElement[];
449
- const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
450
- expect(tooltip.innerText).to.eq('');
451
- const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
452
-
453
- // pointer down and slide right
454
- minSlider.dispatchEvent(new PointerEvent('pointerdown'));
455
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 100 }));
456
- await el.updateComplete;
457
-
458
- // hover over bar
459
- bars[0].dispatchEvent(new PointerEvent('pointerenter'));
460
- await el.updateComplete;
461
- // tooltip display is suppressed while dragging
462
- expect(tooltip.style.display).to.eq('');
463
- });
464
-
465
- it('passes the a11y audit', async () => {
466
- await fixture<HistogramDateRange>(subject).then(el =>
467
- expect(el).shadowDom.to.be.accessible()
468
- );
469
- });
470
-
471
- it('allows range to be pre-selected', async () => {
472
- const el = await fixture<HistogramDateRange>(
473
- html`
474
- <histogram-date-range
475
- minDate="1900"
476
- maxDate="Dec 4, 2020"
477
- minSelectedDate="2012"
478
- maxSelectedDate="2019"
479
- bins="[33, 1, 100]"
480
- >
481
- </histogram-date-range>
482
- `
483
- );
484
- const minDateInput = el.shadowRoot?.querySelector(
485
- '#date-min'
486
- ) as HTMLInputElement;
487
- expect(minDateInput.value).to.eq('2012');
488
-
489
- const maxDateInput = el.shadowRoot?.querySelector(
490
- '#date-max'
491
- ) as HTMLInputElement;
492
- expect(maxDateInput.value).to.eq('2019');
493
- });
494
-
495
- it('extends the selected range when the histogram is clicked outside of the current range', async () => {
496
- const el = await fixture<HistogramDateRange>(
497
- html`
498
- <histogram-date-range
499
- minDate="1900"
500
- maxDate="2020"
501
- minSelectedDate="1950"
502
- maxSelectedDate="1955"
503
- bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
504
- >
505
- </histogram-date-range>
506
- `
507
- );
508
-
509
- const leftBarToClick = Array.from(
510
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
511
- )[1]; // click on second bar to the left
512
-
513
- leftBarToClick.dispatchEvent(new Event('click'));
514
- await el.updateComplete;
515
- expect(el.minSelectedDate).to.eq('1910'); // range was extended to left
516
-
517
- const rightBarToClick = Array.from(
518
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
519
- )[8]; // click on second bar from the right
520
-
521
- rightBarToClick.dispatchEvent(new Event('click'));
522
- expect(el.maxSelectedDate).to.eq('1998'); // range was extended to right
523
- });
524
-
525
- it('narrows the selected range when the histogram is clicked inside of the current range', async () => {
526
- const el = await fixture<HistogramDateRange>(
527
- html`
528
- <histogram-date-range
529
- minDate="1900"
530
- maxDate="2020"
531
- minSelectedDate="1900"
532
- maxSelectedDate="2020"
533
- bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
534
- >
535
- </histogram-date-range>
536
- `
537
- );
538
-
539
- ///////////////////////////////////////////////
540
- // NB: the slider nearest the clicked bar moves
541
- ///////////////////////////////////////////////
542
-
543
- const leftBarToClick = Array.from(
544
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
545
- )[3]; // click on fourth bar to the left
546
-
547
- leftBarToClick.dispatchEvent(new Event('click'));
548
- expect(el.minSelectedDate).to.eq('1932'); // range was extended to the right
549
-
550
- const rightBarToClick = Array.from(
551
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
552
- )[8]; // click on second bar from the right
553
-
554
- rightBarToClick.dispatchEvent(new Event('click'));
555
- expect(el.maxSelectedDate).to.eq('1998'); // range was extended to the left
556
- });
557
-
558
- it('handles invalid pre-selected range by defaulting to overall max and min', async () => {
559
- const el = await fixture<HistogramDateRange>(
560
- html`
561
- <histogram-date-range
562
- minDate="1900"
563
- maxDate="2020"
564
- minSelectedDate="2000xyz"
565
- maxSelectedDate="5000"
566
- bins="[33, 1, 100]"
567
- >
568
- </histogram-date-range>
569
- `
570
- );
571
- const minDateInput = el.shadowRoot?.querySelector(
572
- '#date-min'
573
- ) as HTMLInputElement;
574
- // malformed min date defaults to overall min
575
- expect(minDateInput.value).to.eq('1900');
576
-
577
- const maxDateInput = el.shadowRoot?.querySelector(
578
- '#date-max'
579
- ) as HTMLInputElement;
580
- // well-formed max date is allowed
581
- expect(maxDateInput.value).to.eq('5000');
582
- });
583
-
584
- it('handles year values less than 1000 correctly', async () => {
585
- const el = await fixture<HistogramDateRange>(
586
- html`
587
- <histogram-date-range
588
- dateFormat="M/D/YYYY"
589
- minDate="-2000"
590
- maxDate="2000"
591
- minSelectedDate="-500"
592
- maxSelectedDate="500"
593
- bins="[33, 1, 100]"
594
- >
595
- </histogram-date-range>
596
- `
597
- );
598
- const minDateInput = el.shadowRoot?.querySelector(
599
- '#date-min'
600
- ) as HTMLInputElement;
601
- expect(minDateInput.value).to.eq('1/1/-500');
602
-
603
- const maxDateInput = el.shadowRoot?.querySelector(
604
- '#date-max'
605
- ) as HTMLInputElement;
606
- expect(maxDateInput.value).to.eq('1/1/500');
607
- });
608
-
609
- it('handles missing data', async () => {
610
- let el = await fixture<HistogramDateRange>(
611
- html`<histogram-date-range>
612
- minDate="1900" maxDate="2020" bins=""
613
- </histogram-date-range>`
614
- );
615
- expect(el.shadowRoot?.innerHTML).to.contain('no data');
616
- el = await fixture<HistogramDateRange>(
617
- html`<histogram-date-range
618
- minDate="1900"
619
- maxDate="2020"
620
- bins="[]"
621
- missingDataMessage="no data available"
622
- ></histogram-date-range>`
623
- );
624
- expect(el.shadowRoot?.innerHTML).to.contain('no data available');
625
- });
626
-
627
- it('correctly displays data consisting of a single bin', async () => {
628
- const el = await fixture<HistogramDateRange>(
629
- html`
630
- <histogram-date-range minDate="2020" maxDate="2020" bins="[50]">
631
- </histogram-date-range>
632
- `
633
- );
634
- const bars = el.shadowRoot?.querySelectorAll(
635
- '.bar'
636
- ) as unknown as SVGRectElement[];
637
- const heights = Array.from(bars).map(b => b.height.baseVal.value);
638
- expect(heights).to.eql([157]);
639
- });
640
-
641
- it('correctly displays small diff between max and min values', async () => {
642
- const el = await fixture<HistogramDateRange>(
643
- html`
644
- <histogram-date-range bins="[1519,2643,1880,2041,1638,1441]">
645
- </histogram-date-range>
646
- `
647
- );
648
- const bars = el.shadowRoot?.querySelectorAll(
649
- '.bar'
650
- ) as unknown as SVGRectElement[];
651
- const heights = Array.from(bars).map(b => b.height.baseVal.value);
652
- expect(heights).to.eql([37, 40, 38, 38, 37, 36]);
653
- });
654
-
655
- it('correctly aligns bins to exact month boundaries when binSnapping=month', async () => {
656
- const el = await fixture<HistogramDateRange>(
657
- html`
658
- <histogram-date-range
659
- binSnapping="month"
660
- dateFormat="YYYY-MM"
661
- tooltipDateFormat="MMM YYYY"
662
- minDate="2020-01"
663
- maxDate="2021-12"
664
- bins="[10,20,30,40,50,60,70,80]"
665
- ></histogram-date-range>
666
- `
667
- );
668
- const bars = el.shadowRoot?.querySelectorAll(
669
- '.bar'
670
- ) as unknown as SVGRectElement[];
671
- const tooltips = Array.from(bars).map(b => b.dataset.tooltip);
672
- expect(tooltips).to.eql([
673
- 'Jan 2020 - Mar 2020',
674
- 'Apr 2020 - Jun 2020',
675
- 'Jul 2020 - Sep 2020',
676
- 'Oct 2020 - Dec 2020',
677
- 'Jan 2021 - Mar 2021',
678
- 'Apr 2021 - Jun 2021',
679
- 'Jul 2021 - Sep 2021',
680
- 'Oct 2021 - Dec 2021',
681
- ]);
682
- });
683
-
684
- it('correctly handles month snapping for years 0-99', async () => {
685
- const el = await fixture<HistogramDateRange>(
686
- html`
687
- <histogram-date-range
688
- binSnapping="month"
689
- dateFormat="YYYY-MM"
690
- tooltipDateFormat="MMM YYYY"
691
- minDate="0050-01"
692
- maxDate="0065-12"
693
- bins="[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]"
694
- ></histogram-date-range>
695
- `
696
- );
697
-
698
- const bars = el.shadowRoot?.querySelectorAll(
699
- '.bar'
700
- ) as unknown as SVGRectElement[];
701
- const tooltips = Array.from(bars).map(b => b.dataset.tooltip);
702
- expect(tooltips).to.eql([
703
- 'Jan 50 - Jun 50',
704
- 'Jul 50 - Dec 50',
705
- 'Jan 51 - Jun 51',
706
- 'Jul 51 - Dec 51',
707
- 'Jan 52 - Jun 52',
708
- 'Jul 52 - Dec 52',
709
- 'Jan 53 - Jun 53',
710
- 'Jul 53 - Dec 53',
711
- 'Jan 54 - Jun 54',
712
- 'Jul 54 - Dec 54',
713
- 'Jan 55 - Jun 55',
714
- 'Jul 55 - Dec 55',
715
- 'Jan 56 - Jun 56',
716
- 'Jul 56 - Dec 56',
717
- 'Jan 57 - Jun 57',
718
- 'Jul 57 - Dec 57',
719
- 'Jan 58 - Jun 58',
720
- 'Jul 58 - Dec 58',
721
- 'Jan 59 - Jun 59',
722
- 'Jul 59 - Dec 59',
723
- 'Jan 60 - Jun 60',
724
- 'Jul 60 - Dec 60',
725
- 'Jan 61 - Jun 61',
726
- 'Jul 61 - Dec 61',
727
- 'Jan 62 - Jun 62',
728
- 'Jul 62 - Dec 62',
729
- 'Jan 63 - Jun 63',
730
- 'Jul 63 - Dec 63',
731
- 'Jan 64 - Jun 64',
732
- 'Jul 64 - Dec 64',
733
- 'Jan 65 - Jun 65',
734
- 'Jul 65 - Dec 65',
735
- ]);
736
- });
737
-
738
- it('correctly aligns bins to exact year boundaries when binSnapping=year', async () => {
739
- const el = await fixture<HistogramDateRange>(
740
- html`
741
- <histogram-date-range
742
- binSnapping="year"
743
- minDate="2000"
744
- maxDate="2019"
745
- bins="[10,20,30,40,50,60,70,80,90,100]"
746
- ></histogram-date-range>
747
- `
748
- );
749
- const bars = el.shadowRoot?.querySelectorAll(
750
- '.bar'
751
- ) as unknown as SVGRectElement[];
752
- const tooltips = Array.from(bars).map(b => b.dataset.tooltip);
753
- expect(tooltips).to.eql([
754
- '2000 - 2001',
755
- '2002 - 2003',
756
- '2004 - 2005',
757
- '2006 - 2007',
758
- '2008 - 2009',
759
- '2010 - 2011',
760
- '2012 - 2013',
761
- '2014 - 2015',
762
- '2016 - 2017',
763
- '2018 - 2019',
764
- ]);
765
- });
766
-
767
- it('correctly handles year snapping for years 0-99', async () => {
768
- const el = await fixture<HistogramDateRange>(
769
- html`
770
- <histogram-date-range
771
- binSnapping="year"
772
- dateFormat="YYYY"
773
- minDate="0020"
774
- maxDate="0025"
775
- bins="[1,2,3,4,5,6]"
776
- ></histogram-date-range>
777
- `
778
- );
779
-
780
- const bars = el.shadowRoot?.querySelectorAll(
781
- '.bar'
782
- ) as unknown as SVGRectElement[];
783
- const tooltips = Array.from(bars).map(b => b.dataset.tooltip);
784
- expect(tooltips).to.eql(['20', '21', '22', '23', '24', '25']);
785
- });
786
-
787
- it('does not duplicate start/end date in tooltips when representing a single year', async () => {
788
- const el = await fixture<HistogramDateRange>(
789
- html`
790
- <histogram-date-range
791
- binSnapping="year"
792
- dateFormat="YYYY"
793
- minDate="2001"
794
- maxDate="2005"
795
- bins="[10,20,30,40,50]"
796
- ></histogram-date-range>
797
- `
798
- );
799
- const bars = el.shadowRoot?.querySelectorAll(
800
- '.bar'
801
- ) as unknown as SVGRectElement[];
802
- const tooltips = Array.from(bars).map(b => b.dataset.tooltip);
803
- expect(tooltips).to.eql(['2001', '2002', '2003', '2004', '2005']);
804
- });
805
-
806
- it('falls back to default date format for tooltips if no tooltip date format provided', async () => {
807
- const el = await fixture<HistogramDateRange>(
808
- html`
809
- <histogram-date-range
810
- binSnapping="year"
811
- minDate="2001"
812
- maxDate="2005"
813
- bins="[10,20,30,40,50]"
814
- ></histogram-date-range>
815
- `
816
- );
817
-
818
- const bars = el.shadowRoot?.querySelectorAll(
819
- '.bar'
820
- ) as unknown as SVGRectElement[];
821
- let tooltips = Array.from(bars).map(b => b.dataset.tooltip);
822
- expect(tooltips).to.eql(['2001', '2002', '2003', '2004', '2005']); // default YYYY date format
823
-
824
- el.dateFormat = 'YYYY/MM';
825
- el.minDate = '2001/01';
826
- el.maxDate = '2005/01';
827
- await el.updateComplete;
828
-
829
- // Should use dateFormat fallback for tooltips
830
- tooltips = Array.from(bars).map(b => b.dataset.tooltip);
831
- expect(tooltips).to.eql([
832
- '2001/01 - 2001/12',
833
- '2002/01 - 2002/12',
834
- '2003/01 - 2003/12',
835
- '2004/01 - 2004/12',
836
- '2005/01 - 2005/12',
837
- ]);
838
-
839
- el.dateFormat = 'YYYY';
840
- el.tooltipDateFormat = 'MMMM YYYY';
841
- el.minDate = '2001';
842
- el.maxDate = '2005';
843
- await el.updateComplete;
844
-
845
- // Should use defined tooltipDateFormat for tooltips
846
- tooltips = Array.from(bars).map(b => b.dataset.tooltip);
847
- expect(tooltips).to.eql([
848
- 'January 2001 - December 2001',
849
- 'January 2002 - December 2002',
850
- 'January 2003 - December 2003',
851
- 'January 2004 - December 2004',
852
- 'January 2005 - December 2005',
853
- ]);
854
- });
855
-
856
- it('has a disabled state', async () => {
857
- const el = await fixture<HistogramDateRange>(
858
- html`
859
- <histogram-date-range
860
- minDate="1900"
861
- maxDate="2020"
862
- disabled
863
- bins="[33, 1, 100]"
864
- >
865
- </histogram-date-range>
866
- `
867
- );
868
- expect(
869
- el.shadowRoot
870
- ?.querySelector('.inner-container')
871
- ?.classList.contains('disabled')
872
- ).to.eq(true);
873
-
874
- const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
875
-
876
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8); // initial state
877
-
878
- // attempt to slide to right
879
- minSlider.dispatchEvent(new PointerEvent('pointerdown'));
880
- await el.updateComplete;
881
-
882
- // cursor is not draggable if disabled
883
- expect(minSlider.classList.contains('draggable')).to.be.false;
884
-
885
- // attempt to slide to right
886
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 70 }));
887
- await el.updateComplete;
888
-
889
- // slider does not moved if element disabled
890
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8);
891
- });
892
-
893
- it('has a loading state with an activity indicator', async () => {
894
- const el = await fixture<HistogramDateRange>(
895
- html`
896
- <histogram-date-range
897
- minDate="1900"
898
- maxDate="2020"
899
- loading
900
- bins="[33, 1, 100]"
901
- >
902
- </histogram-date-range>
903
- `
904
- );
905
- expect(
906
- el.shadowRoot
907
- ?.querySelector('ia-activity-indicator')
908
- ?.attributes?.getNamedItem('mode')?.value
909
- ).to.eq('processing');
910
- });
911
-
912
- it('can use LitElement bound properties', async () => {
913
- const el = await fixture<HistogramDateRange>(
914
- html`
915
- <histogram-date-range
916
- .minDate=${1900}
917
- .maxDate=${'Dec 4, 2020'}
918
- .minSelectedDate=${2012}
919
- .maxSelectedDate=${2019}
920
- .bins=${[33, 1, 100]}
921
- >
922
- </histogram-date-range>
923
- `
924
- );
925
- const minDateInput = el.shadowRoot?.querySelector(
926
- '#date-min'
927
- ) as HTMLInputElement;
928
- expect(minDateInput.value).to.eq('2012');
929
-
930
- const maxDateInput = el.shadowRoot?.querySelector(
931
- '#date-max'
932
- ) as HTMLInputElement;
933
- expect(maxDateInput.value).to.eq('2019');
934
- });
935
- });
1
+ /* eslint-disable @typescript-eslint/no-unused-expressions */
2
+ import { html, fixture, expect, oneEvent, aTimeout } from '@open-wc/testing';
3
+
4
+ import { HistogramDateRange } from '../src/histogram-date-range';
5
+ import '../src/histogram-date-range';
6
+
7
+ const SLIDER_WIDTH = 10;
8
+ const WIDTH = 200;
9
+
10
+ const subject = html`
11
+ <histogram-date-range
12
+ width="${WIDTH}"
13
+ tooltipWidth="140"
14
+ height="50"
15
+ dateFormat="M/D/YYYY"
16
+ minDate="1900"
17
+ maxDate="12/4/2020"
18
+ bins="[33, 1, 100]"
19
+ >
20
+ </histogram-date-range>
21
+ `;
22
+
23
+ async function createCustomElementInHTMLContainer(): Promise<HistogramDateRange> {
24
+ document.head.insertAdjacentHTML(
25
+ 'beforeend',
26
+ `<style>
27
+ html {
28
+ font-size:10px;
29
+ }
30
+ .container {
31
+ width: 400px;
32
+ height: 400px;
33
+ display: flex;
34
+ background: #FFF6E1;
35
+ justify-content: center;
36
+ align-items: center;
37
+ }
38
+ </style>`
39
+ );
40
+ // https://open-wc.org/docs/testing/helpers/#customize-the-fixture-container
41
+ const parentNode = document.createElement('div');
42
+ parentNode.setAttribute('class', 'container');
43
+ return fixture<HistogramDateRange>(subject, { parentNode });
44
+ }
45
+
46
+ describe('HistogramDateRange', () => {
47
+ it('shows scaled histogram bars when provided with data', async () => {
48
+ const el = await createCustomElementInHTMLContainer();
49
+ const bars = el.shadowRoot?.querySelectorAll(
50
+ '.bar'
51
+ ) as unknown as SVGRectElement[];
52
+ const heights = Array.from(bars, b => b.height.baseVal.value);
53
+
54
+ expect(heights).to.eql([38, 7, 50]);
55
+ });
56
+
57
+ it('changes the position of the sliders and standardizes date format when dates are entered', async () => {
58
+ const el = await createCustomElementInHTMLContainer();
59
+
60
+ /* -------------------------- minimum (left) slider ------------------------- */
61
+ expect(el.minSliderX).to.eq(SLIDER_WIDTH);
62
+ const minDateInput = el.shadowRoot?.querySelector(
63
+ '#date-min'
64
+ ) as HTMLInputElement;
65
+
66
+ const pressEnterEvent = new KeyboardEvent('keyup', {
67
+ key: 'Enter',
68
+ });
69
+
70
+ // valid min date
71
+ minDateInput.value = '1950';
72
+ minDateInput.dispatchEvent(pressEnterEvent);
73
+
74
+ expect(Math.floor(el.minSliderX)).to.eq(84);
75
+ expect(el.minSelectedDate).to.eq('1/1/1950'); // set to correct format
76
+
77
+ // attempt to set date earlier than first item
78
+ minDateInput.value = '10/1/1850';
79
+ minDateInput.dispatchEvent(new Event('blur'));
80
+
81
+ expect(Math.floor(el.minSliderX)).to.eq(SLIDER_WIDTH); // leftmost valid position
82
+ // allow date value less than slider range
83
+ expect(el.minSelectedDate).to.eq('10/1/1850');
84
+
85
+ /* -------------------------- maximum (right) slider ------------------------- */
86
+ expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH);
87
+ const maxDateInput = el.shadowRoot?.querySelector(
88
+ '#date-max'
89
+ ) as HTMLInputElement;
90
+
91
+ // set valid max date
92
+ maxDateInput.value = '3/12/1975';
93
+ maxDateInput.dispatchEvent(pressEnterEvent);
94
+
95
+ expect(Math.floor(el.maxSliderX)).to.eq(121);
96
+ expect(maxDateInput.value).to.eq('3/12/1975');
97
+
98
+ // attempt to set date later than last item
99
+ maxDateInput.value = '12/31/2199';
100
+ maxDateInput.dispatchEvent(new Event('blur'));
101
+ await el.updateComplete;
102
+
103
+ expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH); // rightmost valid position
104
+ // allow date value greater than slider range
105
+ expect(maxDateInput.value).to.eq('12/31/2199');
106
+ });
107
+
108
+ it('when updateWhileFocused option is true, updates are fired upon changing input focus', async () => {
109
+ const el = await createCustomElementInHTMLContainer();
110
+ el.updateWhileFocused = true;
111
+ await el.updateComplete;
112
+
113
+ let updateEventFired = false;
114
+ el.addEventListener(
115
+ 'histogramDateRangeUpdated',
116
+ () => (updateEventFired = true)
117
+ );
118
+
119
+ /* -------------------------- minimum (left) slider ------------------------- */
120
+ const minDateInput = el.shadowRoot?.querySelector(
121
+ '#date-min'
122
+ ) as HTMLInputElement;
123
+
124
+ /* -------------------------- maximum (right) slider ------------------------- */
125
+ const maxDateInput = el.shadowRoot?.querySelector(
126
+ '#date-max'
127
+ ) as HTMLInputElement;
128
+
129
+ minDateInput.focus();
130
+
131
+ // set valid min date, but don't hit Enter -- just switch focus to the max date input
132
+ minDateInput.value = '1950';
133
+ maxDateInput.focus();
134
+ await el.updateComplete;
135
+ await aTimeout(0);
136
+
137
+ // update event should have fired, setting the minSelectedDate prop & slider position accordingly
138
+ expect(updateEventFired).to.be.true;
139
+ expect(Math.floor(el.minSliderX)).to.eq(84);
140
+ expect(el.minSelectedDate).to.eq('1/1/1950');
141
+
142
+ updateEventFired = false;
143
+ await el.updateComplete;
144
+
145
+ // set valid max date, but don't hit Enter -- just switch focus to the min date input
146
+ maxDateInput.value = '3/12/1975';
147
+ minDateInput.focus();
148
+ await el.updateComplete;
149
+ await aTimeout(0);
150
+
151
+ // update event should have fired, setting the maxSelectedDate prop & slider position accordingly
152
+ expect(updateEventFired).to.be.true;
153
+ expect(Math.floor(el.maxSliderX)).to.eq(121);
154
+ expect(el.maxSelectedDate).to.eq('3/12/1975');
155
+ });
156
+
157
+ it('when updateWhileFocused option is false (default), updates are not fired while one of the inputs remains focused', async () => {
158
+ const el = await createCustomElementInHTMLContainer();
159
+
160
+ let updateEventFired = false;
161
+ el.addEventListener(
162
+ 'histogramDateRangeUpdated',
163
+ () => (updateEventFired = true)
164
+ );
165
+
166
+ /* -------------------------- minimum (left) slider ------------------------- */
167
+ const minDateInput = el.shadowRoot?.querySelector(
168
+ '#date-min'
169
+ ) as HTMLInputElement;
170
+
171
+ /* -------------------------- maximum (right) slider ------------------------- */
172
+ const maxDateInput = el.shadowRoot?.querySelector(
173
+ '#date-max'
174
+ ) as HTMLInputElement;
175
+
176
+ minDateInput.focus();
177
+
178
+ // set valid min date, but don't hit Enter -- just switch focus to the max date input
179
+ minDateInput.value = '1950';
180
+ maxDateInput.focus();
181
+ await el.updateComplete;
182
+ await aTimeout(0);
183
+
184
+ // update event should NOT have fired, because focus remains within the inputs
185
+ expect(updateEventFired).to.be.false;
186
+
187
+ // set valid max date, but don't hit Enter -- just switch focus to the min date input
188
+ maxDateInput.value = '3/12/1975';
189
+ minDateInput.focus();
190
+ await el.updateComplete;
191
+ await aTimeout(0);
192
+
193
+ // update event should NOT have fired, because focus remains within the inputs
194
+ expect(updateEventFired).to.be.false;
195
+ });
196
+
197
+ it('handles invalid date inputs', async () => {
198
+ const el = await createCustomElementInHTMLContainer();
199
+
200
+ /* -------------------------- minimum (left) slider ------------------------- */
201
+ const minDateInput = el.shadowRoot?.querySelector(
202
+ '#date-min'
203
+ ) as HTMLInputElement;
204
+
205
+ minDateInput.value = '5/17/1961';
206
+ minDateInput.dispatchEvent(new Event('blur'));
207
+ await el.updateComplete;
208
+
209
+ expect(Math.floor(el.minSliderX)).to.eq(101);
210
+ expect(minDateInput.value).to.eq('5/17/1961');
211
+
212
+ // enter invalid value
213
+ minDateInput.value = 'invalid';
214
+ minDateInput.dispatchEvent(new Event('blur'));
215
+ await el.updateComplete;
216
+
217
+ expect(Math.floor(el.minSliderX)).to.eq(101); // does not move
218
+ expect(minDateInput.value).to.eq('5/17/1961'); // resets back to previous date
219
+
220
+ /* -------------------------- maximum (right) slider ------------------------- */
221
+ const maxDateInput = el.shadowRoot?.querySelector(
222
+ '#date-max'
223
+ ) as HTMLInputElement;
224
+
225
+ // initial values
226
+ expect(el.maxSliderX).to.eq(WIDTH - SLIDER_WIDTH);
227
+ expect(maxDateInput.value).to.eq('12/4/2020');
228
+
229
+ // enter invalid value
230
+ maxDateInput.value = 'Abc 12, 1YYY';
231
+ maxDateInput.dispatchEvent(new Event('blur'));
232
+ await el.updateComplete;
233
+
234
+ expect(Math.floor(el.maxSliderX)).to.eq(WIDTH - SLIDER_WIDTH); // does not move
235
+ expect(maxDateInput.value).to.eq('12/4/2020'); // resets back to previous date
236
+ });
237
+
238
+ it('updates the date inputs when the sliders are moved', async () => {
239
+ const el = await createCustomElementInHTMLContainer();
240
+
241
+ /* -------------------------- minimum (left) slider ------------------------- */
242
+ const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
243
+ const container = el.shadowRoot?.querySelector(
244
+ '#container'
245
+ ) as HTMLDivElement;
246
+ const minDateInput = el.shadowRoot?.querySelector(
247
+ '#date-min'
248
+ ) as HTMLInputElement;
249
+
250
+ // initial state
251
+ expect(minSlider.getBoundingClientRect().x).to.eq(108);
252
+ expect(minSlider.classList.contains('draggable')).to.be.true;
253
+
254
+ // pointer down
255
+ minSlider.dispatchEvent(new PointerEvent('pointerdown'));
256
+ await el.updateComplete;
257
+
258
+ // cursor changes to 'grab'
259
+ const classList = minSlider.classList;
260
+ expect(classList.contains('draggable')).to.be.true;
261
+ expect(classList.contains('dragging')).to.be.true;
262
+
263
+ // slide to right
264
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 60 }));
265
+ await el.updateComplete;
266
+
267
+ // slider has moved
268
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(168);
269
+ // min date is updated
270
+ expect(minDateInput.value).to.eq('4/23/1940');
271
+
272
+ // stop dragging
273
+ window.dispatchEvent(new PointerEvent('pointerup'));
274
+ await el.updateComplete;
275
+
276
+ // cursor returns to normal
277
+ expect(Array.from(container.classList)).not.to.include('dragging');
278
+
279
+ /* -------------------------- maximum (right) slider ------------------------- */
280
+ const maxSlider = el.shadowRoot?.querySelector('#slider-max') as SVGElement;
281
+ const maxDateInput = el.shadowRoot?.querySelector(
282
+ '#date-max'
283
+ ) as HTMLInputElement;
284
+
285
+ // initial state
286
+ expect(maxSlider.getBoundingClientRect().x).to.eq(298);
287
+
288
+ // slide to left
289
+ maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 195 }));
290
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 165 }));
291
+ await el.updateComplete;
292
+
293
+ // slider has moved
294
+ expect(Math.round(maxSlider.getBoundingClientRect().x)).to.eq(268);
295
+ // max date is updated
296
+ expect(maxDateInput.value).to.eq('10/8/2000');
297
+ await el.updateComplete;
298
+
299
+ // try to slide min slider past max slider
300
+ minSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 62 }));
301
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 190 }));
302
+ await el.updateComplete;
303
+
304
+ // slider moves all the way to meet the right slider
305
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(258);
306
+
307
+ // try to slide max slider past min slider
308
+ maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 120 }));
309
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 50 }));
310
+ await el.updateComplete;
311
+ expect(Math.round(maxSlider.getBoundingClientRect().x)).to.eq(268); // max slider didn't move
312
+
313
+ // try to slide max slider off the right edge
314
+ maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 120 }));
315
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 300 }));
316
+ await el.updateComplete;
317
+ expect(maxSlider.getBoundingClientRect().x).to.eq(298); // back to its initial position
318
+ expect(el.maxSelectedDate).to.equal('12/4/2020');
319
+ });
320
+
321
+ it('does not permit sliders to select dates outside the allowed range', async () => {
322
+ const el = await createCustomElementInHTMLContainer();
323
+ el.binSnapping = 'month';
324
+ el.dateFormat = 'YYYY-MM';
325
+ el.minDate = '2020-01';
326
+ el.maxDate = '2021-12';
327
+ el.minSelectedDate = '2020-01';
328
+ el.maxSelectedDate = '2021-12';
329
+ el.bins = [10, 20, 30, 40, 50, 60, 70, 80];
330
+ await el.updateComplete;
331
+
332
+ const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
333
+ const maxSlider = el.shadowRoot?.querySelector('#slider-max') as SVGElement;
334
+
335
+ const minDateInput = el.shadowRoot?.querySelector(
336
+ '#date-min'
337
+ ) as HTMLInputElement;
338
+ const maxDateInput = el.shadowRoot?.querySelector(
339
+ '#date-max'
340
+ ) as HTMLInputElement;
341
+
342
+ // initial state
343
+ expect(minSlider.getBoundingClientRect().x).to.eq(108, 'initial');
344
+ expect(maxSlider.getBoundingClientRect().x).to.eq(298, 'initial');
345
+ expect(minDateInput.value).to.eq('2020-01', 'initial');
346
+ expect(maxDateInput.value).to.eq('2021-12', 'initial');
347
+
348
+ // try dragging the min slider too far to the left
349
+ minSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 0 }));
350
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: -50 }));
351
+ await el.updateComplete;
352
+ expect(minSlider.getBoundingClientRect().x).to.eq(108); // slider didn't move
353
+ expect(minDateInput.value).to.eq('2020-01'); // value unchanged
354
+
355
+ // try dragging the max slider too far to the right
356
+ maxSlider.dispatchEvent(new PointerEvent('pointerdown', { clientX: 195 }));
357
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 250 }));
358
+ await el.updateComplete;
359
+ expect(maxSlider.getBoundingClientRect().x).to.eq(298); // slider didn't move
360
+ expect(maxDateInput.value).to.eq('2021-12'); // value unchanged
361
+ });
362
+
363
+ it("emits a custom event when the element's date range changes", async () => {
364
+ const el = await createCustomElementInHTMLContainer();
365
+ el.updateDelay = 30; // set debounce delay of 30ms
366
+
367
+ const minDateInput = el.shadowRoot?.querySelector(
368
+ '#date-min'
369
+ ) as HTMLInputElement;
370
+ const updateEventPromise = oneEvent(el, 'histogramDateRangeUpdated');
371
+
372
+ // simulate typing a new value into input
373
+ minDateInput.value = '1955';
374
+ minDateInput.dispatchEvent(new Event('blur'));
375
+
376
+ // will wait longer than debounce delay
377
+ const { detail } = await updateEventPromise;
378
+ // verify that event is emitted
379
+ expect(detail.minDate).to.equal('1/1/1955');
380
+ expect(detail.maxDate).to.equal('12/4/2020');
381
+
382
+ let eventCount = 0;
383
+ el.addEventListener('histogramDateRangeUpdated', () => (eventCount += 1));
384
+
385
+ // events are not sent if no change since the last event that was sent
386
+ minDateInput.value = '1955';
387
+ minDateInput.dispatchEvent(new Event('blur'));
388
+ await aTimeout(60); // wait longer than debounce delay
389
+ expect(eventCount).to.equal(0);
390
+
391
+ const updateEventPromise2 = oneEvent(el, 'histogramDateRangeUpdated');
392
+
393
+ // with the debounce, multiple quick changes only result in one event sent
394
+ minDateInput.value = '1965';
395
+ minDateInput.dispatchEvent(new Event('blur'));
396
+ await aTimeout(10); // wait less than the debounce delay
397
+
398
+ minDateInput.dispatchEvent(new Event('focus'));
399
+ minDateInput.value = '1975';
400
+ minDateInput.dispatchEvent(new Event('blur'));
401
+ await aTimeout(10);
402
+
403
+ minDateInput.dispatchEvent(new Event('focus'));
404
+ minDateInput.value = '1985';
405
+ minDateInput.dispatchEvent(new Event('blur'));
406
+ await aTimeout(10);
407
+
408
+ const event2 = await updateEventPromise2;
409
+ expect(event2.detail.minDate).to.equal('1/1/1985');
410
+ expect(eventCount).to.equal(1); // only one event was fired
411
+ });
412
+
413
+ it('shows/hides tooltip when hovering over (or pointing at) a bar', async () => {
414
+ const el = await createCustomElementInHTMLContainer();
415
+ // include a number which will require commas (1,000,000)
416
+ el.bins = [1000000, 1, 100];
417
+ await aTimeout(10);
418
+ const bars = el.shadowRoot?.querySelectorAll(
419
+ '.bar-pointer-target'
420
+ ) as unknown as SVGRectElement[];
421
+ const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
422
+ expect(tooltip.innerText).to.eq('');
423
+
424
+ // hover
425
+ bars[0].dispatchEvent(new PointerEvent('pointerenter'));
426
+ await el.updateComplete;
427
+ expect(tooltip.innerText).to.match(
428
+ /^1,000,000 items\n1\/1\/1900 - 4\/23\/1940/
429
+ );
430
+ expect(getComputedStyle(tooltip).display).to.eq('block');
431
+
432
+ // leave
433
+ bars[0].dispatchEvent(new PointerEvent('pointerleave'));
434
+ await el.updateComplete;
435
+ expect(getComputedStyle(tooltip).display).to.eq('none');
436
+ expect(tooltip.innerText).to.eq('');
437
+
438
+ // ensure singular item is not pluralized
439
+ bars[1].dispatchEvent(new PointerEvent('pointerenter'));
440
+ await el.updateComplete;
441
+ expect(tooltip.innerText).to.match(/^1 item\n4\/23\/1940 - 8\/13\/1980/);
442
+ });
443
+
444
+ it('uses provided tooltip label', async () => {
445
+ const el = await createCustomElementInHTMLContainer();
446
+ el.bins = [1000000, 1, 100];
447
+ el.tooltipLabel = 'foobar';
448
+ await aTimeout(10);
449
+ const bars = el.shadowRoot?.querySelectorAll(
450
+ '.bar-pointer-target'
451
+ ) as unknown as SVGRectElement[];
452
+ const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
453
+ expect(tooltip.innerText).to.eq('');
454
+
455
+ // hover
456
+ bars[0].dispatchEvent(new PointerEvent('pointerenter'));
457
+ await el.updateComplete;
458
+ expect(tooltip.innerText).to.match(
459
+ /^1,000,000 foobars\n1\/1\/1900 - 4\/23\/1940/
460
+ );
461
+ expect(getComputedStyle(tooltip).display).to.eq('block');
462
+
463
+ // leave
464
+ bars[0].dispatchEvent(new PointerEvent('pointerleave'));
465
+ await el.updateComplete;
466
+ expect(getComputedStyle(tooltip).display).to.eq('none');
467
+ expect(tooltip.innerText).to.eq('');
468
+
469
+ // ensure singular item is not pluralized
470
+ bars[1].dispatchEvent(new PointerEvent('pointerenter'));
471
+ await el.updateComplete;
472
+ expect(tooltip.innerText).to.match(/^1 foobar\n4\/23\/1940 - 8\/13\/1980/);
473
+ });
474
+
475
+ it('does not show tooltip while dragging', async () => {
476
+ const el = await createCustomElementInHTMLContainer();
477
+ const bars = el.shadowRoot?.querySelectorAll(
478
+ '.bar-pointer-target'
479
+ ) as unknown as SVGRectElement[];
480
+ const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
481
+ expect(tooltip.innerText).to.eq('');
482
+ const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
483
+
484
+ // pointer down and slide right
485
+ minSlider.dispatchEvent(new PointerEvent('pointerdown'));
486
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 100 }));
487
+ await el.updateComplete;
488
+
489
+ // hover over bar
490
+ bars[0].dispatchEvent(new PointerEvent('pointerenter'));
491
+ await el.updateComplete;
492
+ // tooltip display is suppressed while dragging
493
+ expect(tooltip.style.display).to.eq('');
494
+ });
495
+
496
+ it('passes the a11y audit', async () => {
497
+ await fixture<HistogramDateRange>(subject).then(el =>
498
+ expect(el).shadowDom.to.be.accessible()
499
+ );
500
+ });
501
+
502
+ it('allows range to be pre-selected', async () => {
503
+ const el = await fixture<HistogramDateRange>(
504
+ html`
505
+ <histogram-date-range
506
+ minDate="1900"
507
+ maxDate="Dec 4, 2020"
508
+ minSelectedDate="2012"
509
+ maxSelectedDate="2019"
510
+ bins="[33, 1, 100]"
511
+ >
512
+ </histogram-date-range>
513
+ `
514
+ );
515
+ const minDateInput = el.shadowRoot?.querySelector(
516
+ '#date-min'
517
+ ) as HTMLInputElement;
518
+ expect(minDateInput.value).to.eq('2012');
519
+
520
+ const maxDateInput = el.shadowRoot?.querySelector(
521
+ '#date-max'
522
+ ) as HTMLInputElement;
523
+ expect(maxDateInput.value).to.eq('2019');
524
+ });
525
+
526
+ it('extends the selected range when the histogram is clicked outside of the current range', async () => {
527
+ const el = await fixture<HistogramDateRange>(
528
+ html`
529
+ <histogram-date-range
530
+ minDate="1900"
531
+ maxDate="2020"
532
+ minSelectedDate="1950"
533
+ maxSelectedDate="1955"
534
+ bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
535
+ >
536
+ </histogram-date-range>
537
+ `
538
+ );
539
+
540
+ const leftBarToClick = Array.from(
541
+ el.shadowRoot?.querySelectorAll('.bar-pointer-target') as NodeList
542
+ )[1]; // click on second bar to the left
543
+
544
+ leftBarToClick.dispatchEvent(new Event('click'));
545
+ await el.updateComplete;
546
+ expect(el.minSelectedDate).to.eq('1910'); // range was extended to left
547
+
548
+ const rightBarToClick = Array.from(
549
+ el.shadowRoot?.querySelectorAll('.bar-pointer-target') as NodeList
550
+ )[8]; // click on second bar from the right
551
+
552
+ rightBarToClick.dispatchEvent(new Event('click'));
553
+ expect(el.maxSelectedDate).to.eq('1998'); // range was extended to right
554
+ });
555
+
556
+ it('narrows the selected range when the histogram is clicked inside of the current range', async () => {
557
+ const el = await fixture<HistogramDateRange>(
558
+ html`
559
+ <histogram-date-range
560
+ minDate="1900"
561
+ maxDate="2020"
562
+ minSelectedDate="1900"
563
+ maxSelectedDate="2020"
564
+ bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
565
+ >
566
+ </histogram-date-range>
567
+ `
568
+ );
569
+
570
+ ///////////////////////////////////////////////
571
+ // NB: the slider nearest the clicked bar moves
572
+ ///////////////////////////////////////////////
573
+
574
+ const leftBarToClick = Array.from(
575
+ el.shadowRoot?.querySelectorAll('.bar-pointer-target') as NodeList
576
+ )[3]; // click on fourth bar to the left
577
+
578
+ leftBarToClick.dispatchEvent(new Event('click'));
579
+ expect(el.minSelectedDate).to.eq('1932'); // range was extended to the right
580
+
581
+ const rightBarToClick = Array.from(
582
+ el.shadowRoot?.querySelectorAll('.bar-pointer-target') as NodeList
583
+ )[8]; // click on second bar from the right
584
+
585
+ rightBarToClick.dispatchEvent(new Event('click'));
586
+ expect(el.maxSelectedDate).to.eq('1998'); // range was extended to the left
587
+ });
588
+
589
+ it('handles invalid pre-selected range by defaulting to overall max and min', async () => {
590
+ const el = await fixture<HistogramDateRange>(
591
+ html`
592
+ <histogram-date-range
593
+ minDate="1900"
594
+ maxDate="2020"
595
+ minSelectedDate="2000xyz"
596
+ maxSelectedDate="5000"
597
+ bins="[33, 1, 100]"
598
+ >
599
+ </histogram-date-range>
600
+ `
601
+ );
602
+ const minDateInput = el.shadowRoot?.querySelector(
603
+ '#date-min'
604
+ ) as HTMLInputElement;
605
+ // malformed min date defaults to overall min
606
+ expect(minDateInput.value).to.eq('1900');
607
+
608
+ const maxDateInput = el.shadowRoot?.querySelector(
609
+ '#date-max'
610
+ ) as HTMLInputElement;
611
+ // well-formed max date is allowed
612
+ expect(maxDateInput.value).to.eq('5000');
613
+ });
614
+
615
+ it('handles year values less than 1000 correctly', async () => {
616
+ const el = await fixture<HistogramDateRange>(
617
+ html`
618
+ <histogram-date-range
619
+ dateFormat="M/D/YYYY"
620
+ minDate="-2000"
621
+ maxDate="2000"
622
+ minSelectedDate="-500"
623
+ maxSelectedDate="500"
624
+ bins="[33, 1, 100]"
625
+ >
626
+ </histogram-date-range>
627
+ `
628
+ );
629
+ const minDateInput = el.shadowRoot?.querySelector(
630
+ '#date-min'
631
+ ) as HTMLInputElement;
632
+ expect(minDateInput.value).to.eq('1/1/-500');
633
+
634
+ const maxDateInput = el.shadowRoot?.querySelector(
635
+ '#date-max'
636
+ ) as HTMLInputElement;
637
+ expect(maxDateInput.value).to.eq('1/1/500');
638
+ });
639
+
640
+ it('handles missing data', async () => {
641
+ let el = await fixture<HistogramDateRange>(
642
+ html`<histogram-date-range>
643
+ minDate="1900" maxDate="2020" bins=""
644
+ </histogram-date-range>`
645
+ );
646
+ expect(el.shadowRoot?.innerHTML).to.contain('no data');
647
+ el = await fixture<HistogramDateRange>(
648
+ html`<histogram-date-range
649
+ minDate="1900"
650
+ maxDate="2020"
651
+ bins="[]"
652
+ missingDataMessage="no data available"
653
+ ></histogram-date-range>`
654
+ );
655
+ expect(el.shadowRoot?.innerHTML).to.contain('no data available');
656
+ });
657
+
658
+ it('correctly displays data consisting of a single bin', async () => {
659
+ const el = await fixture<HistogramDateRange>(
660
+ html`
661
+ <histogram-date-range minDate="2020" maxDate="2020" bins="[50]">
662
+ </histogram-date-range>
663
+ `
664
+ );
665
+ const bars = el.shadowRoot?.querySelectorAll(
666
+ '.bar'
667
+ ) as unknown as SVGRectElement[];
668
+ const heights = Array.from(bars, b => b.height.baseVal.value);
669
+ expect(heights).to.eql([157]);
670
+ });
671
+
672
+ it('correctly displays small diff between max and min values', async () => {
673
+ const el = await fixture<HistogramDateRange>(
674
+ html`
675
+ <histogram-date-range bins="[1519,2643,1880,2041,1638,1441]">
676
+ </histogram-date-range>
677
+ `
678
+ );
679
+ const bars = el.shadowRoot?.querySelectorAll(
680
+ '.bar'
681
+ ) as unknown as SVGRectElement[];
682
+ const heights = Array.from(bars, b => b.height.baseVal.value);
683
+ expect(heights).to.eql([37, 40, 38, 38, 37, 36]);
684
+ });
685
+
686
+ it('correctly aligns bins to exact month boundaries when binSnapping=month', async () => {
687
+ const el = await fixture<HistogramDateRange>(
688
+ html`
689
+ <histogram-date-range
690
+ binSnapping="month"
691
+ dateFormat="YYYY-MM"
692
+ tooltipDateFormat="MMM YYYY"
693
+ minDate="2020-01"
694
+ maxDate="2021-12"
695
+ bins="[10,20,30,40,50,60,70,80]"
696
+ ></histogram-date-range>
697
+ `
698
+ );
699
+ const bars = el.shadowRoot?.querySelectorAll(
700
+ '.bar-pointer-target'
701
+ ) as unknown as SVGRectElement[];
702
+ const tooltips = Array.from(bars, b => b.dataset.tooltip);
703
+ expect(tooltips).to.eql([
704
+ 'Jan 2020 - Mar 2020',
705
+ 'Apr 2020 - Jun 2020',
706
+ 'Jul 2020 - Sep 2020',
707
+ 'Oct 2020 - Dec 2020',
708
+ 'Jan 2021 - Mar 2021',
709
+ 'Apr 2021 - Jun 2021',
710
+ 'Jul 2021 - Sep 2021',
711
+ 'Oct 2021 - Dec 2021',
712
+ ]);
713
+ });
714
+
715
+ it('correctly handles month snapping for years 0-99', async () => {
716
+ const el = await fixture<HistogramDateRange>(
717
+ html`
718
+ <histogram-date-range
719
+ binSnapping="month"
720
+ dateFormat="YYYY-MM"
721
+ tooltipDateFormat="MMM YYYY"
722
+ minDate="0050-01"
723
+ maxDate="0065-12"
724
+ bins="[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32]"
725
+ ></histogram-date-range>
726
+ `
727
+ );
728
+
729
+ const bars = el.shadowRoot?.querySelectorAll(
730
+ '.bar-pointer-target'
731
+ ) as unknown as SVGRectElement[];
732
+ const tooltips = Array.from(bars, b => b.dataset.tooltip);
733
+ expect(tooltips).to.eql([
734
+ 'Jan 50 - Jun 50',
735
+ 'Jul 50 - Dec 50',
736
+ 'Jan 51 - Jun 51',
737
+ 'Jul 51 - Dec 51',
738
+ 'Jan 52 - Jun 52',
739
+ 'Jul 52 - Dec 52',
740
+ 'Jan 53 - Jun 53',
741
+ 'Jul 53 - Dec 53',
742
+ 'Jan 54 - Jun 54',
743
+ 'Jul 54 - Dec 54',
744
+ 'Jan 55 - Jun 55',
745
+ 'Jul 55 - Dec 55',
746
+ 'Jan 56 - Jun 56',
747
+ 'Jul 56 - Dec 56',
748
+ 'Jan 57 - Jun 57',
749
+ 'Jul 57 - Dec 57',
750
+ 'Jan 58 - Jun 58',
751
+ 'Jul 58 - Dec 58',
752
+ 'Jan 59 - Jun 59',
753
+ 'Jul 59 - Dec 59',
754
+ 'Jan 60 - Jun 60',
755
+ 'Jul 60 - Dec 60',
756
+ 'Jan 61 - Jun 61',
757
+ 'Jul 61 - Dec 61',
758
+ 'Jan 62 - Jun 62',
759
+ 'Jul 62 - Dec 62',
760
+ 'Jan 63 - Jun 63',
761
+ 'Jul 63 - Dec 63',
762
+ 'Jan 64 - Jun 64',
763
+ 'Jul 64 - Dec 64',
764
+ 'Jan 65 - Jun 65',
765
+ 'Jul 65 - Dec 65',
766
+ ]);
767
+ });
768
+
769
+ it('correctly aligns bins to exact year boundaries when binSnapping=year', async () => {
770
+ const el = await fixture<HistogramDateRange>(
771
+ html`
772
+ <histogram-date-range
773
+ binSnapping="year"
774
+ minDate="2000"
775
+ maxDate="2019"
776
+ bins="[10,20,30,40,50,60,70,80,90,100]"
777
+ ></histogram-date-range>
778
+ `
779
+ );
780
+ const bars = el.shadowRoot?.querySelectorAll(
781
+ '.bar-pointer-target'
782
+ ) as unknown as SVGRectElement[];
783
+ const tooltips = Array.from(bars, b => b.dataset.tooltip);
784
+ expect(tooltips).to.eql([
785
+ '2000 - 2001',
786
+ '2002 - 2003',
787
+ '2004 - 2005',
788
+ '2006 - 2007',
789
+ '2008 - 2009',
790
+ '2010 - 2011',
791
+ '2012 - 2013',
792
+ '2014 - 2015',
793
+ '2016 - 2017',
794
+ '2018 - 2019',
795
+ ]);
796
+ });
797
+
798
+ it('correctly handles year snapping for years 0-99', async () => {
799
+ const el = await fixture<HistogramDateRange>(
800
+ html`
801
+ <histogram-date-range
802
+ binSnapping="year"
803
+ dateFormat="YYYY"
804
+ minDate="0020"
805
+ maxDate="0025"
806
+ bins="[1,2,3,4,5,6]"
807
+ ></histogram-date-range>
808
+ `
809
+ );
810
+
811
+ const bars = el.shadowRoot?.querySelectorAll(
812
+ '.bar-pointer-target'
813
+ ) as unknown as SVGRectElement[];
814
+ const tooltips = Array.from(bars, b => b.dataset.tooltip);
815
+ expect(tooltips).to.eql(['20', '21', '22', '23', '24', '25']);
816
+ });
817
+
818
+ it('does not duplicate start/end date in tooltips when representing a single year', async () => {
819
+ const el = await fixture<HistogramDateRange>(
820
+ html`
821
+ <histogram-date-range
822
+ binSnapping="year"
823
+ dateFormat="YYYY"
824
+ minDate="2001"
825
+ maxDate="2005"
826
+ bins="[10,20,30,40,50]"
827
+ ></histogram-date-range>
828
+ `
829
+ );
830
+ const bars = el.shadowRoot?.querySelectorAll(
831
+ '.bar-pointer-target'
832
+ ) as unknown as SVGRectElement[];
833
+ const tooltips = Array.from(bars, b => b.dataset.tooltip);
834
+ expect(tooltips).to.eql(['2001', '2002', '2003', '2004', '2005']);
835
+ });
836
+
837
+ it('falls back to default date format for tooltips if no tooltip date format provided', async () => {
838
+ const el = await fixture<HistogramDateRange>(
839
+ html`
840
+ <histogram-date-range
841
+ binSnapping="year"
842
+ minDate="2001"
843
+ maxDate="2005"
844
+ bins="[10,20,30,40,50]"
845
+ ></histogram-date-range>
846
+ `
847
+ );
848
+
849
+ const bars = el.shadowRoot?.querySelectorAll(
850
+ '.bar-pointer-target'
851
+ ) as unknown as SVGRectElement[];
852
+ let tooltips = Array.from(bars, b => b.dataset.tooltip);
853
+ expect(tooltips).to.eql(['2001', '2002', '2003', '2004', '2005']); // default YYYY date format
854
+
855
+ el.dateFormat = 'YYYY/MM';
856
+ el.minDate = '2001/01';
857
+ el.maxDate = '2005/01';
858
+ await el.updateComplete;
859
+
860
+ // Should use dateFormat fallback for tooltips
861
+ tooltips = Array.from(bars, b => b.dataset.tooltip);
862
+ expect(tooltips).to.eql([
863
+ '2001/01 - 2001/12',
864
+ '2002/01 - 2002/12',
865
+ '2003/01 - 2003/12',
866
+ '2004/01 - 2004/12',
867
+ '2005/01 - 2005/12',
868
+ ]);
869
+
870
+ el.dateFormat = 'YYYY';
871
+ el.tooltipDateFormat = 'MMMM YYYY';
872
+ el.minDate = '2001';
873
+ el.maxDate = '2005';
874
+ await el.updateComplete;
875
+
876
+ // Should use defined tooltipDateFormat for tooltips
877
+ tooltips = Array.from(bars, b => b.dataset.tooltip);
878
+ expect(tooltips).to.eql([
879
+ 'January 2001 - December 2001',
880
+ 'January 2002 - December 2002',
881
+ 'January 2003 - December 2003',
882
+ 'January 2004 - December 2004',
883
+ 'January 2005 - December 2005',
884
+ ]);
885
+ });
886
+
887
+ it('has a disabled state', async () => {
888
+ const el = await fixture<HistogramDateRange>(
889
+ html`
890
+ <histogram-date-range
891
+ minDate="1900"
892
+ maxDate="2020"
893
+ disabled
894
+ bins="[33, 1, 100]"
895
+ >
896
+ </histogram-date-range>
897
+ `
898
+ );
899
+ expect(
900
+ el.shadowRoot
901
+ ?.querySelector('.inner-container')
902
+ ?.classList.contains('disabled')
903
+ ).to.eq(true);
904
+
905
+ const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
906
+
907
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8); // initial state
908
+
909
+ // attempt to slide to right
910
+ minSlider.dispatchEvent(new PointerEvent('pointerdown'));
911
+ await el.updateComplete;
912
+
913
+ // cursor is not draggable if disabled
914
+ expect(minSlider.classList.contains('draggable')).to.be.false;
915
+
916
+ // attempt to slide to right
917
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 70 }));
918
+ await el.updateComplete;
919
+
920
+ // slider does not moved if element disabled
921
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8);
922
+ });
923
+
924
+ it('has a loading state with an activity indicator', async () => {
925
+ const el = await fixture<HistogramDateRange>(
926
+ html`
927
+ <histogram-date-range
928
+ minDate="1900"
929
+ maxDate="2020"
930
+ loading
931
+ bins="[33, 1, 100]"
932
+ >
933
+ </histogram-date-range>
934
+ `
935
+ );
936
+ expect(
937
+ el.shadowRoot
938
+ ?.querySelector('ia-activity-indicator')
939
+ ?.attributes?.getNamedItem('mode')?.value
940
+ ).to.eq('processing');
941
+ });
942
+
943
+ it('can use LitElement bound properties', async () => {
944
+ const el = await fixture<HistogramDateRange>(
945
+ html`
946
+ <histogram-date-range
947
+ .minDate=${1900}
948
+ .maxDate=${'Dec 4, 2020'}
949
+ .minSelectedDate=${2012}
950
+ .maxSelectedDate=${2019}
951
+ .bins=${[33, 1, 100]}
952
+ >
953
+ </histogram-date-range>
954
+ `
955
+ );
956
+ const minDateInput = el.shadowRoot?.querySelector(
957
+ '#date-min'
958
+ ) as HTMLInputElement;
959
+ expect(minDateInput.value).to.eq('2012');
960
+
961
+ const maxDateInput = el.shadowRoot?.querySelector(
962
+ '#date-max'
963
+ ) as HTMLInputElement;
964
+ expect(maxDateInput.value).to.eq('2019');
965
+ });
966
+ });