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