@reliability-design/web 1.4.4 → 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-accordion/rel-accordion.ts +13 -1
- 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-button/rel-button.ts +18 -2
- package/dist/components/rel-calendar/rel-calendar.ts +86 -18
- package/dist/components/rel-carousel/rel-carousel.ts +45 -2
- package/dist/components/rel-checkbox/rel-checkbox.ts +4 -1
- package/dist/components/rel-chip/rel-chip.ts +59 -19
- 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 +675 -93
- package/dist/components/rel-drawer/rel-drawer.ts +28 -61
- package/dist/components/rel-menu/rel-menu.ts +130 -15
- package/dist/components/rel-menubar/rel-menubar.ts +221 -63
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +39 -3
- package/dist/components/rel-otp/rel-otp.ts +14 -4
- package/dist/components/rel-pagination/rel-pagination.ts +13 -0
- package/dist/components/rel-popover/rel-popover.ts +64 -5
- 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 +581 -91
- package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +55 -13
- package/dist/components/rel-select/rel-select.ts +203 -24
- package/dist/components/rel-sheet/rel-sheet.ts +184 -18
- package/dist/components/rel-sidebar/rel-sidebar.ts +50 -40
- 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 +47 -2
- package/dist/components/rel-switch/rel-switch.ts +3 -1
- package/dist/components/rel-tab/rel-tab.ts +32 -7
- package/dist/components/rel-textfield/rel-textfield.ts +263 -75
- package/dist/components/rel-time-picker/rel-time-picker.ts +749 -261
- 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 +33 -0
- package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-item.ts +318 -319
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-profile.ts +224 -224
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar.ts +440 -440
- package/dist/components/rel-upload/rel-upload.ts +25 -0
- package/dist/index.d.mts +461 -23
- package/dist/index.d.ts +461 -23
- package/dist/index.js +3906 -807
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4143 -1055
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -270,6 +270,9 @@ export class RelAccordion extends LitElement {
|
|
|
270
270
|
}
|
|
271
271
|
`;
|
|
272
272
|
|
|
273
|
+
/** Stable id prefix wiring the header to its panel. */
|
|
274
|
+
private accordionId = `rel-accordion-${Math.random().toString(36).slice(2)}`;
|
|
275
|
+
|
|
273
276
|
private toggle() {
|
|
274
277
|
this.open = !this.open;
|
|
275
278
|
this.dispatchEvent(
|
|
@@ -286,9 +289,11 @@ export class RelAccordion extends LitElement {
|
|
|
286
289
|
<div part="base" class="accordion">
|
|
287
290
|
<div
|
|
288
291
|
part="trigger" class="header"
|
|
292
|
+
id="${this.accordionId}-header"
|
|
289
293
|
role="button"
|
|
290
294
|
tabindex="0"
|
|
291
295
|
aria-expanded="${this.open}"
|
|
296
|
+
aria-controls="${this.accordionId}-body"
|
|
292
297
|
@click=${this.toggle}
|
|
293
298
|
@keydown=${(e: KeyboardEvent) => {
|
|
294
299
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -315,7 +320,14 @@ export class RelAccordion extends LitElement {
|
|
|
315
320
|
</slot>
|
|
316
321
|
</div>
|
|
317
322
|
</div>
|
|
318
|
-
<div
|
|
323
|
+
<div
|
|
324
|
+
part="content"
|
|
325
|
+
class="body ${this.open ? "open" : "closed"}"
|
|
326
|
+
id="${this.accordionId}-body"
|
|
327
|
+
role="region"
|
|
328
|
+
aria-labelledby="${this.accordionId}-header"
|
|
329
|
+
?inert=${!this.open}
|
|
330
|
+
>
|
|
319
331
|
<slot></slot>
|
|
320
332
|
</div>
|
|
321
333
|
</div>
|
|
@@ -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
|
`;
|
|
@@ -137,9 +137,12 @@ export class RelButton extends LitElement {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/* Sizes
|
|
140
|
+
/* Sizes. Min-widths come from the Figma Button master (S/M/L =
|
|
141
|
+
80/100/120) so short labels like "OK" keep a proper pill shape;
|
|
142
|
+
--rel-button-min-width lets a consumer override per instance. */
|
|
141
143
|
.size-small {
|
|
142
144
|
height: 32px;
|
|
145
|
+
min-width: var(--rel-button-min-width, 80px);
|
|
143
146
|
padding: 0 var(--rel-spacing-sm);
|
|
144
147
|
font-family: var(--rel-action-md-font-family);
|
|
145
148
|
font-weight: var(--rel-action-md-font-weight);
|
|
@@ -150,6 +153,7 @@ export class RelButton extends LitElement {
|
|
|
150
153
|
|
|
151
154
|
.size-medium {
|
|
152
155
|
height: 40px;
|
|
156
|
+
min-width: var(--rel-button-min-width, 100px);
|
|
153
157
|
gap: var(--rel-spacing-xs);
|
|
154
158
|
padding: 0 var(--rel-spacing-md);
|
|
155
159
|
font-family: var(--rel-action-lg-font-family);
|
|
@@ -161,6 +165,7 @@ export class RelButton extends LitElement {
|
|
|
161
165
|
|
|
162
166
|
.size-large {
|
|
163
167
|
height: 48px;
|
|
168
|
+
min-width: var(--rel-button-min-width, 120px);
|
|
164
169
|
padding: 0 var(--rel-spacing-lg);
|
|
165
170
|
font-family: var(--rel-action-xl-font-family);
|
|
166
171
|
font-weight: var(--rel-action-xl-font-weight);
|
|
@@ -267,6 +272,11 @@ export class RelButton extends LitElement {
|
|
|
267
272
|
color: var(--rel-action-neutral-label-icons-ghost);
|
|
268
273
|
}
|
|
269
274
|
|
|
275
|
+
/* Figma master: ghost S has a narrower 56px min than primary/outline. */
|
|
276
|
+
.btn-text.size-small {
|
|
277
|
+
min-width: var(--rel-button-min-width, 56px);
|
|
278
|
+
}
|
|
279
|
+
|
|
270
280
|
.btn-text.tone-critical {
|
|
271
281
|
color: var(--rel-action-danger-label-icons-ghost);
|
|
272
282
|
}
|
|
@@ -275,6 +285,8 @@ export class RelButton extends LitElement {
|
|
|
275
285
|
background: transparent;
|
|
276
286
|
color: var(--rel-action-neutral-label-icons-link);
|
|
277
287
|
text-decoration: underline;
|
|
288
|
+
/* Links are inline actions — no minimum footprint in the Figma master. */
|
|
289
|
+
min-width: 0;
|
|
278
290
|
}
|
|
279
291
|
|
|
280
292
|
button.btn-link:hover:not(:disabled) {
|
|
@@ -303,6 +315,7 @@ export class RelButton extends LitElement {
|
|
|
303
315
|
/* Icon Only as an independent axis: square button of the current style */
|
|
304
316
|
.btn-icon-only {
|
|
305
317
|
padding: 0;
|
|
318
|
+
min-width: 0;
|
|
306
319
|
}
|
|
307
320
|
|
|
308
321
|
.btn-icon-only.size-small {
|
|
@@ -321,6 +334,7 @@ export class RelButton extends LitElement {
|
|
|
321
334
|
width: 40px;
|
|
322
335
|
height: 40px;
|
|
323
336
|
padding: 0;
|
|
337
|
+
min-width: 0;
|
|
324
338
|
border-radius: 50%;
|
|
325
339
|
}
|
|
326
340
|
.btn-icon.tone-default {
|
|
@@ -365,7 +379,9 @@ export class RelButton extends LitElement {
|
|
|
365
379
|
.btn-text: ghost gets tone-accent hovers below (this generic grey overlay
|
|
366
380
|
has higher specificity than the per-tone ghost rules and was overriding
|
|
367
381
|
them, which is why every ghost hover looked grey). */
|
|
368
|
-
|
|
382
|
+
/* Link buttons change label color only (rule above) — the overlay fill
|
|
383
|
+
here would make a hovered link read as a ghost button. */
|
|
384
|
+
button:hover:not(:disabled):not(.btn-primary):not(.btn-default):not(.btn-icon):not(.btn-secondary):not(.btn-static):not(.btn-text):not(.btn-link) {
|
|
369
385
|
background: var(--rel-action-overlay-hover);
|
|
370
386
|
}
|
|
371
387
|
|
|
@@ -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
|
}
|
|
@@ -426,12 +458,38 @@ export class RelCalendar extends LitElement {
|
|
|
426
458
|
}
|
|
427
459
|
}
|
|
428
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Index of the single grid cell carrying tabindex="0". The old expression
|
|
463
|
+
* (`isSelected || (index === 0 && !focusedDate)`) left the grid with ZERO
|
|
464
|
+
* tab stops once the user arrowed around without a selection — Tab could
|
|
465
|
+
* never re-enter the calendar.
|
|
466
|
+
*/
|
|
467
|
+
private getRovingIndex(days: CalendarDay[]): number {
|
|
468
|
+
const candidates = [
|
|
469
|
+
this.focusedDate
|
|
470
|
+
? days.findIndex(
|
|
471
|
+
(d) => d.isCurrentMonth && this.isSameDay(d.date, this.focusedDate!)
|
|
472
|
+
)
|
|
473
|
+
: -1,
|
|
474
|
+
days.findIndex((d) => d.isSelected),
|
|
475
|
+
days.findIndex((d) => d.isCurrentMonth && d.isToday),
|
|
476
|
+
days.findIndex((d) => d.isCurrentMonth && !d.isDisabled),
|
|
477
|
+
];
|
|
478
|
+
const found = candidates.find((i) => i >= 0);
|
|
479
|
+
return found ?? 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
429
482
|
render() {
|
|
430
483
|
const days = this.getCalendarDays();
|
|
431
484
|
const weekdayLabels = this.getWeekdayLabels();
|
|
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
|
+
}
|
|
432
490
|
|
|
433
491
|
return html`
|
|
434
|
-
<div
|
|
492
|
+
<div>
|
|
435
493
|
<div part="header" class="header">
|
|
436
494
|
<button
|
|
437
495
|
part="action" class="nav-button"
|
|
@@ -461,6 +519,7 @@ export class RelCalendar extends LitElement {
|
|
|
461
519
|
</button>
|
|
462
520
|
</div>
|
|
463
521
|
|
|
522
|
+
<div role="grid" aria-label="${this.getMonthName()} ${this.viewYear}">
|
|
464
523
|
<div part="list" class="calendar-grid" role="row">
|
|
465
524
|
${weekdayLabels.map(
|
|
466
525
|
(label) => html`
|
|
@@ -471,26 +530,35 @@ export class RelCalendar extends LitElement {
|
|
|
471
530
|
)}
|
|
472
531
|
</div>
|
|
473
532
|
|
|
474
|
-
<div part="list" class="calendar-grid">
|
|
475
|
-
${
|
|
476
|
-
(
|
|
477
|
-
<
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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>
|
|
491
558
|
`
|
|
492
559
|
)}
|
|
493
560
|
</div>
|
|
561
|
+
</div>
|
|
494
562
|
</div>
|
|
495
563
|
`;
|
|
496
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`
|
|
@@ -251,6 +253,11 @@ export class RelCarousel extends LitElement {
|
|
|
251
253
|
"(prefers-reduced-motion: reduce)"
|
|
252
254
|
).matches;
|
|
253
255
|
|
|
256
|
+
// WCAG 2.2.2: auto-advance must pause while keyboard focus is inside the
|
|
257
|
+
// carousel, not just on mouse hover.
|
|
258
|
+
this.addEventListener("focusin", this.handleFocusIn);
|
|
259
|
+
this.addEventListener("focusout", this.handleFocusOut);
|
|
260
|
+
|
|
254
261
|
if (this.autoplay) {
|
|
255
262
|
this.startAutoplay();
|
|
256
263
|
}
|
|
@@ -285,6 +292,8 @@ export class RelCarousel extends LitElement {
|
|
|
285
292
|
|
|
286
293
|
disconnectedCallback() {
|
|
287
294
|
super.disconnectedCallback();
|
|
295
|
+
this.removeEventListener("focusin", this.handleFocusIn);
|
|
296
|
+
this.removeEventListener("focusout", this.handleFocusOut);
|
|
288
297
|
this._ro?.disconnect();
|
|
289
298
|
this.stopAutoplay();
|
|
290
299
|
if (this.animationId) {
|
|
@@ -300,6 +309,12 @@ export class RelCarousel extends LitElement {
|
|
|
300
309
|
const elements = slot.assignedElements();
|
|
301
310
|
this.slideCount = elements.length;
|
|
302
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
|
+
|
|
303
318
|
// Clamp value to valid real-slide range
|
|
304
319
|
if (this.value >= this.slideCount) this.value = Math.max(0, this.slideCount - 1);
|
|
305
320
|
if (this.value < 0) this.value = 0;
|
|
@@ -390,9 +405,14 @@ export class RelCarousel extends LitElement {
|
|
|
390
405
|
this.currentTranslate = translate;
|
|
391
406
|
this.prevTranslate = translate;
|
|
392
407
|
this.track!.style.transform = this.axisTransform(translate);
|
|
408
|
+
const fromAutoplay = this._autoplayTick;
|
|
393
409
|
this.track!.addEventListener(
|
|
394
410
|
"transitionend",
|
|
395
|
-
() =>
|
|
411
|
+
() => {
|
|
412
|
+
this._autoplayTick = fromAutoplay;
|
|
413
|
+
this.silentJump(this.slideCount - 1);
|
|
414
|
+
this._autoplayTick = false;
|
|
415
|
+
},
|
|
396
416
|
{ once: true }
|
|
397
417
|
);
|
|
398
418
|
}
|
|
@@ -412,9 +432,14 @@ export class RelCarousel extends LitElement {
|
|
|
412
432
|
this.currentTranslate = translate;
|
|
413
433
|
this.prevTranslate = translate;
|
|
414
434
|
this.track!.style.transform = this.axisTransform(translate);
|
|
435
|
+
const fromAutoplay = this._autoplayTick;
|
|
415
436
|
this.track!.addEventListener(
|
|
416
437
|
"transitionend",
|
|
417
|
-
() =>
|
|
438
|
+
() => {
|
|
439
|
+
this._autoplayTick = fromAutoplay;
|
|
440
|
+
this.silentJump(0);
|
|
441
|
+
this._autoplayTick = false;
|
|
442
|
+
},
|
|
418
443
|
{ once: true }
|
|
419
444
|
);
|
|
420
445
|
}
|
|
@@ -445,6 +470,9 @@ export class RelCarousel extends LitElement {
|
|
|
445
470
|
}
|
|
446
471
|
|
|
447
472
|
private emitChange() {
|
|
473
|
+
if (!this._autoplayTick && this.slideCount > 0) {
|
|
474
|
+
announce(`Slide ${this.value + 1} of ${this.slideCount}`);
|
|
475
|
+
}
|
|
448
476
|
this.dispatchEvent(
|
|
449
477
|
new CustomEvent("rel-change", {
|
|
450
478
|
detail: { value: this.value },
|
|
@@ -459,7 +487,9 @@ export class RelCarousel extends LitElement {
|
|
|
459
487
|
|
|
460
488
|
this.stopAutoplay();
|
|
461
489
|
this.autoplayTimer = window.setInterval(() => {
|
|
490
|
+
this._autoplayTick = true;
|
|
462
491
|
this.goToNext();
|
|
492
|
+
this._autoplayTick = false;
|
|
463
493
|
}, this.interval);
|
|
464
494
|
}
|
|
465
495
|
|
|
@@ -556,6 +586,18 @@ export class RelCarousel extends LitElement {
|
|
|
556
586
|
}
|
|
557
587
|
}
|
|
558
588
|
|
|
589
|
+
private handleFocusIn = () => {
|
|
590
|
+
if (this.autoplay) this.stopAutoplay();
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
private handleFocusOut = (e: FocusEvent) => {
|
|
594
|
+
if (!this.autoplay) return;
|
|
595
|
+
const next = e.relatedTarget as Node | null;
|
|
596
|
+
// Resume only once focus has fully left the carousel (light or shadow).
|
|
597
|
+
if (next && (this.contains(next) || this.shadowRoot?.contains(next))) return;
|
|
598
|
+
this.startAutoplay();
|
|
599
|
+
};
|
|
600
|
+
|
|
559
601
|
private handleMouseEnter() {
|
|
560
602
|
if (this.autoplay && this.pauseOnHover) {
|
|
561
603
|
this.stopAutoplay();
|
|
@@ -667,6 +709,7 @@ export class RelCarousel extends LitElement {
|
|
|
667
709
|
class="dot ${i === this.value ? "active" : ""}"
|
|
668
710
|
part="dot"
|
|
669
711
|
aria-label="Go to slide ${i + 1}"
|
|
712
|
+
aria-current="${i === this.value ? "true" : nothing}"
|
|
670
713
|
@click=${() => this.goToSlide(i)}
|
|
671
714
|
></button>
|
|
672
715
|
`)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LitElement, html, css, type PropertyValues } from "lit";
|
|
1
|
+
import { LitElement, html, css, nothing, type PropertyValues } from "lit";
|
|
2
2
|
import { customElement, property } from "lit/decorators.js";
|
|
3
3
|
import "@reliability-design/icons/rel-icon";
|
|
4
4
|
|
|
@@ -334,6 +334,9 @@ export class RelCheckbox extends LitElement {
|
|
|
334
334
|
<label class="row">
|
|
335
335
|
<input
|
|
336
336
|
type="checkbox"
|
|
337
|
+
.checked=${this.status === "checked"}
|
|
338
|
+
.indeterminate=${this.status === "indeterminate"}
|
|
339
|
+
aria-checked="${this.status === "indeterminate" ? "mixed" : nothing}"
|
|
337
340
|
@click=${this.toggle}
|
|
338
341
|
@blur=${() => this._onTouched()}
|
|
339
342
|
?disabled=${this.state === "disabled"}
|
|
@@ -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,8 +371,37 @@ export class RelChip extends LitElement {
|
|
|
363
371
|
composed: true,
|
|
364
372
|
})
|
|
365
373
|
);
|
|
374
|
+
announce(`${this.label} removed`);
|
|
366
375
|
}
|
|
367
376
|
|
|
377
|
+
/** The chip body is announced as a button, so it must also act on keys:
|
|
378
|
+
* Enter/Space re-dispatch as a host click (consumers' @click handlers
|
|
379
|
+
* fire), Delete/Backspace remove when removable, and arrow keys ask the
|
|
380
|
+
* container to reorder a draggable chip (`chip-move`) — the keyboard
|
|
381
|
+
* alternative to pointer drag-and-drop. */
|
|
382
|
+
private handleKeydown = (e: KeyboardEvent) => {
|
|
383
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
384
|
+
e.preventDefault();
|
|
385
|
+
this.click();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if ((e.key === "Delete" || e.key === "Backspace") && this.removable) {
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
this.handleRemove(e);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (this.draggableChip && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
this.dispatchEvent(
|
|
396
|
+
new CustomEvent("chip-move", {
|
|
397
|
+
detail: { direction: e.key === "ArrowLeft" ? -1 : 1 },
|
|
398
|
+
bubbles: true,
|
|
399
|
+
composed: true,
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
368
405
|
private onDragStart = (e: DragEvent) => {
|
|
369
406
|
if (!this.draggableChip) return;
|
|
370
407
|
this.dragging = true;
|
|
@@ -404,24 +441,27 @@ export class RelChip extends LitElement {
|
|
|
404
441
|
}
|
|
405
442
|
|
|
406
443
|
return html`
|
|
407
|
-
<span
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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>
|
|
425
465
|
${this.removable
|
|
426
466
|
? html`<button
|
|
427
467
|
class="chip-remove"
|