@internetarchive/histogram-date-range 1.2.0 → 1.2.1

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 (47) hide show
  1. package/.editorconfig +29 -29
  2. package/.eslintrc.js +14 -14
  3. package/.github/workflows/ci.yml +30 -26
  4. package/LICENSE +661 -661
  5. package/README.md +113 -113
  6. package/demo/index.css +22 -22
  7. package/demo/index.html +159 -159
  8. package/dist/demo/app-root.d.ts +19 -0
  9. package/dist/demo/app-root.js +58 -0
  10. package/dist/demo/app-root.js.map +1 -0
  11. package/dist/demo/js/app-root.d.ts +19 -19
  12. package/dist/demo/js/app-root.js +46 -46
  13. package/dist/docs/_snowpack/pkg/@internetarchive/ia-activity-indicator/ia-activity-indicator.d.ts +1 -0
  14. package/dist/docs/_snowpack/pkg/@internetarchive/ia-activity-indicator/ia-activity-indicator.js +2 -0
  15. package/dist/docs/_snowpack/pkg/@internetarchive/ia-activity-indicator/ia-activity-indicator.js.map +1 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/src/histogram-date-range.d.ts +159 -159
  20. package/dist/src/histogram-date-range.js +846 -853
  21. package/dist/src/histogram-date-range.js.map +1 -1
  22. package/dist/test/histogram-date-range.test.d.ts +1 -1
  23. package/dist/test/histogram-date-range.test.js +488 -488
  24. package/dist/test/histogram-date-range.test.js.map +1 -1
  25. package/docs/_snowpack/pkg/@internetarchive/{ia-activity-indicator/ia-activity-indicator.js → ia-activity-indicator.js} +193 -168
  26. package/docs/_snowpack/pkg/common/lit-element-4616b61d.js +22 -0
  27. package/docs/_snowpack/pkg/common/lit-html-a4f3a51c.js +8 -0
  28. package/docs/_snowpack/pkg/common/query-assigned-elements-61aefe32.js +21 -0
  29. package/docs/_snowpack/pkg/dayjs/esm/plugin/customParseFormat.js +355 -0
  30. package/docs/_snowpack/pkg/dayjs/esm.js +631 -0
  31. package/docs/_snowpack/pkg/import-map.json +3 -1
  32. package/docs/_snowpack/pkg/lit/decorators.js +4 -21
  33. package/docs/_snowpack/pkg/lit/directives/live.js +3 -3
  34. package/docs/_snowpack/pkg/lit.js +2 -2
  35. package/docs/demo/index.css +22 -22
  36. package/docs/demo/index.html +159 -159
  37. package/docs/dist/src/histogram-date-range.js +5 -5
  38. package/index.ts +1 -1
  39. package/package.json +85 -84
  40. package/snowpack.config.js +10 -10
  41. package/src/histogram-date-range.ts +914 -922
  42. package/test/histogram-date-range.test.ts +684 -684
  43. package/tsconfig.json +21 -21
  44. package/web-dev-server.config.mjs +28 -28
  45. package/web-test-runner.config.mjs +29 -29
  46. package/docs/_snowpack/pkg/common/lit-element-2ebaea62.js +0 -22
  47. package/docs/_snowpack/pkg/common/lit-html-ab4365d1.js +0 -8
