@reliability-design/web 1.4.5 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/rel-alert/rel-alert.ts +1 -1
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
- package/dist/components/rel-badge/rel-badge.ts +6 -1
- package/dist/components/rel-calendar/rel-calendar.ts +64 -18
- package/dist/components/rel-carousel/rel-carousel.ts +25 -2
- package/dist/components/rel-chip/rel-chip.ts +31 -20
- package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
- package/dist/components/rel-date-picker/date-parse.ts +178 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +529 -90
- package/dist/components/rel-drawer/rel-drawer.ts +6 -3
- package/dist/components/rel-menu/rel-menu.ts +6 -0
- package/dist/components/rel-otp/rel-otp.ts +12 -2
- package/dist/components/rel-pagination/rel-pagination.ts +13 -0
- package/dist/components/rel-popover/rel-popover.ts +33 -2
- package/dist/components/rel-progress/rel-progress.ts +13 -4
- package/dist/components/rel-radio/README.md +6 -3
- package/dist/components/rel-radio/rel-radio.ts +236 -60
- package/dist/components/rel-range-picker/rel-range-picker.ts +452 -95
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +5 -1
- package/dist/components/rel-select/rel-select.ts +44 -13
- package/dist/components/rel-sheet/rel-sheet.ts +163 -18
- package/dist/components/rel-sidebar/rel-sidebar.ts +7 -1
- package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
- package/dist/components/rel-slider/rel-slider.ts +6 -3
- package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
- package/dist/components/rel-stepper/rel-stepper.ts +3 -1
- package/dist/components/rel-switch/rel-switch.ts +3 -1
- package/dist/components/rel-textfield/rel-textfield.ts +263 -75
- package/dist/components/rel-time-picker/rel-time-picker.ts +450 -94
- package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
- package/dist/components/rel-time-picker/time-parse.ts +85 -0
- package/dist/components/rel-toast/rel-toast.ts +4 -2
- package/dist/components/rel-tooltip/README.md +13 -5
- package/dist/components/rel-tooltip/rel-tooltip.ts +3 -0
- package/dist/components/rel-upload/rel-upload.ts +25 -0
- package/dist/index.d.mts +241 -11
- package/dist/index.d.ts +241 -11
- package/dist/index.js +2573 -598
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2614 -640
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -278,7 +278,7 @@ export class RelAlert extends LitElement {
|
|
|
278
278
|
|
|
279
279
|
render() {
|
|
280
280
|
return html`
|
|
281
|
-
<div part="base" class="rel-alert alert-${this.type}" role="alert">
|
|
281
|
+
<div part="base" class="rel-alert alert-${this.type}" role="alert" aria-atomic="true">
|
|
282
282
|
<div part="icon" class="rel-icon">${this.iconTemplate}</div>
|
|
283
283
|
|
|
284
284
|
<div part="content" class="rel-content">
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
TextFieldVariant,
|
|
10
10
|
FieldLabelStyle,
|
|
11
11
|
} from "../rel-textfield/rel-textfield.js";
|
|
12
|
+
import { announce } from "../../utils/announce";
|
|
12
13
|
|
|
13
14
|
/** An item is a plain string or any object read through displayField/valueField. */
|
|
14
15
|
export type AutocompleteItem = string | Record<string, unknown>;
|
|
@@ -246,6 +247,15 @@ export class RelAutocomplete extends LitElement {
|
|
|
246
247
|
this.internalValueWrite = false;
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
private wasLoading = false;
|
|
251
|
+
|
|
252
|
+
protected updated() {
|
|
253
|
+
if (this.open && this.isLoading && !this.wasLoading) {
|
|
254
|
+
announce("Loading suggestions");
|
|
255
|
+
}
|
|
256
|
+
this.wasLoading = this.isLoading;
|
|
257
|
+
}
|
|
258
|
+
|
|
249
259
|
/** Full normalized source list (remote results while searchFn drives, else items). */
|
|
250
260
|
private get allItems(): NormalizedItem[] {
|
|
251
261
|
const source = this.searchFn && this.remoteItems !== null ? this.remoteItems : this.items;
|
|
@@ -290,13 +300,15 @@ export class RelAutocomplete extends LitElement {
|
|
|
290
300
|
// pointerdown and on any outside scroll instead of re-anchoring) ──
|
|
291
301
|
|
|
292
302
|
private addGlobalListeners() {
|
|
293
|
-
|
|
303
|
+
// Capture phase so a pointerdown that another component stops in its own
|
|
304
|
+
// capture listener (e.g. a rel-popover trigger) still closes this panel.
|
|
305
|
+
document.addEventListener("pointerdown", this.onOutsidePointerDown, true);
|
|
294
306
|
window.addEventListener("resize", this.updatePosition);
|
|
295
307
|
window.addEventListener("scroll", this.onPageScroll, true);
|
|
296
308
|
}
|
|
297
309
|
|
|
298
310
|
private removeGlobalListeners() {
|
|
299
|
-
document.removeEventListener("pointerdown", this.onOutsidePointerDown);
|
|
311
|
+
document.removeEventListener("pointerdown", this.onOutsidePointerDown, true);
|
|
300
312
|
window.removeEventListener("resize", this.updatePosition);
|
|
301
313
|
window.removeEventListener("scroll", this.onPageScroll, true);
|
|
302
314
|
}
|
|
@@ -433,6 +445,17 @@ export class RelAutocomplete extends LitElement {
|
|
|
433
445
|
this.debounceTimer = setTimeout(() => this.runSearch(this.inputText), this.debounceMs);
|
|
434
446
|
} else {
|
|
435
447
|
this.syncPanel();
|
|
448
|
+
this.announceResults();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private announceResults() {
|
|
453
|
+
if (!this.open || this.isLoading) return;
|
|
454
|
+
const n = this.filteredItems.length;
|
|
455
|
+
if (n === 0) {
|
|
456
|
+
if (this.inputText) announce(this.noResultsText);
|
|
457
|
+
} else {
|
|
458
|
+
announce(`${n} result${n === 1 ? "" : "s"} available`);
|
|
436
459
|
}
|
|
437
460
|
}
|
|
438
461
|
|
|
@@ -452,6 +475,7 @@ export class RelAutocomplete extends LitElement {
|
|
|
452
475
|
if (seq === this.searchSeq) {
|
|
453
476
|
this.searching = false;
|
|
454
477
|
this.syncPanel();
|
|
478
|
+
this.announceResults();
|
|
455
479
|
}
|
|
456
480
|
}
|
|
457
481
|
}
|
|
@@ -523,11 +547,13 @@ export class RelAutocomplete extends LitElement {
|
|
|
523
547
|
return;
|
|
524
548
|
}
|
|
525
549
|
this.highlightedIndex = Math.min(filtered.length - 1, this.highlightedIndex + 1);
|
|
550
|
+
this.announceHighlighted(filtered);
|
|
526
551
|
break;
|
|
527
552
|
case "ArrowUp":
|
|
528
553
|
e.preventDefault();
|
|
529
554
|
if (!this.open) return;
|
|
530
555
|
this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
|
|
556
|
+
this.announceHighlighted(filtered);
|
|
531
557
|
break;
|
|
532
558
|
case "Enter":
|
|
533
559
|
if (this.open && this.highlightedIndex >= 0 && filtered[this.highlightedIndex]) {
|
|
@@ -552,6 +578,11 @@ export class RelAutocomplete extends LitElement {
|
|
|
552
578
|
}
|
|
553
579
|
}
|
|
554
580
|
|
|
581
|
+
private announceHighlighted(filtered: NormalizedItem[]) {
|
|
582
|
+
const item = filtered[this.highlightedIndex];
|
|
583
|
+
if (item) announce(`${item.label}, ${this.highlightedIndex + 1} of ${filtered.length}`);
|
|
584
|
+
}
|
|
585
|
+
|
|
555
586
|
private onFocusIn() {
|
|
556
587
|
if (this.disabled || this.readonly) return;
|
|
557
588
|
if (this.queryLongEnough && (this.filteredItems.length > 0 || this.minSearchLength === 0)) {
|
|
@@ -142,6 +142,11 @@ export class RelBadge extends LitElement {
|
|
|
142
142
|
return ["badge", `badge-${this.size}`, `badge-${this.variant}`].join(" ");
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
private get computedAriaLabel() {
|
|
146
|
+
if (this.ariaLabel) return this.ariaLabel;
|
|
147
|
+
return this.number ? `${this.number} ${this.suffix}` : `New ${this.suffix}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
145
150
|
override updated(changedProperties: Map<PropertyKey, unknown>): void {
|
|
146
151
|
if (changedProperties.has("color")) {
|
|
147
152
|
if (this.color) {
|
|
@@ -162,7 +167,7 @@ export class RelBadge extends LitElement {
|
|
|
162
167
|
|
|
163
168
|
render() {
|
|
164
169
|
return html`
|
|
165
|
-
<span part="base" class="${this.classes}" aria-label=${this.
|
|
170
|
+
<span part="base" class="${this.classes}" role="status" aria-label=${this.computedAriaLabel}>
|
|
166
171
|
${this.variant === "number" ? this.number : null}
|
|
167
172
|
</span>
|
|
168
173
|
`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
+
import { announce } from "../../utils/announce";
|
|
3
4
|
|
|
4
5
|
interface CalendarDay {
|
|
5
6
|
date: Date;
|
|
@@ -99,6 +100,13 @@ export class RelCalendar extends LitElement {
|
|
|
99
100
|
gap: var(--rel-spacing-2xs);
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
.calendar-week {
|
|
104
|
+
grid-column: 1 / -1;
|
|
105
|
+
display: grid;
|
|
106
|
+
grid-template-columns: repeat(7, 1fr);
|
|
107
|
+
gap: var(--rel-spacing-2xs);
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
.weekday {
|
|
103
111
|
text-align: center;
|
|
104
112
|
color: var(--rel-text-placeholder);
|
|
@@ -315,6 +323,26 @@ export class RelCalendar extends LitElement {
|
|
|
315
323
|
return formatter.format(new Date(this.viewYear, this.viewMonth, 1));
|
|
316
324
|
}
|
|
317
325
|
|
|
326
|
+
private dayLabelFormatter: Intl.DateTimeFormat | null = null;
|
|
327
|
+
private dayLabelLocale = "";
|
|
328
|
+
|
|
329
|
+
private formatDayLabel(date: Date): string {
|
|
330
|
+
if (!this.dayLabelFormatter || this.dayLabelLocale !== this.locale) {
|
|
331
|
+
this.dayLabelLocale = this.locale;
|
|
332
|
+
this.dayLabelFormatter = new Intl.DateTimeFormat(this.locale, {
|
|
333
|
+
weekday: "long",
|
|
334
|
+
month: "long",
|
|
335
|
+
day: "numeric",
|
|
336
|
+
year: "numeric",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return this.dayLabelFormatter.format(date);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private announceMonth() {
|
|
343
|
+
announce(`${this.getMonthName()} ${this.viewYear}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
318
346
|
private previousMonth() {
|
|
319
347
|
if (this.viewMonth === 0) {
|
|
320
348
|
this.viewMonth = 11;
|
|
@@ -322,6 +350,7 @@ export class RelCalendar extends LitElement {
|
|
|
322
350
|
} else {
|
|
323
351
|
this.viewMonth--;
|
|
324
352
|
}
|
|
353
|
+
this.announceMonth();
|
|
325
354
|
}
|
|
326
355
|
|
|
327
356
|
private nextMonth() {
|
|
@@ -331,6 +360,7 @@ export class RelCalendar extends LitElement {
|
|
|
331
360
|
} else {
|
|
332
361
|
this.viewMonth++;
|
|
333
362
|
}
|
|
363
|
+
this.announceMonth();
|
|
334
364
|
}
|
|
335
365
|
|
|
336
366
|
private selectDay(day: CalendarDay) {
|
|
@@ -385,6 +415,7 @@ export class RelCalendar extends LitElement {
|
|
|
385
415
|
e.preventDefault();
|
|
386
416
|
if (e.shiftKey) {
|
|
387
417
|
this.viewYear--;
|
|
418
|
+
this.announceMonth();
|
|
388
419
|
} else {
|
|
389
420
|
this.previousMonth();
|
|
390
421
|
}
|
|
@@ -393,6 +424,7 @@ export class RelCalendar extends LitElement {
|
|
|
393
424
|
e.preventDefault();
|
|
394
425
|
if (e.shiftKey) {
|
|
395
426
|
this.viewYear++;
|
|
427
|
+
this.announceMonth();
|
|
396
428
|
} else {
|
|
397
429
|
this.nextMonth();
|
|
398
430
|
}
|
|
@@ -451,9 +483,13 @@ export class RelCalendar extends LitElement {
|
|
|
451
483
|
const days = this.getCalendarDays();
|
|
452
484
|
const weekdayLabels = this.getWeekdayLabels();
|
|
453
485
|
const rovingIndex = this.getRovingIndex(days);
|
|
486
|
+
const weeks: CalendarDay[][] = [];
|
|
487
|
+
for (let i = 0; i < days.length; i += 7) {
|
|
488
|
+
weeks.push(days.slice(i, i + 7));
|
|
489
|
+
}
|
|
454
490
|
|
|
455
491
|
return html`
|
|
456
|
-
<div
|
|
492
|
+
<div>
|
|
457
493
|
<div part="header" class="header">
|
|
458
494
|
<button
|
|
459
495
|
part="action" class="nav-button"
|
|
@@ -483,6 +519,7 @@ export class RelCalendar extends LitElement {
|
|
|
483
519
|
</button>
|
|
484
520
|
</div>
|
|
485
521
|
|
|
522
|
+
<div role="grid" aria-label="${this.getMonthName()} ${this.viewYear}">
|
|
486
523
|
<div part="list" class="calendar-grid" role="row">
|
|
487
524
|
${weekdayLabels.map(
|
|
488
525
|
(label) => html`
|
|
@@ -493,26 +530,35 @@ export class RelCalendar extends LitElement {
|
|
|
493
530
|
)}
|
|
494
531
|
</div>
|
|
495
532
|
|
|
496
|
-
<div part="list" class="calendar-grid">
|
|
497
|
-
${
|
|
498
|
-
(
|
|
499
|
-
<
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
533
|
+
<div part="list" class="calendar-grid" role="rowgroup">
|
|
534
|
+
${weeks.map(
|
|
535
|
+
(week, weekIndex) => html`
|
|
536
|
+
<div class="calendar-week" role="row">
|
|
537
|
+
${week.map((day, dayIndex) => {
|
|
538
|
+
const index = weekIndex * 7 + dayIndex;
|
|
539
|
+
return html`
|
|
540
|
+
<button
|
|
541
|
+
part="item" class="day-button ${day.isSelected ? "selected" : ""} ${day.isToday
|
|
542
|
+
? "today"
|
|
543
|
+
: ""} ${!day.isCurrentMonth ? "other-month" : ""} ${day.isWeekend ? "weekend" : ""}"
|
|
544
|
+
role="gridcell"
|
|
545
|
+
aria-label="${this.formatDayLabel(day.date)}"
|
|
546
|
+
aria-selected="${day.isSelected}"
|
|
547
|
+
aria-current=${day.isToday ? "date" : nothing}
|
|
548
|
+
?disabled=${day.isDisabled}
|
|
549
|
+
@click=${() => this.selectDay(day)}
|
|
550
|
+
@keydown=${(e: KeyboardEvent) => this.handleKeyDown(e, day, index)}
|
|
551
|
+
tabindex="${index === rovingIndex ? 0 : -1}"
|
|
552
|
+
>
|
|
553
|
+
${day.day}
|
|
554
|
+
</button>
|
|
555
|
+
`;
|
|
556
|
+
})}
|
|
557
|
+
</div>
|
|
513
558
|
`
|
|
514
559
|
)}
|
|
515
560
|
</div>
|
|
561
|
+
</div>
|
|
516
562
|
</div>
|
|
517
563
|
`;
|
|
518
564
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property, state, query } from "lit/decorators.js";
|
|
3
|
+
import { announce } from "../../utils/announce";
|
|
3
4
|
|
|
4
5
|
@customElement("rel-carousel")
|
|
5
6
|
export class RelCarousel extends LitElement {
|
|
@@ -41,6 +42,7 @@ export class RelCarousel extends LitElement {
|
|
|
41
42
|
// Number of clone slides prepended before the real slides
|
|
42
43
|
private clonesBefore = 0;
|
|
43
44
|
private _silentJumping = false;
|
|
45
|
+
private _autoplayTick = false;
|
|
44
46
|
private _ro?: ResizeObserver;
|
|
45
47
|
|
|
46
48
|
static styles = css`
|
|
@@ -307,6 +309,12 @@ export class RelCarousel extends LitElement {
|
|
|
307
309
|
const elements = slot.assignedElements();
|
|
308
310
|
this.slideCount = elements.length;
|
|
309
311
|
|
|
312
|
+
elements.forEach((el, i) => {
|
|
313
|
+
el.setAttribute("role", "group");
|
|
314
|
+
el.setAttribute("aria-roledescription", "slide");
|
|
315
|
+
el.setAttribute("aria-label", `${i + 1} of ${elements.length}`);
|
|
316
|
+
});
|
|
317
|
+
|
|
310
318
|
// Clamp value to valid real-slide range
|
|
311
319
|
if (this.value >= this.slideCount) this.value = Math.max(0, this.slideCount - 1);
|
|
312
320
|
if (this.value < 0) this.value = 0;
|
|
@@ -397,9 +405,14 @@ export class RelCarousel extends LitElement {
|
|
|
397
405
|
this.currentTranslate = translate;
|
|
398
406
|
this.prevTranslate = translate;
|
|
399
407
|
this.track!.style.transform = this.axisTransform(translate);
|
|
408
|
+
const fromAutoplay = this._autoplayTick;
|
|
400
409
|
this.track!.addEventListener(
|
|
401
410
|
"transitionend",
|
|
402
|
-
() =>
|
|
411
|
+
() => {
|
|
412
|
+
this._autoplayTick = fromAutoplay;
|
|
413
|
+
this.silentJump(this.slideCount - 1);
|
|
414
|
+
this._autoplayTick = false;
|
|
415
|
+
},
|
|
403
416
|
{ once: true }
|
|
404
417
|
);
|
|
405
418
|
}
|
|
@@ -419,9 +432,14 @@ export class RelCarousel extends LitElement {
|
|
|
419
432
|
this.currentTranslate = translate;
|
|
420
433
|
this.prevTranslate = translate;
|
|
421
434
|
this.track!.style.transform = this.axisTransform(translate);
|
|
435
|
+
const fromAutoplay = this._autoplayTick;
|
|
422
436
|
this.track!.addEventListener(
|
|
423
437
|
"transitionend",
|
|
424
|
-
() =>
|
|
438
|
+
() => {
|
|
439
|
+
this._autoplayTick = fromAutoplay;
|
|
440
|
+
this.silentJump(0);
|
|
441
|
+
this._autoplayTick = false;
|
|
442
|
+
},
|
|
425
443
|
{ once: true }
|
|
426
444
|
);
|
|
427
445
|
}
|
|
@@ -452,6 +470,9 @@ export class RelCarousel extends LitElement {
|
|
|
452
470
|
}
|
|
453
471
|
|
|
454
472
|
private emitChange() {
|
|
473
|
+
if (!this._autoplayTick && this.slideCount > 0) {
|
|
474
|
+
announce(`Slide ${this.value + 1} of ${this.slideCount}`);
|
|
475
|
+
}
|
|
455
476
|
this.dispatchEvent(
|
|
456
477
|
new CustomEvent("rel-change", {
|
|
457
478
|
detail: { value: this.value },
|
|
@@ -466,7 +487,9 @@ export class RelCarousel extends LitElement {
|
|
|
466
487
|
|
|
467
488
|
this.stopAutoplay();
|
|
468
489
|
this.autoplayTimer = window.setInterval(() => {
|
|
490
|
+
this._autoplayTick = true;
|
|
469
491
|
this.goToNext();
|
|
492
|
+
this._autoplayTick = false;
|
|
470
493
|
}, this.interval);
|
|
471
494
|
}
|
|
472
495
|
|
|
@@ -2,6 +2,7 @@ import { LitElement, html, css } from "lit";
|
|
|
2
2
|
import { customElement, property, state } from "lit/decorators.js";
|
|
3
3
|
import "../rel-avatar/rel-avatar.js";
|
|
4
4
|
import "@reliability-design/icons/rel-icon";
|
|
5
|
+
import { announce } from "../../utils/announce";
|
|
5
6
|
|
|
6
7
|
export type ChipTone = "neutral" | "danger" | "warning" | "success" | "info";
|
|
7
8
|
export type ChipVariant = "fill" | "subtle";
|
|
@@ -76,6 +77,13 @@ export class RelChip extends LitElement {
|
|
|
76
77
|
transition: background-color 0.15s ease;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
.chip-action {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: var(--rel-spacing-xs);
|
|
84
|
+
outline: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
/* ── Size ── */
|
|
80
88
|
:host([size="lg"]) .chip {
|
|
81
89
|
height: 32px;
|
|
@@ -262,7 +270,7 @@ export class RelChip extends LitElement {
|
|
|
262
270
|
outward by strokeWeight — outline-offset must be 0, not a positive
|
|
263
271
|
gap (a positive offset was the bug: it drew a visible gap that
|
|
264
272
|
doesn't exist in Figma). ── */
|
|
265
|
-
.chip:focus-visible {
|
|
273
|
+
.chip:has(.chip-action:focus-visible) {
|
|
266
274
|
outline: 1.5px solid var(--rel-chip-focus-ring, var(--rel-border-focus));
|
|
267
275
|
outline-offset: 1.5px;
|
|
268
276
|
box-shadow: 0 0 0 1.5px var(--rel-border-focus-inner);
|
|
@@ -363,6 +371,7 @@ export class RelChip extends LitElement {
|
|
|
363
371
|
composed: true,
|
|
364
372
|
})
|
|
365
373
|
);
|
|
374
|
+
announce(`${this.label} removed`);
|
|
366
375
|
}
|
|
367
376
|
|
|
368
377
|
/** The chip body is announced as a button, so it must also act on keys:
|
|
@@ -432,25 +441,27 @@ export class RelChip extends LitElement {
|
|
|
432
441
|
}
|
|
433
442
|
|
|
434
443
|
return html`
|
|
435
|
-
<span
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
444
|
+
<span part="base" class="${this.classes}">
|
|
445
|
+
<span
|
|
446
|
+
class="chip-action"
|
|
447
|
+
role="button"
|
|
448
|
+
tabindex="0"
|
|
449
|
+
aria-label="${this.label}"
|
|
450
|
+
@keydown=${this.handleKeydown}
|
|
451
|
+
>
|
|
452
|
+
${showAvatar
|
|
453
|
+
? html`<rel-avatar type="text" text=${this.avatar} size="xs"></rel-avatar>`
|
|
454
|
+
: null}
|
|
455
|
+
${showLeadingIcon
|
|
456
|
+
? html`<rel-icon part="icon" class="chip-icon" name="${this.leadingIcon}"></rel-icon>`
|
|
457
|
+
: null}
|
|
458
|
+
|
|
459
|
+
<span part="label" class="chip-label">${this.label}</span>
|
|
460
|
+
|
|
461
|
+
${this.trailingIcon
|
|
462
|
+
? html`<rel-icon class="chip-icon" name="${this.trailingIcon}"></rel-icon>`
|
|
463
|
+
: null}
|
|
464
|
+
</span>
|
|
454
465
|
${this.removable
|
|
455
466
|
? html`<button
|
|
456
467
|
class="chip-remove"
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
parseDateInput,
|
|
5
|
+
isDayFirstPattern,
|
|
6
|
+
isDayFirstLocale,
|
|
7
|
+
} from "./date-parse";
|
|
8
|
+
|
|
9
|
+
// Reference "today": 2026-07-21 (a Tuesday), noon to dodge TZ edges.
|
|
10
|
+
const ref = new Date(2026, 6, 21, 12, 0, 0);
|
|
11
|
+
const opts = { referenceDate: ref };
|
|
12
|
+
|
|
13
|
+
const ymd = (d: Date | null) =>
|
|
14
|
+
d === null
|
|
15
|
+
? null
|
|
16
|
+
: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(
|
|
17
|
+
d.getDate()
|
|
18
|
+
).padStart(2, "0")}`;
|
|
19
|
+
|
|
20
|
+
test("shortcuts: t / today / t±N / tN", () => {
|
|
21
|
+
assert.equal(ymd(parseDateInput("t", opts)), "2026-07-21");
|
|
22
|
+
assert.equal(ymd(parseDateInput("T", opts)), "2026-07-21");
|
|
23
|
+
assert.equal(ymd(parseDateInput("today", opts)), "2026-07-21");
|
|
24
|
+
assert.equal(ymd(parseDateInput("t+1", opts)), "2026-07-22");
|
|
25
|
+
assert.equal(ymd(parseDateInput("t1", opts)), "2026-07-22");
|
|
26
|
+
assert.equal(ymd(parseDateInput("T + 7", opts)), "2026-07-28");
|
|
27
|
+
assert.equal(ymd(parseDateInput("t-1", opts)), "2026-07-20");
|
|
28
|
+
assert.equal(ymd(parseDateInput("t-30", opts)), "2026-06-21");
|
|
29
|
+
// month/year rollover
|
|
30
|
+
assert.equal(ymd(parseDateInput("t+11", opts)), "2026-08-01");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("incomplete shortcut is not 'today'", () => {
|
|
34
|
+
assert.equal(parseDateInput("t+", opts), null);
|
|
35
|
+
assert.equal(parseDateInput("t-", opts), null);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("ISO and year-first forms", () => {
|
|
39
|
+
assert.equal(ymd(parseDateInput("2026-07-21", opts)), "2026-07-21");
|
|
40
|
+
assert.equal(ymd(parseDateInput("2026/7/21", opts)), "2026-07-21");
|
|
41
|
+
assert.equal(ymd(parseDateInput("2026.07.21", opts)), "2026-07-21");
|
|
42
|
+
assert.equal(ymd(parseDateInput("20260721", opts)), "2026-07-21");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("numeric M/D/Y vs D/M/Y via dayFirst", () => {
|
|
46
|
+
assert.equal(ymd(parseDateInput("7/21/2026", opts)), "2026-07-21");
|
|
47
|
+
assert.equal(
|
|
48
|
+
ymd(parseDateInput("21/7/2026", { ...opts, dayFirst: true })),
|
|
49
|
+
"2026-07-21"
|
|
50
|
+
);
|
|
51
|
+
// ambiguous "3/4": order decided by dayFirst
|
|
52
|
+
assert.equal(ymd(parseDateInput("3/4/2026", opts)), "2026-03-04");
|
|
53
|
+
assert.equal(
|
|
54
|
+
ymd(parseDateInput("3/4/2026", { ...opts, dayFirst: true })),
|
|
55
|
+
"2026-04-03"
|
|
56
|
+
);
|
|
57
|
+
// a part > 12 forces the order regardless of dayFirst
|
|
58
|
+
assert.equal(ymd(parseDateInput("21/7/2026", opts)), "2026-07-21");
|
|
59
|
+
assert.equal(ymd(parseDateInput("7/21/2026", { ...opts, dayFirst: true })), "2026-07-21");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("two-digit years map to 20YY", () => {
|
|
63
|
+
assert.equal(ymd(parseDateInput("7/21/26", opts)), "2026-07-21");
|
|
64
|
+
assert.equal(ymd(parseDateInput("21/7/26", { ...opts, dayFirst: true })), "2026-07-21");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("month names, long and short, any case", () => {
|
|
68
|
+
assert.equal(ymd(parseDateInput("21 Jul 2026", opts)), "2026-07-21");
|
|
69
|
+
assert.equal(ymd(parseDateInput("Jul 21 2026", opts)), "2026-07-21");
|
|
70
|
+
assert.equal(ymd(parseDateInput("July 21, 2026", opts)), "2026-07-21");
|
|
71
|
+
assert.equal(ymd(parseDateInput("21 december 2026", opts)), "2026-12-21");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("partial input fills from the reference date", () => {
|
|
75
|
+
assert.equal(ymd(parseDateInput("5", opts)), "2026-07-05"); // day in ref month
|
|
76
|
+
assert.equal(ymd(parseDateInput("21/7", { ...opts, dayFirst: true })), "2026-07-21");
|
|
77
|
+
assert.equal(ymd(parseDateInput("12/25", opts)), "2026-12-25");
|
|
78
|
+
assert.equal(ymd(parseDateInput("25 Dec", opts)), "2026-12-25");
|
|
79
|
+
assert.equal(ymd(parseDateInput("Dec 25", opts)), "2026-12-25");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("impossible dates rejected", () => {
|
|
83
|
+
assert.equal(parseDateInput("2026-02-30", opts), null);
|
|
84
|
+
assert.equal(parseDateInput("31/4/2026", { ...opts, dayFirst: true }), null);
|
|
85
|
+
assert.equal(parseDateInput("0", opts), null);
|
|
86
|
+
assert.equal(parseDateInput("32", opts), null);
|
|
87
|
+
assert.equal(parseDateInput("13/13/2026", opts), null);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("garbage rejected", () => {
|
|
91
|
+
assert.equal(parseDateInput("", opts), null);
|
|
92
|
+
assert.equal(parseDateInput("asdf", opts), null);
|
|
93
|
+
assert.equal(parseDateInput("1/2/3/4", opts), null);
|
|
94
|
+
assert.equal(parseDateInput("Jul", opts), null);
|
|
95
|
+
assert.equal(parseDateInput("tomorrow+1", opts), null);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("leap day handling", () => {
|
|
99
|
+
assert.equal(ymd(parseDateInput("2028-02-29", opts)), "2028-02-29");
|
|
100
|
+
assert.equal(parseDateInput("2026-02-29", opts), null);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("isDayFirstPattern", () => {
|
|
104
|
+
assert.equal(isDayFirstPattern("DD/MM/YYYY"), true);
|
|
105
|
+
assert.equal(isDayFirstPattern("MM/DD/YYYY"), false);
|
|
106
|
+
assert.equal(isDayFirstPattern("YYYY-MM-DD"), false);
|
|
107
|
+
assert.equal(isDayFirstPattern("D MMM YYYY"), true);
|
|
108
|
+
assert.equal(isDayFirstPattern("[Due] MM/DD"), false);
|
|
109
|
+
assert.equal(isDayFirstPattern("YYYY"), null);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("isDayFirstLocale", () => {
|
|
113
|
+
assert.equal(isDayFirstLocale("en-US"), false);
|
|
114
|
+
assert.equal(isDayFirstLocale("en-GB"), true);
|
|
115
|
+
assert.equal(isDayFirstLocale("de-DE"), true);
|
|
116
|
+
});
|