@reliability-design/web 1.4.3 → 1.4.5
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-autocomplete/README.md +1 -0
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +56 -1
- package/dist/components/rel-button/rel-button.ts +18 -2
- package/dist/components/rel-calendar/rel-calendar.ts +23 -1
- package/dist/components/rel-carousel/rel-carousel.ts +20 -0
- package/dist/components/rel-checkbox/README.md +69 -22
- package/dist/components/rel-checkbox/rel-checkbox.ts +119 -9
- package/dist/components/rel-chip/rel-chip.ts +29 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +156 -13
- package/dist/components/rel-drawer/rel-drawer.ts +22 -58
- package/dist/components/rel-menu/rel-menu.ts +124 -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 +3 -3
- package/dist/components/rel-pagination/README.md +14 -0
- package/dist/components/rel-pagination/rel-pagination.ts +30 -14
- package/dist/components/rel-popover/rel-popover.ts +31 -3
- package/dist/components/rel-range-picker/rel-range-picker.ts +251 -3
- package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +50 -12
- package/dist/components/rel-select/README.md +1 -0
- package/dist/components/rel-select/rel-select.ts +1385 -1170
- package/dist/components/rel-sheet/rel-sheet.ts +22 -1
- package/dist/components/rel-sidebar/rel-sidebar.ts +43 -39
- package/dist/components/rel-stepper/rel-stepper.ts +44 -1
- package/dist/components/rel-tab/rel-tab.ts +32 -7
- package/dist/components/rel-time-picker/rel-time-picker.ts +338 -206
- package/dist/components/rel-tooltip/rel-tooltip.ts +30 -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/UPLOAD-DESIGN-CHANGES.md +134 -0
- package/dist/components/rel-upload/rel-upload-actions.ts +163 -0
- package/dist/components/rel-upload/rel-upload.ts +943 -0
- package/dist/index.d.mts +491 -18
- package/dist/index.d.ts +491 -18
- package/dist/index.js +2748 -410
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2953 -627
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -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>
|
|
@@ -94,3 +94,4 @@ Two data modes:
|
|
|
94
94
|
- Form validation is *displayed* (`error`, `error-message`, `required`), not performed — validity logic belongs to the form layer (e.g. Angular validators)
|
|
95
95
|
- CSS parts: `panel`, `list`, `option`, plus re-exported textfield parts (`label`, `control`, `input`, `helper`, `right-icon`)
|
|
96
96
|
- Panel styling matches `rel-select`'s listbox; override via `--rel-autocomplete-bg`, `--rel-autocomplete-menu-border`, `--rel-autocomplete-menu-radius`, `--rel-autocomplete-menu-shadow`, `--rel-autocomplete-option-hover`
|
|
97
|
+
- The open panel is promoted to the browser **top layer** (native Popover API, `popover="manual"`), so it renders correctly inside transformed / `overflow: hidden` containers — a centered `rel-modal`, a bottom `rel-drawer`. Engines without the Popover API fall back to `position: fixed` with a measure-and-cancel correction for transformed ancestors
|
|
@@ -143,6 +143,15 @@ export class RelAutocomplete extends LitElement {
|
|
|
143
143
|
position: fixed;
|
|
144
144
|
z-index: 1001;
|
|
145
145
|
display: none;
|
|
146
|
+
/* Reset native [popover] UA styles (the panel is promoted to the
|
|
147
|
+
top layer so transformed ancestors — e.g. a centered rel-modal
|
|
148
|
+
panel — can't hijack its containing block or clip it). */
|
|
149
|
+
inset: auto;
|
|
150
|
+
margin: 0;
|
|
151
|
+
padding: 0;
|
|
152
|
+
width: auto;
|
|
153
|
+
height: auto;
|
|
154
|
+
color: inherit;
|
|
146
155
|
box-sizing: border-box;
|
|
147
156
|
background: var(--rel-autocomplete-bg, var(--rel-bg-neutral));
|
|
148
157
|
border: 1px solid var(--rel-autocomplete-menu-border, var(--rel-border-default));
|
|
@@ -222,6 +231,7 @@ export class RelAutocomplete extends LitElement {
|
|
|
222
231
|
disconnectedCallback() {
|
|
223
232
|
super.disconnectedCallback();
|
|
224
233
|
this.removeGlobalListeners();
|
|
234
|
+
this.hideNativePopover();
|
|
225
235
|
clearTimeout(this.debounceTimer);
|
|
226
236
|
}
|
|
227
237
|
|
|
@@ -309,7 +319,10 @@ export class RelAutocomplete extends LitElement {
|
|
|
309
319
|
this.open = true;
|
|
310
320
|
this.highlightedIndex = -1;
|
|
311
321
|
this.addGlobalListeners();
|
|
312
|
-
requestAnimationFrame(() =>
|
|
322
|
+
requestAnimationFrame(() => {
|
|
323
|
+
this.showNativePopover();
|
|
324
|
+
this.updatePosition();
|
|
325
|
+
});
|
|
313
326
|
}
|
|
314
327
|
|
|
315
328
|
private closePanel() {
|
|
@@ -317,6 +330,35 @@ export class RelAutocomplete extends LitElement {
|
|
|
317
330
|
this.open = false;
|
|
318
331
|
this.highlightedIndex = -1;
|
|
319
332
|
this.removeGlobalListeners();
|
|
333
|
+
this.hideNativePopover();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* A transformed ancestor (e.g. rel-modal's centered panel with
|
|
337
|
+
* translate(-50%,-50%)) becomes the containing block for position:fixed
|
|
338
|
+
* descendants, breaking viewport-based coordinates and letting the
|
|
339
|
+
* panel's overflow:hidden clip the suggestions. Promoting the panel to
|
|
340
|
+
* the browser top layer sidesteps both — same fix as rel-popover.
|
|
341
|
+
* `manual` keeps light-dismiss and Escape handling under this
|
|
342
|
+
* component's own control. */
|
|
343
|
+
|
|
344
|
+
private showNativePopover() {
|
|
345
|
+
const p = this.panelEl;
|
|
346
|
+
if (!p || typeof p.showPopover !== "function") return;
|
|
347
|
+
try {
|
|
348
|
+
if (!p.matches(":popover-open")) p.showPopover();
|
|
349
|
+
} catch {
|
|
350
|
+
/* older engines / detached element — fixed-position fallback applies */
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private hideNativePopover() {
|
|
355
|
+
const p = this.panelEl;
|
|
356
|
+
if (!p || typeof p.hidePopover !== "function") return;
|
|
357
|
+
try {
|
|
358
|
+
if (p.matches(":popover-open")) p.hidePopover();
|
|
359
|
+
} catch {
|
|
360
|
+
/* already hidden or unsupported — nothing to do */
|
|
361
|
+
}
|
|
320
362
|
}
|
|
321
363
|
|
|
322
364
|
private updatePosition = () => {
|
|
@@ -337,6 +379,18 @@ export class RelAutocomplete extends LitElement {
|
|
|
337
379
|
left = Math.max(8, window.innerWidth - t.width - 8);
|
|
338
380
|
}
|
|
339
381
|
|
|
382
|
+
// Without top-layer support a transformed ancestor may be the containing
|
|
383
|
+
// block for position:fixed, shifting the panel away from the viewport
|
|
384
|
+
// coordinates computed above. Apply, re-measure, and cancel the offset.
|
|
385
|
+
this.panelEl.style.top = `${top}px`;
|
|
386
|
+
this.panelEl.style.left = `${left}px`;
|
|
387
|
+
this.panelEl.style.width = `${t.width}px`;
|
|
388
|
+
const applied = this.panelEl.getBoundingClientRect();
|
|
389
|
+
top -= applied.top - top;
|
|
390
|
+
left -= applied.left - left;
|
|
391
|
+
this.panelEl.style.top = `${top}px`;
|
|
392
|
+
this.panelEl.style.left = `${left}px`;
|
|
393
|
+
|
|
340
394
|
this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
|
|
341
395
|
};
|
|
342
396
|
|
|
@@ -583,6 +637,7 @@ export class RelAutocomplete extends LitElement {
|
|
|
583
637
|
id="${panelId}"
|
|
584
638
|
part="panel list"
|
|
585
639
|
class="ac-panel"
|
|
640
|
+
popover="manual"
|
|
586
641
|
role="listbox"
|
|
587
642
|
aria-label="${this.accessibleName || "Suggestions"}"
|
|
588
643
|
style="${this.panelStyle}"
|
|
@@ -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
|
|
|
@@ -426,9 +426,31 @@ export class RelCalendar extends LitElement {
|
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Index of the single grid cell carrying tabindex="0". The old expression
|
|
431
|
+
* (`isSelected || (index === 0 && !focusedDate)`) left the grid with ZERO
|
|
432
|
+
* tab stops once the user arrowed around without a selection — Tab could
|
|
433
|
+
* never re-enter the calendar.
|
|
434
|
+
*/
|
|
435
|
+
private getRovingIndex(days: CalendarDay[]): number {
|
|
436
|
+
const candidates = [
|
|
437
|
+
this.focusedDate
|
|
438
|
+
? days.findIndex(
|
|
439
|
+
(d) => d.isCurrentMonth && this.isSameDay(d.date, this.focusedDate!)
|
|
440
|
+
)
|
|
441
|
+
: -1,
|
|
442
|
+
days.findIndex((d) => d.isSelected),
|
|
443
|
+
days.findIndex((d) => d.isCurrentMonth && d.isToday),
|
|
444
|
+
days.findIndex((d) => d.isCurrentMonth && !d.isDisabled),
|
|
445
|
+
];
|
|
446
|
+
const found = candidates.find((i) => i >= 0);
|
|
447
|
+
return found ?? 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
429
450
|
render() {
|
|
430
451
|
const days = this.getCalendarDays();
|
|
431
452
|
const weekdayLabels = this.getWeekdayLabels();
|
|
453
|
+
const rovingIndex = this.getRovingIndex(days);
|
|
432
454
|
|
|
433
455
|
return html`
|
|
434
456
|
<div role="grid" aria-label="Calendar">
|
|
@@ -484,7 +506,7 @@ export class RelCalendar extends LitElement {
|
|
|
484
506
|
?disabled=${day.isDisabled}
|
|
485
507
|
@click=${() => this.selectDay(day)}
|
|
486
508
|
@keydown=${(e: KeyboardEvent) => this.handleKeyDown(e, day, index)}
|
|
487
|
-
tabindex="${
|
|
509
|
+
tabindex="${index === rovingIndex ? 0 : -1}"
|
|
488
510
|
>
|
|
489
511
|
${day.day}
|
|
490
512
|
</button>
|
|
@@ -251,6 +251,11 @@ export class RelCarousel extends LitElement {
|
|
|
251
251
|
"(prefers-reduced-motion: reduce)"
|
|
252
252
|
).matches;
|
|
253
253
|
|
|
254
|
+
// WCAG 2.2.2: auto-advance must pause while keyboard focus is inside the
|
|
255
|
+
// carousel, not just on mouse hover.
|
|
256
|
+
this.addEventListener("focusin", this.handleFocusIn);
|
|
257
|
+
this.addEventListener("focusout", this.handleFocusOut);
|
|
258
|
+
|
|
254
259
|
if (this.autoplay) {
|
|
255
260
|
this.startAutoplay();
|
|
256
261
|
}
|
|
@@ -285,6 +290,8 @@ export class RelCarousel extends LitElement {
|
|
|
285
290
|
|
|
286
291
|
disconnectedCallback() {
|
|
287
292
|
super.disconnectedCallback();
|
|
293
|
+
this.removeEventListener("focusin", this.handleFocusIn);
|
|
294
|
+
this.removeEventListener("focusout", this.handleFocusOut);
|
|
288
295
|
this._ro?.disconnect();
|
|
289
296
|
this.stopAutoplay();
|
|
290
297
|
if (this.animationId) {
|
|
@@ -556,6 +563,18 @@ export class RelCarousel extends LitElement {
|
|
|
556
563
|
}
|
|
557
564
|
}
|
|
558
565
|
|
|
566
|
+
private handleFocusIn = () => {
|
|
567
|
+
if (this.autoplay) this.stopAutoplay();
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
private handleFocusOut = (e: FocusEvent) => {
|
|
571
|
+
if (!this.autoplay) return;
|
|
572
|
+
const next = e.relatedTarget as Node | null;
|
|
573
|
+
// Resume only once focus has fully left the carousel (light or shadow).
|
|
574
|
+
if (next && (this.contains(next) || this.shadowRoot?.contains(next))) return;
|
|
575
|
+
this.startAutoplay();
|
|
576
|
+
};
|
|
577
|
+
|
|
559
578
|
private handleMouseEnter() {
|
|
560
579
|
if (this.autoplay && this.pauseOnHover) {
|
|
561
580
|
this.stopAutoplay();
|
|
@@ -667,6 +686,7 @@ export class RelCarousel extends LitElement {
|
|
|
667
686
|
class="dot ${i === this.value ? "active" : ""}"
|
|
668
687
|
part="dot"
|
|
669
688
|
aria-label="Go to slide ${i + 1}"
|
|
689
|
+
aria-current="${i === this.value ? "true" : nothing}"
|
|
670
690
|
@click=${() => this.goToSlide(i)}
|
|
671
691
|
></button>
|
|
672
692
|
`)
|
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
# rel-checkbox
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
|
-
A tri-state checkbox component supporting checked, unchecked, and indeterminate states. Includes label
|
|
4
|
+
A tri-state checkbox component supporting checked, unchecked, and indeterminate states. Includes label, optional supporting text, and optional label prefix icon, with built-in error and disabled states. Supports two sizes, two densities, and leading/trailing box alignment — matching the Figma Checkbox sheet vocabulary (Size, Density, Alignment) verbatim.
|
|
5
5
|
|
|
6
6
|
## Usage
|
|
7
7
|
```html
|
|
8
8
|
<!-- Basic example -->
|
|
9
9
|
<rel-checkbox label="Accept terms"></rel-checkbox>
|
|
10
10
|
|
|
11
|
-
<!--
|
|
11
|
+
<!-- Supporting text, compact density (default: text on its own line, indented past the box) -->
|
|
12
12
|
<rel-checkbox
|
|
13
13
|
label="Subscribe to newsletter"
|
|
14
14
|
supportingtext="We'll send you weekly updates"
|
|
15
15
|
status="unchecked"
|
|
16
16
|
state="default">
|
|
17
17
|
</rel-checkbox>
|
|
18
|
+
|
|
19
|
+
<!-- Comfortable density: label + supporting text stack beside the box -->
|
|
20
|
+
<rel-checkbox
|
|
21
|
+
density="comfortable"
|
|
22
|
+
label="Notify the broker"
|
|
23
|
+
supportingtext="Sends an email when the POD is uploaded">
|
|
24
|
+
</rel-checkbox>
|
|
25
|
+
|
|
26
|
+
<!-- Trailing alignment: box at the far edge of the row (settings-row style).
|
|
27
|
+
Trailing always renders comfortable density — compact has no trailing layout. -->
|
|
28
|
+
<rel-checkbox
|
|
29
|
+
alignment="trailing"
|
|
30
|
+
label="Email notifications"
|
|
31
|
+
supportingtext="Weekly summary of load activity">
|
|
32
|
+
</rel-checkbox>
|
|
33
|
+
|
|
34
|
+
<!-- Small size -->
|
|
35
|
+
<rel-checkbox size="small" label="Include archived"></rel-checkbox>
|
|
36
|
+
|
|
37
|
+
<!-- Indeterminate (programmatic-only; user toggle resolves to checked/unchecked) -->
|
|
38
|
+
<rel-checkbox label="Select all" status="indeterminate"></rel-checkbox>
|
|
18
39
|
```
|
|
19
40
|
|
|
20
41
|
## API
|
|
@@ -23,42 +44,62 @@ A tri-state checkbox component supporting checked, unchecked, and indeterminate
|
|
|
23
44
|
| Name | Type | Default | Description |
|
|
24
45
|
|------|------|---------|-------------|
|
|
25
46
|
| `label` | `string` | `"Label"` | Label text displayed next to the checkbox |
|
|
26
|
-
| `icon` | `string` | `""` | `rel-icon` name rendered between the checkbox and the label (label prefix icon). Unset renders no icon and reserves no space |
|
|
27
|
-
| `supportingtext` | `string` | `""` | Helper text
|
|
47
|
+
| `icon` | `string` | `""` | `rel-icon` name rendered between the checkbox and the label (label prefix icon; scales with `size`). Unset renders no icon and reserves no space |
|
|
48
|
+
| `supportingtext` | `string` | `""` | Helper text; placement depends on `density` |
|
|
28
49
|
| `state` | `"default" \| "error" \| "disabled"` | `"default"` | Visual and interaction state of the checkbox |
|
|
29
|
-
| `status` | `"unchecked" \| "checked" \| "indeterminate"` | `"unchecked"` | Current
|
|
50
|
+
| `status` | `"unchecked" \| "checked" \| "indeterminate"` | `"unchecked"` | Current check state. Indeterminate is programmatic-only |
|
|
51
|
+
| `checked` | `boolean` (accessor) | `false` | Convenience accessor over `status` (used by the Angular CVA) |
|
|
52
|
+
| `size` | `"small" \| "medium"` | `"medium"` | Control scale: medium = 18px box, body-lg label, body-sm supporting text; small = 14px box, body-md label, body-xs supporting text (reflected) |
|
|
53
|
+
| `density` | `"compact" \| "comfortable"` | `"compact"` | Supporting-text layout: compact puts it on its own line under the row, indented past the box; comfortable stacks label + supporting text beside the box (reflected) |
|
|
54
|
+
| `alignment` | `"leading" \| "trailing"` | `"leading"` | Box position: leading = before the label; trailing = far edge of the row. Trailing always renders comfortable density — compact+trailing is "Not available" in the Figma sheet, and a dev-mode `console.warn` fires if requested (reflected) |
|
|
55
|
+
| `value` | `string` | `""` | Value reported in the `rel-change` event detail |
|
|
56
|
+
|
|
57
|
+
### Layout matrix (mirrors the Figma sheet)
|
|
58
|
+
| Size | Density | Leading | Trailing |
|
|
59
|
+
|------|---------|---------|----------|
|
|
60
|
+
| M | Comfortable | ✓ | ✓ |
|
|
61
|
+
| M | Compact | ✓ (default) | — renders comfortable |
|
|
62
|
+
| S | Comfortable | ✓ | ✓ |
|
|
63
|
+
| S | Compact | ✓ | — renders comfortable |
|
|
30
64
|
|
|
31
65
|
## Events
|
|
32
66
|
| Event | Detail | Description |
|
|
33
67
|
|-------|--------|-------------|
|
|
34
|
-
| `change` | `{ checked: boolean,
|
|
68
|
+
| `rel-change` | `{ checked: boolean, value: string }` | Fired on user toggle (bubbles, composed) |
|
|
35
69
|
|
|
36
70
|
## Slots
|
|
37
71
|
Default slot not used.
|
|
38
72
|
|
|
39
73
|
## CSS Parts
|
|
40
|
-
|
|
74
|
+
- `base` — outer wrapper
|
|
75
|
+
- `label` — label text span
|
|
76
|
+
- `helper` — supporting text span
|
|
41
77
|
|
|
42
78
|
## Tokens / CSS Variables
|
|
43
79
|
|
|
44
80
|
### Semantic Tokens
|
|
45
81
|
- `--rel-fonts-font-family` - Font family
|
|
46
|
-
- `--rel-body-
|
|
47
|
-
- `--rel-
|
|
82
|
+
- `--rel-body-lg-regular-*` / `--rel-body-sm-regular-*` - Label and supporting text type (medium size)
|
|
83
|
+
- `--rel-body-md-regular-*` / `--rel-body-xs-regular-*` - Label and supporting text type (small size)
|
|
84
|
+
|
|
85
|
+
### Spacing Tokens
|
|
86
|
+
- `--rel-spacing-xs` - Gap between box, icon, and label
|
|
87
|
+
- `--rel-spacing-3xs` - Gap between label and supporting text (comfortable) / row and supporting text (compact)
|
|
48
88
|
|
|
49
89
|
### Color Tokens
|
|
50
|
-
- `--rel-text-
|
|
51
|
-
- `--rel-
|
|
52
|
-
- `--rel-
|
|
53
|
-
- `--rel-text-
|
|
54
|
-
- `--rel-
|
|
55
|
-
- `--rel-border-
|
|
90
|
+
- `--rel-text-default` - Box border and label text color (rest)
|
|
91
|
+
- `--rel-text-body` - Hover color for box, label, and icon
|
|
92
|
+
- `--rel-bg-neutral-inverse-2` - Checked box fill and indeterminate dash
|
|
93
|
+
- `--rel-text-inverse` - Checkmark color
|
|
94
|
+
- `--rel-text-caption` - Supporting text color
|
|
95
|
+
- `--rel-border-focus` - Focus outline color
|
|
96
|
+
- `--rel-border-danger` / `--rel-action-danger-fill` / `--rel-text-danger` - Error state colors
|
|
56
97
|
|
|
57
98
|
## Accessibility
|
|
58
99
|
- Uses visually hidden native checkbox input for screen reader support
|
|
59
100
|
- Custom visual checkbox with proper focus indicators
|
|
60
101
|
- Focus visible state with outline offset for keyboard users
|
|
61
|
-
- Label click targets the checkbox for larger interaction area
|
|
102
|
+
- Label click targets the checkbox for larger interaction area (in trailing alignment the whole row is clickable)
|
|
62
103
|
- Disabled state communicated through opacity reduction and cursor change
|
|
63
104
|
|
|
64
105
|
### Keyboard Interactions
|
|
@@ -67,12 +108,18 @@ None exposed.
|
|
|
67
108
|
|
|
68
109
|
### Focus Behavior
|
|
69
110
|
- Focus outline appears with offset around the custom checkbox box
|
|
70
|
-
- Uses
|
|
111
|
+
- Uses focus border color for high contrast
|
|
71
112
|
|
|
72
113
|
## Notes / Edge Cases
|
|
73
|
-
-
|
|
114
|
+
- User toggling is binary: indeterminate is never entered by interaction — toggling from indeterminate resolves to checked. Set `status="indeterminate"` programmatically (e.g. a Select-All parent driven by app state)
|
|
74
115
|
- When `state="disabled"`, click events are blocked and opacity is reduced to 0.55
|
|
75
|
-
- The
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
|
|
116
|
+
- The host is `display: block`, so a trailing checkbox stretches to its container width; constrain the parent to control row width
|
|
117
|
+
- Comfortable density with no supporting text is visually identical to compact
|
|
118
|
+
- The hidden native input is absolutely positioned against the `.row` containing block; keep `position: relative` on `.row` (Tab-scroll page-jump fix, shared with rel-radio)
|
|
119
|
+
|
|
120
|
+
## Recipes (intentionally NOT component props)
|
|
121
|
+
- **Selectable list row**: `alignment="trailing"` checkbox inside a padded, hover-highlighted container
|
|
122
|
+
- **Select All / nested group**: parent checkbox with `status="indeterminate"` wired to children in app state
|
|
123
|
+
- **Selectable card**: `rel-card` wrapping a checkbox; toggle a selected border on `rel-change`
|
|
124
|
+
|
|
125
|
+
See the Storybook Checkbox stories for working recipe markup.
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { LitElement, html, css } 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
|
|
|
5
5
|
type CheckboxState = "default" | "error" | "disabled";
|
|
6
6
|
type CheckboxStatus = "unchecked" | "checked" | "indeterminate";
|
|
7
|
+
type CheckboxSize = "small" | "medium";
|
|
8
|
+
type CheckboxDensity = "compact" | "comfortable";
|
|
9
|
+
type CheckboxAlignment = "leading" | "trailing";
|
|
7
10
|
|
|
8
11
|
@customElement("rel-checkbox")
|
|
9
12
|
export class RelCheckbox extends LitElement {
|
|
@@ -15,6 +18,21 @@ export class RelCheckbox extends LitElement {
|
|
|
15
18
|
@property({ type: String }) status: CheckboxStatus = "unchecked";
|
|
16
19
|
@property({ type: String }) value = "";
|
|
17
20
|
|
|
21
|
+
/** Control scale (Figma Size). `"medium"` (default) = 18px box with body-lg
|
|
22
|
+
* label; `"small"` = 14px box with body-md label. */
|
|
23
|
+
@property({ type: String, reflect: true }) size: CheckboxSize = "medium";
|
|
24
|
+
|
|
25
|
+
/** Supporting-text layout (Figma Density). `"compact"` (default) puts the
|
|
26
|
+
* supporting text on its own line under the row, indented past the box;
|
|
27
|
+
* `"comfortable"` stacks label + supporting text beside the box. */
|
|
28
|
+
@property({ type: String, reflect: true }) density: CheckboxDensity = "compact";
|
|
29
|
+
|
|
30
|
+
/** Box position (Figma Alignment). `"leading"` (default) renders the box
|
|
31
|
+
* before the label; `"trailing"` pushes it to the far edge of the row.
|
|
32
|
+
* Trailing has no compact layout in the spec, so it always renders with
|
|
33
|
+
* comfortable density. */
|
|
34
|
+
@property({ type: String, reflect: true }) alignment: CheckboxAlignment = "leading";
|
|
35
|
+
|
|
18
36
|
get checked(): boolean {
|
|
19
37
|
return this.status === "checked";
|
|
20
38
|
}
|
|
@@ -53,6 +71,29 @@ export class RelCheckbox extends LitElement {
|
|
|
53
71
|
cursor: pointer;
|
|
54
72
|
}
|
|
55
73
|
|
|
74
|
+
/* Trailing alignment — box moves to the far edge, label stack to the
|
|
75
|
+
start. row-reverse keeps the box first in DOM order (input + .box
|
|
76
|
+
adjacency drives the focus ring) while space-between stretches the
|
|
77
|
+
row to its container. */
|
|
78
|
+
:host([alignment="trailing"]) .row {
|
|
79
|
+
flex-direction: row-reverse;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Comfortable density — label and supporting text stack beside the box. */
|
|
84
|
+
.content {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: var(--rel-spacing-3xs);
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.label-line {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: var(--rel-spacing-xs);
|
|
95
|
+
}
|
|
96
|
+
|
|
56
97
|
input {
|
|
57
98
|
position: absolute;
|
|
58
99
|
inset: 0;
|
|
@@ -195,7 +236,6 @@ export class RelCheckbox extends LitElement {
|
|
|
195
236
|
|
|
196
237
|
.text {
|
|
197
238
|
color: var(--rel-text-caption);
|
|
198
|
-
padding-left: 28px;
|
|
199
239
|
|
|
200
240
|
font-family: var(--rel-body-sm-regular-font-family);
|
|
201
241
|
|
|
@@ -208,8 +248,63 @@ export class RelCheckbox extends LitElement {
|
|
|
208
248
|
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
209
249
|
|
|
210
250
|
}
|
|
251
|
+
|
|
252
|
+
/* Compact density — supporting text sits under the whole row, indented
|
|
253
|
+
past the box (box width + row gap, matching the pre-existing 28px). */
|
|
254
|
+
.text-below {
|
|
255
|
+
padding-left: 28px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Size variant (medium is the base scale above). */
|
|
259
|
+
:host([size="small"]) .box {
|
|
260
|
+
width: 14px;
|
|
261
|
+
height: 14px;
|
|
262
|
+
border-radius: 3px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
:host([size="small"]) svg {
|
|
266
|
+
width: 10px;
|
|
267
|
+
height: 10px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
:host([size="small"]) .label {
|
|
271
|
+
font-family: var(--rel-body-md-regular-font-family);
|
|
272
|
+
font-weight: var(--rel-body-md-regular-font-weight);
|
|
273
|
+
font-size: var(--rel-body-md-regular-font-size);
|
|
274
|
+
line-height: var(--rel-body-md-regular-line-height);
|
|
275
|
+
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
:host([size="small"]) .text {
|
|
279
|
+
font-family: var(--rel-body-xs-regular-font-family);
|
|
280
|
+
font-weight: var(--rel-body-xs-regular-font-weight);
|
|
281
|
+
font-size: var(--rel-body-xs-regular-font-size);
|
|
282
|
+
line-height: var(--rel-body-xs-regular-line-height);
|
|
283
|
+
letter-spacing: var(--rel-body-xs-regular-letter-spacing);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
:host([size="small"]) .text-below {
|
|
287
|
+
padding-left: 24px;
|
|
288
|
+
}
|
|
211
289
|
`;
|
|
212
290
|
|
|
291
|
+
protected willUpdate(changed: PropertyValues<this>) {
|
|
292
|
+
if (
|
|
293
|
+
(changed.has("alignment") || changed.has("density")) &&
|
|
294
|
+
this.alignment === "trailing" &&
|
|
295
|
+
this.density === "compact"
|
|
296
|
+
) {
|
|
297
|
+
console.warn(
|
|
298
|
+
'[rel-checkbox] density="compact" has no trailing layout; rendering comfortable density.'
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Trailing has no compact layout in the spec — trailing wins. */
|
|
304
|
+
private get effectiveDensity(): CheckboxDensity {
|
|
305
|
+
return this.alignment === "trailing" ? "comfortable" : this.density;
|
|
306
|
+
}
|
|
307
|
+
|
|
213
308
|
private toggle() {
|
|
214
309
|
if (this.state === "disabled") return;
|
|
215
310
|
|
|
@@ -239,6 +334,9 @@ export class RelCheckbox extends LitElement {
|
|
|
239
334
|
<label class="row">
|
|
240
335
|
<input
|
|
241
336
|
type="checkbox"
|
|
337
|
+
.checked=${this.status === "checked"}
|
|
338
|
+
.indeterminate=${this.status === "indeterminate"}
|
|
339
|
+
aria-checked="${this.status === "indeterminate" ? "mixed" : nothing}"
|
|
242
340
|
@click=${this.toggle}
|
|
243
341
|
@blur=${() => this._onTouched()}
|
|
244
342
|
?disabled=${this.state === "disabled"}
|
|
@@ -256,15 +354,27 @@ export class RelCheckbox extends LitElement {
|
|
|
256
354
|
</svg>
|
|
257
355
|
</span>
|
|
258
356
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
357
|
+
<span class="content">
|
|
358
|
+
<span class="label-line">
|
|
359
|
+
${this.icon
|
|
360
|
+
? html`<rel-icon
|
|
361
|
+
name="${this.icon}"
|
|
362
|
+
size="${this.size === "small" ? "sm" : "md"}"
|
|
363
|
+
class="icon-prefix"
|
|
364
|
+
></rel-icon>`
|
|
365
|
+
: null}
|
|
366
|
+
|
|
367
|
+
<span part="label" class="label">${this.label}</span>
|
|
368
|
+
</span>
|
|
369
|
+
|
|
370
|
+
${this.effectiveDensity === "comfortable" && this.supportingtext
|
|
371
|
+
? html`<span part="helper" class="text">${this.supportingtext}</span>`
|
|
372
|
+
: null}
|
|
373
|
+
</span>
|
|
264
374
|
</label>
|
|
265
375
|
|
|
266
|
-
${this.supportingtext
|
|
267
|
-
? html`<span part="helper" class="text">${this.supportingtext}</span>`
|
|
376
|
+
${this.effectiveDensity === "compact" && this.supportingtext
|
|
377
|
+
? html`<span part="helper" class="text text-below">${this.supportingtext}</span>`
|
|
268
378
|
: null}
|
|
269
379
|
</div>
|
|
270
380
|
`;
|