@@ -1,684 +1,684 @@
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(Array.from(minSlider.classList).join(' ')).to.eq('draggable');
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
-
313
- it("emits a custom event when the element's date range changes", async () => {
314
- const el = await createCustomElementInHTMLContainer();
315
- el.updateDelay = 30; // set debounce delay of 30ms
316
-
317
- const minDateInput = el.shadowRoot?.querySelector(
318
- '#date-min'
319
- ) as HTMLInputElement;
320
- const updateEventPromise = oneEvent(el, 'histogramDateRangeUpdated');
321
-
322
- // simulate typing a new value into input
323
- minDateInput.value = '1955';
324
- minDateInput.dispatchEvent(new Event('blur'));
325
-
326
- // will wait longer than debounce delay
327
- const { detail } = await updateEventPromise;
328
- // verify that event is emitted
329
- expect(detail.minDate).to.equal('1/1/1955');
330
- expect(detail.maxDate).to.equal('12/4/2020');
331
-
332
- let eventCount = 0;
333
- el.addEventListener('histogramDateRangeUpdated', () => (eventCount += 1));
334
-
335
- // events are not sent if no change since the last event that was sent
336
- minDateInput.value = '1955';
337
- minDateInput.dispatchEvent(new Event('blur'));
338
- await aTimeout(60); // wait longer than debounce delay
339
- expect(eventCount).to.equal(0);
340
-
341
- const updateEventPromise2 = oneEvent(el, 'histogramDateRangeUpdated');
342
-
343
- // with the debounce, multiple quick changes only result in one event sent
344
- minDateInput.value = '1965';
345
- minDateInput.dispatchEvent(new Event('blur'));
346
- await aTimeout(10); // wait less than the debounce delay
347
-
348
- minDateInput.dispatchEvent(new Event('focus'));
349
- minDateInput.value = '1975';
350
- minDateInput.dispatchEvent(new Event('blur'));
351
- await aTimeout(10);
352
-
353
- minDateInput.dispatchEvent(new Event('focus'));
354
- minDateInput.value = '1985';
355
- minDateInput.dispatchEvent(new Event('blur'));
356
- await aTimeout(10);
357
-
358
- const event2 = await updateEventPromise2;
359
- expect(event2.detail.minDate).to.equal('1/1/1985');
360
- expect(eventCount).to.equal(1); // only one event was fired
361
- });
362
-
363
- it('shows/hides tooltip when hovering over (or pointing at) a bar', async () => {
364
- const el = await createCustomElementInHTMLContainer();
365
- // include a number which will require commas (1,000,000)
366
- el.bins = [1000000, 1, 100];
367
- await aTimeout(10);
368
- const bars = el.shadowRoot?.querySelectorAll(
369
- '.bar'
370
- ) as unknown as SVGRectElement[];
371
- const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
372
- expect(tooltip.innerText).to.eq('');
373
-
374
- // hover
375
- bars[0].dispatchEvent(new PointerEvent('pointerenter'));
376
- await el.updateComplete;
377
- expect(tooltip.innerText).to.match(
378
- /^1,000,000 items\n1\/1\/1900 - 4\/23\/1940/
379
- );
380
- expect(getComputedStyle(tooltip).display).to.eq('block');
381
-
382
- // leave
383
- bars[0].dispatchEvent(new PointerEvent('pointerleave'));
384
- await el.updateComplete;
385
- expect(getComputedStyle(tooltip).display).to.eq('none');
386
- expect(tooltip.innerText).to.eq('');
387
-
388
- // ensure singular item is not pluralized
389
- bars[1].dispatchEvent(new PointerEvent('pointerenter'));
390
- await el.updateComplete;
391
- expect(tooltip.innerText).to.match(/^1 item\n4\/23\/1940 - 8\/13\/1980/);
392
- });
393
-
394
- it('does not show tooltip while dragging', async () => {
395
- const el = await createCustomElementInHTMLContainer();
396
- const bars = el.shadowRoot?.querySelectorAll(
397
- '.bar'
398
- ) as unknown as SVGRectElement[];
399
- const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
400
- expect(tooltip.innerText).to.eq('');
401
- const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
402
-
403
- // pointer down and slide right
404
- minSlider.dispatchEvent(new PointerEvent('pointerdown'));
405
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 100 }));
406
- await el.updateComplete;
407
-
408
- // hover over bar
409
- bars[0].dispatchEvent(new PointerEvent('pointerenter'));
410
- await el.updateComplete;
411
- // tooltip display is suppressed while dragging
412
- expect(tooltip.style.display).to.eq('');
413
- });
414
-
415
- it('passes the a11y audit', async () => {
416
- await fixture<HistogramDateRange>(subject).then(el =>
417
- expect(el).shadowDom.to.be.accessible()
418
- );
419
- });
420
-
421
- it('allows range to be pre-selected', async () => {
422
- const el = await fixture<HistogramDateRange>(
423
- html`
424
- <histogram-date-range
425
- minDate="1900"
426
- maxDate="Dec 4, 2020"
427
- minSelectedDate="2012"
428
- maxSelectedDate="2019"
429
- bins="[33, 1, 100]"
430
- >
431
- </histogram-date-range>
432
- `
433
- );
434
- const minDateInput = el.shadowRoot?.querySelector(
435
- '#date-min'
436
- ) as HTMLInputElement;
437
- expect(minDateInput.value).to.eq('2012');
438
-
439
- const maxDateInput = el.shadowRoot?.querySelector(
440
- '#date-max'
441
- ) as HTMLInputElement;
442
- expect(maxDateInput.value).to.eq('2019');
443
- });
444
-
445
- it('extends the selected range when the histogram is clicked outside of the current range', async () => {
446
- const el = await fixture<HistogramDateRange>(
447
- html`
448
- <histogram-date-range
449
- minDate="1900"
450
- maxDate="2020"
451
- minSelectedDate="1950"
452
- maxSelectedDate="1955"
453
- bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
454
- >
455
- </histogram-date-range>
456
- `
457
- );
458
-
459
- const leftBarToClick = Array.from(
460
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
461
- )[1]; // click on second bar to the left
462
-
463
- leftBarToClick.dispatchEvent(new Event('click'));
464
- await el.updateComplete;
465
- expect(el.minSelectedDate).to.eq('1910'); // range was extended to left
466
-
467
- const rightBarToClick = Array.from(
468
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
469
- )[8]; // click on second bar from the right
470
-
471
- rightBarToClick.dispatchEvent(new Event('click'));
472
- expect(el.maxSelectedDate).to.eq('1998'); // range was extended to right
473
- });
474
-
475
- it('narrows the selected range when the histogram is clicked inside of the current range', async () => {
476
- const el = await fixture<HistogramDateRange>(
477
- html`
478
- <histogram-date-range
479
- minDate="1900"
480
- maxDate="2020"
481
- minSelectedDate="1900"
482
- maxSelectedDate="2020"
483
- bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
484
- >
485
- </histogram-date-range>
486
- `
487
- );
488
-
489
- ///////////////////////////////////////////////
490
- // NB: the slider nearest the clicked bar moves
491
- ///////////////////////////////////////////////
492
-
493
- const leftBarToClick = Array.from(
494
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
495
- )[3]; // click on fourth bar to the left
496
-
497
- leftBarToClick.dispatchEvent(new Event('click'));
498
- expect(el.minSelectedDate).to.eq('1932'); // range was extended to the right
499
-
500
- const rightBarToClick = Array.from(
501
- el.shadowRoot?.querySelectorAll('.bar') as NodeList
502
- )[8]; // click on second bar from the right
503
-
504
- rightBarToClick.dispatchEvent(new Event('click'));
505
- expect(el.maxSelectedDate).to.eq('1998'); // range was extended to the left
506
- });
507
-
508
- it('handles invalid pre-selected range by defaulting to overall max and min', async () => {
509
- const el = await fixture<HistogramDateRange>(
510
- html`
511
- <histogram-date-range
512
- minDate="1900"
513
- maxDate="2020"
514
- minSelectedDate="2000xyz"
515
- maxSelectedDate="5000"
516
- bins="[33, 1, 100]"
517
- >
518
- </histogram-date-range>
519
- `
520
- );
521
- const minDateInput = el.shadowRoot?.querySelector(
522
- '#date-min'
523
- ) as HTMLInputElement;
524
- // malformed min date defaults to overall min
525
- expect(minDateInput.value).to.eq('1900');
526
-
527
- const maxDateInput = el.shadowRoot?.querySelector(
528
- '#date-max'
529
- ) as HTMLInputElement;
530
- // well-formed max date is allowed
531
- expect(maxDateInput.value).to.eq('5000');
532
- });
533
-
534
- it('handles year values less than 1000 by overriding date format to just display year', async () => {
535
- const el = await fixture<HistogramDateRange>(
536
- html`
537
- <histogram-date-range
538
- dateFormat="M/D/YYYY"
539
- minDate="-2000"
540
- maxDate="2000"
541
- minSelectedDate="-500"
542
- maxSelectedDate="500"
543
- bins="[33, 1, 100]"
544
- >
545
- </histogram-date-range>
546
- `
547
- );
548
- const minDateInput = el.shadowRoot?.querySelector(
549
- '#date-min'
550
- ) as HTMLInputElement;
551
- expect(minDateInput.value).to.eq('-500');
552
-
553
- const maxDateInput = el.shadowRoot?.querySelector(
554
- '#date-max'
555
- ) as HTMLInputElement;
556
- expect(maxDateInput.value).to.eq('500');
557
- });
558
-
559
- it('handles missing data', async () => {
560
- let el = await fixture<HistogramDateRange>(
561
- html`<histogram-date-range>
562
- minDate="1900" maxDate="2020" bins=""
563
- </histogram-date-range>`
564
- );
565
- expect(el.shadowRoot?.innerHTML).to.contain('no data');
566
- el = await fixture<HistogramDateRange>(
567
- html`<histogram-date-range
568
- minDate="1900"
569
- maxDate="2020"
570
- bins="[]"
571
- missingDataMessage="no data available"
572
- ></histogram-date-range>`
573
- );
574
- expect(el.shadowRoot?.innerHTML).to.contain('no data available');
575
- });
576
-
577
- it('correctly displays data consisting of a single bin', async () => {
578
- const el = await fixture<HistogramDateRange>(
579
- html`
580
- <histogram-date-range minDate="2020" maxDate="2020" bins="[50]">
581
- </histogram-date-range>
582
- `
583
- );
584
- const bars = el.shadowRoot?.querySelectorAll(
585
- '.bar'
586
- ) as unknown as SVGRectElement[];
587
- const heights = Array.from(bars).map(b => b.height.baseVal.value);
588
- expect(heights).to.eql([157]);
589
- });
590
-
591
- it('correctly displays small diff between max and min values', async () => {
592
- const el = await fixture<HistogramDateRange>(
593
- html`
594
- <histogram-date-range bins="[1519,2643,1880,2041,1638,1441]">
595
- </histogram-date-range>
596
- `
597
- );
598
- const bars = el.shadowRoot?.querySelectorAll(
599
- '.bar'
600
- ) as unknown as SVGRectElement[];
601
- const heights = Array.from(bars).map(b => b.height.baseVal.value);
602
- expect(heights).to.eql([37, 40, 38, 38, 37, 36]);
603
- });
604
-
605
- it('has a disabled state', async () => {
606
- const el = await fixture<HistogramDateRange>(
607
- html`
608
- <histogram-date-range
609
- minDate="1900"
610
- maxDate="2020"
611
- disabled
612
- bins="[33, 1, 100]"
613
- >
614
- </histogram-date-range>
615
- `
616
- );
617
- expect(
618
- el.shadowRoot
619
- ?.querySelector('.inner-container')
620
- ?.classList.contains('disabled')
621
- ).to.eq(true);
622
-
623
- const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
624
-
625
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8); // initial state
626
-
627
- // attempt to slide to right
628
- minSlider.dispatchEvent(new PointerEvent('pointerdown'));
629
- await el.updateComplete;
630
-
631
- // cursor is not draggable if disabled
632
- expect(Array.from(minSlider.classList).join(' ')).to.eq('');
633
-
634
- // attempt to slide to right
635
- window.dispatchEvent(new PointerEvent('pointermove', { clientX: 70 }));
636
- await el.updateComplete;
637
-
638
- // slider does not moved if element disabled
639
- expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8);
640
- });
641
-
642
- it('has a loading state with an activity indicator', async () => {
643
- const el = await fixture<HistogramDateRange>(
644
- html`
645
- <histogram-date-range
646
- minDate="1900"
647
- maxDate="2020"
648
- loading
649
- bins="[33, 1, 100]"
650
- >
651
- </histogram-date-range>
652
- `
653
- );
654
- expect(
655
- el.shadowRoot
656
- ?.querySelector('ia-activity-indicator')
657
- ?.attributes?.getNamedItem('mode')?.value
658
- ).to.eq('processing');
659
- });
660
-
661
- it('can use LitElement bound properties', async () => {
662
- const el = await fixture<HistogramDateRange>(
663
- html`
664
- <histogram-date-range
665
- .minDate=${1900}
666
- .maxDate=${'Dec 4, 2020'}
667
- .minSelectedDate=${2012}
668
- .maxSelectedDate=${2019}
669
- .bins=${[33, 1, 100]}
670
- >
671
- </histogram-date-range>
672
- `
673
- );
674
- const minDateInput = el.shadowRoot?.querySelector(
675
- '#date-min'
676
- ) as HTMLInputElement;
677
- expect(minDateInput.value).to.eq('2012');
678
-
679
- const maxDateInput = el.shadowRoot?.querySelector(
680
- '#date-max'
681
- ) as HTMLInputElement;
682
- expect(maxDateInput.value).to.eq('2019');
683
- });
684
- });
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(Array.from(minSlider.classList).join(' ')).to.eq('draggable');
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
+
313
+ it("emits a custom event when the element's date range changes", async () => {
314
+ const el = await createCustomElementInHTMLContainer();
315
+ el.updateDelay = 30; // set debounce delay of 30ms
316
+
317
+ const minDateInput = el.shadowRoot?.querySelector(
318
+ '#date-min'
319
+ ) as HTMLInputElement;
320
+ const updateEventPromise = oneEvent(el, 'histogramDateRangeUpdated');
321
+
322
+ // simulate typing a new value into input
323
+ minDateInput.value = '1955';
324
+ minDateInput.dispatchEvent(new Event('blur'));
325
+
326
+ // will wait longer than debounce delay
327
+ const { detail } = await updateEventPromise;
328
+ // verify that event is emitted
329
+ expect(detail.minDate).to.equal('1/1/1955');
330
+ expect(detail.maxDate).to.equal('12/4/2020');
331
+
332
+ let eventCount = 0;
333
+ el.addEventListener('histogramDateRangeUpdated', () => (eventCount += 1));
334
+
335
+ // events are not sent if no change since the last event that was sent
336
+ minDateInput.value = '1955';
337
+ minDateInput.dispatchEvent(new Event('blur'));
338
+ await aTimeout(60); // wait longer than debounce delay
339
+ expect(eventCount).to.equal(0);
340
+
341
+ const updateEventPromise2 = oneEvent(el, 'histogramDateRangeUpdated');
342
+
343
+ // with the debounce, multiple quick changes only result in one event sent
344
+ minDateInput.value = '1965';
345
+ minDateInput.dispatchEvent(new Event('blur'));
346
+ await aTimeout(10); // wait less than the debounce delay
347
+
348
+ minDateInput.dispatchEvent(new Event('focus'));
349
+ minDateInput.value = '1975';
350
+ minDateInput.dispatchEvent(new Event('blur'));
351
+ await aTimeout(10);
352
+
353
+ minDateInput.dispatchEvent(new Event('focus'));
354
+ minDateInput.value = '1985';
355
+ minDateInput.dispatchEvent(new Event('blur'));
356
+ await aTimeout(10);
357
+
358
+ const event2 = await updateEventPromise2;
359
+ expect(event2.detail.minDate).to.equal('1/1/1985');
360
+ expect(eventCount).to.equal(1); // only one event was fired
361
+ });
362
+
363
+ it('shows/hides tooltip when hovering over (or pointing at) a bar', async () => {
364
+ const el = await createCustomElementInHTMLContainer();
365
+ // include a number which will require commas (1,000,000)
366
+ el.bins = [1000000, 1, 100];
367
+ await aTimeout(10);
368
+ const bars = el.shadowRoot?.querySelectorAll(
369
+ '.bar'
370
+ ) as unknown as SVGRectElement[];
371
+ const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
372
+ expect(tooltip.innerText).to.eq('');
373
+
374
+ // hover
375
+ bars[0].dispatchEvent(new PointerEvent('pointerenter'));
376
+ await el.updateComplete;
377
+ expect(tooltip.innerText).to.match(
378
+ /^1,000,000 items\n1\/1\/1900 - 4\/23\/1940/
379
+ );
380
+ expect(getComputedStyle(tooltip).display).to.eq('block');
381
+
382
+ // leave
383
+ bars[0].dispatchEvent(new PointerEvent('pointerleave'));
384
+ await el.updateComplete;
385
+ expect(getComputedStyle(tooltip).display).to.eq('none');
386
+ expect(tooltip.innerText).to.eq('');
387
+
388
+ // ensure singular item is not pluralized
389
+ bars[1].dispatchEvent(new PointerEvent('pointerenter'));
390
+ await el.updateComplete;
391
+ expect(tooltip.innerText).to.match(/^1 item\n4\/23\/1940 - 8\/13\/1980/);
392
+ });
393
+
394
+ it('does not show tooltip while dragging', async () => {
395
+ const el = await createCustomElementInHTMLContainer();
396
+ const bars = el.shadowRoot?.querySelectorAll(
397
+ '.bar'
398
+ ) as unknown as SVGRectElement[];
399
+ const tooltip = el.shadowRoot?.querySelector('#tooltip') as HTMLDivElement;
400
+ expect(tooltip.innerText).to.eq('');
401
+ const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
402
+
403
+ // pointer down and slide right
404
+ minSlider.dispatchEvent(new PointerEvent('pointerdown'));
405
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 100 }));
406
+ await el.updateComplete;
407
+
408
+ // hover over bar
409
+ bars[0].dispatchEvent(new PointerEvent('pointerenter'));
410
+ await el.updateComplete;
411
+ // tooltip display is suppressed while dragging
412
+ expect(tooltip.style.display).to.eq('');
413
+ });
414
+
415
+ it('passes the a11y audit', async () => {
416
+ await fixture<HistogramDateRange>(subject).then(el =>
417
+ expect(el).shadowDom.to.be.accessible()
418
+ );
419
+ });
420
+
421
+ it('allows range to be pre-selected', async () => {
422
+ const el = await fixture<HistogramDateRange>(
423
+ html`
424
+ <histogram-date-range
425
+ minDate="1900"
426
+ maxDate="Dec 4, 2020"
427
+ minSelectedDate="2012"
428
+ maxSelectedDate="2019"
429
+ bins="[33, 1, 100]"
430
+ >
431
+ </histogram-date-range>
432
+ `
433
+ );
434
+ const minDateInput = el.shadowRoot?.querySelector(
435
+ '#date-min'
436
+ ) as HTMLInputElement;
437
+ expect(minDateInput.value).to.eq('2012');
438
+
439
+ const maxDateInput = el.shadowRoot?.querySelector(
440
+ '#date-max'
441
+ ) as HTMLInputElement;
442
+ expect(maxDateInput.value).to.eq('2019');
443
+ });
444
+
445
+ it('extends the selected range when the histogram is clicked outside of the current range', async () => {
446
+ const el = await fixture<HistogramDateRange>(
447
+ html`
448
+ <histogram-date-range
449
+ minDate="1900"
450
+ maxDate="2020"
451
+ minSelectedDate="1950"
452
+ maxSelectedDate="1955"
453
+ bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
454
+ >
455
+ </histogram-date-range>
456
+ `
457
+ );
458
+
459
+ const leftBarToClick = Array.from(
460
+ el.shadowRoot?.querySelectorAll('.bar') as NodeList
461
+ )[1]; // click on second bar to the left
462
+
463
+ leftBarToClick.dispatchEvent(new Event('click'));
464
+ await el.updateComplete;
465
+ expect(el.minSelectedDate).to.eq('1910'); // range was extended to left
466
+
467
+ const rightBarToClick = Array.from(
468
+ el.shadowRoot?.querySelectorAll('.bar') as NodeList
469
+ )[8]; // click on second bar from the right
470
+
471
+ rightBarToClick.dispatchEvent(new Event('click'));
472
+ expect(el.maxSelectedDate).to.eq('1998'); // range was extended to right
473
+ });
474
+
475
+ it('narrows the selected range when the histogram is clicked inside of the current range', async () => {
476
+ const el = await fixture<HistogramDateRange>(
477
+ html`
478
+ <histogram-date-range
479
+ minDate="1900"
480
+ maxDate="2020"
481
+ minSelectedDate="1900"
482
+ maxSelectedDate="2020"
483
+ bins="[33, 1, 1, 1, 10, 10, 1, 1, 1, 50, 100]"
484
+ >
485
+ </histogram-date-range>
486
+ `
487
+ );
488
+
489
+ ///////////////////////////////////////////////
490
+ // NB: the slider nearest the clicked bar moves
491
+ ///////////////////////////////////////////////
492
+
493
+ const leftBarToClick = Array.from(
494
+ el.shadowRoot?.querySelectorAll('.bar') as NodeList
495
+ )[3]; // click on fourth bar to the left
496
+
497
+ leftBarToClick.dispatchEvent(new Event('click'));
498
+ expect(el.minSelectedDate).to.eq('1932'); // range was extended to the right
499
+
500
+ const rightBarToClick = Array.from(
501
+ el.shadowRoot?.querySelectorAll('.bar') as NodeList
502
+ )[8]; // click on second bar from the right
503
+
504
+ rightBarToClick.dispatchEvent(new Event('click'));
505
+ expect(el.maxSelectedDate).to.eq('1998'); // range was extended to the left
506
+ });
507
+
508
+ it('handles invalid pre-selected range by defaulting to overall max and min', async () => {
509
+ const el = await fixture<HistogramDateRange>(
510
+ html`
511
+ <histogram-date-range
512
+ minDate="1900"
513
+ maxDate="2020"
514
+ minSelectedDate="2000xyz"
515
+ maxSelectedDate="5000"
516
+ bins="[33, 1, 100]"
517
+ >
518
+ </histogram-date-range>
519
+ `
520
+ );
521
+ const minDateInput = el.shadowRoot?.querySelector(
522
+ '#date-min'
523
+ ) as HTMLInputElement;
524
+ // malformed min date defaults to overall min
525
+ expect(minDateInput.value).to.eq('1900');
526
+
527
+ const maxDateInput = el.shadowRoot?.querySelector(
528
+ '#date-max'
529
+ ) as HTMLInputElement;
530
+ // well-formed max date is allowed
531
+ expect(maxDateInput.value).to.eq('5000');
532
+ });
533
+
534
+ it('handles year values less than 1000 by overriding date format to just display year', async () => {
535
+ const el = await fixture<HistogramDateRange>(
536
+ html`
537
+ <histogram-date-range
538
+ dateFormat="M/D/YYYY"
539
+ minDate="-2000"
540
+ maxDate="2000"
541
+ minSelectedDate="-500"
542
+ maxSelectedDate="500"
543
+ bins="[33, 1, 100]"
544
+ >
545
+ </histogram-date-range>
546
+ `
547
+ );
548
+ const minDateInput = el.shadowRoot?.querySelector(
549
+ '#date-min'
550
+ ) as HTMLInputElement;
551
+ expect(minDateInput.value).to.eq('-500');
552
+
553
+ const maxDateInput = el.shadowRoot?.querySelector(
554
+ '#date-max'
555
+ ) as HTMLInputElement;
556
+ expect(maxDateInput.value).to.eq('500');
557
+ });
558
+
559
+ it('handles missing data', async () => {
560
+ let el = await fixture<HistogramDateRange>(
561
+ html`<histogram-date-range>
562
+ minDate="1900" maxDate="2020" bins=""
563
+ </histogram-date-range>`
564
+ );
565
+ expect(el.shadowRoot?.innerHTML).to.contain('no data');
566
+ el = await fixture<HistogramDateRange>(
567
+ html`<histogram-date-range
568
+ minDate="1900"
569
+ maxDate="2020"
570
+ bins="[]"
571
+ missingDataMessage="no data available"
572
+ ></histogram-date-range>`
573
+ );
574
+ expect(el.shadowRoot?.innerHTML).to.contain('no data available');
575
+ });
576
+
577
+ it('correctly displays data consisting of a single bin', async () => {
578
+ const el = await fixture<HistogramDateRange>(
579
+ html`
580
+ <histogram-date-range minDate="2020" maxDate="2020" bins="[50]">
581
+ </histogram-date-range>
582
+ `
583
+ );
584
+ const bars = el.shadowRoot?.querySelectorAll(
585
+ '.bar'
586
+ ) as unknown as SVGRectElement[];
587
+ const heights = Array.from(bars).map(b => b.height.baseVal.value);
588
+ expect(heights).to.eql([157]);
589
+ });
590
+
591
+ it('correctly displays small diff between max and min values', async () => {
592
+ const el = await fixture<HistogramDateRange>(
593
+ html`
594
+ <histogram-date-range bins="[1519,2643,1880,2041,1638,1441]">
595
+ </histogram-date-range>
596
+ `
597
+ );
598
+ const bars = el.shadowRoot?.querySelectorAll(
599
+ '.bar'
600
+ ) as unknown as SVGRectElement[];
601
+ const heights = Array.from(bars).map(b => b.height.baseVal.value);
602
+ expect(heights).to.eql([37, 40, 38, 38, 37, 36]);
603
+ });
604
+
605
+ it('has a disabled state', async () => {
606
+ const el = await fixture<HistogramDateRange>(
607
+ html`
608
+ <histogram-date-range
609
+ minDate="1900"
610
+ maxDate="2020"
611
+ disabled
612
+ bins="[33, 1, 100]"
613
+ >
614
+ </histogram-date-range>
615
+ `
616
+ );
617
+ expect(
618
+ el.shadowRoot
619
+ ?.querySelector('.inner-container')
620
+ ?.classList.contains('disabled')
621
+ ).to.eq(true);
622
+
623
+ const minSlider = el.shadowRoot?.querySelector('#slider-min') as SVGElement;
624
+
625
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8); // initial state
626
+
627
+ // attempt to slide to right
628
+ minSlider.dispatchEvent(new PointerEvent('pointerdown'));
629
+ await el.updateComplete;
630
+
631
+ // cursor is not draggable if disabled
632
+ expect(Array.from(minSlider.classList).join(' ')).to.eq('');
633
+
634
+ // attempt to slide to right
635
+ window.dispatchEvent(new PointerEvent('pointermove', { clientX: 70 }));
636
+ await el.updateComplete;
637
+
638
+ // slider does not moved if element disabled
639
+ expect(Math.round(minSlider.getBoundingClientRect().x)).to.eq(8);
640
+ });
641
+
642
+ it('has a loading state with an activity indicator', async () => {
643
+ const el = await fixture<HistogramDateRange>(
644
+ html`
645
+ <histogram-date-range
646
+ minDate="1900"
647
+ maxDate="2020"
648
+ loading
649
+ bins="[33, 1, 100]"
650
+ >
651
+ </histogram-date-range>
652
+ `
653
+ );
654
+ expect(
655
+ el.shadowRoot
656
+ ?.querySelector('ia-activity-indicator')
657
+ ?.attributes?.getNamedItem('mode')?.value
658
+ ).to.eq('processing');
659
+ });
660
+
661
+ it('can use LitElement bound properties', async () => {
662
+ const el = await fixture<HistogramDateRange>(
663
+ html`
664
+ <histogram-date-range
665
+ .minDate=${1900}
666
+ .maxDate=${'Dec 4, 2020'}
667
+ .minSelectedDate=${2012}
668
+ .maxSelectedDate=${2019}
669
+ .bins=${[33, 1, 100]}
670
+ >
671
+ </histogram-date-range>
672
+ `
673
+ );
674
+ const minDateInput = el.shadowRoot?.querySelector(
675
+ '#date-min'
676
+ ) as HTMLInputElement;
677
+ expect(minDateInput.value).to.eq('2012');
678
+
679
+ const maxDateInput = el.shadowRoot?.querySelector(
680
+ '#date-max'
681
+ ) as HTMLInputElement;
682
+ expect(maxDateInput.value).to.eq('2019');
683
+ });
684
+ });