@reliability-design/web 1.4.2 → 1.4.4
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/CHANGELOG.md +27 -0
- package/dist/components/rel-autocomplete/README.md +4 -1
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +80 -3
- package/dist/components/rel-button/rel-button.ts +2 -2
- package/dist/components/rel-checkbox/README.md +69 -22
- package/dist/components/rel-checkbox/rel-checkbox.ts +116 -9
- package/dist/components/rel-pagination/README.md +14 -0
- package/dist/components/rel-pagination/rel-pagination.ts +30 -14
- package/dist/components/rel-radio/README.md +16 -1
- package/dist/components/rel-radio/rel-radio.ts +52 -1
- package/dist/components/rel-range-picker/rel-range-picker.ts +117 -2
- package/dist/components/rel-select/README.md +7 -0
- package/dist/components/rel-select/rel-select.ts +1237 -1119
- package/dist/components/rel-textfield/README.md +16 -0
- package/dist/components/rel-textfield/rel-textfield.ts +104 -6
- package/dist/components/rel-time-picker/README.md +199 -0
- package/dist/components/rel-time-picker/rel-time-picker.ts +994 -0
- 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 +453 -39
- package/dist/index.d.ts +453 -39
- package/dist/index.js +2659 -339
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2663 -346
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -28,6 +28,17 @@ A radio button group component that allows users to select a single option from
|
|
|
28
28
|
planRadio.selectedValue = 'basic';
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
|
+
<!-- Horizontal row layout -->
|
|
32
|
+
<rel-radio id="sizeRadio" name="size-group" orientation="horizontal"></rel-radio>
|
|
33
|
+
<script>
|
|
34
|
+
const sizeRadio = document.getElementById('sizeRadio');
|
|
35
|
+
sizeRadio.options = [
|
|
36
|
+
{ value: 's', label: 'Small' },
|
|
37
|
+
{ value: 'm', label: 'Medium' },
|
|
38
|
+
{ value: 'l', label: 'Large' }
|
|
39
|
+
];
|
|
40
|
+
</script>
|
|
41
|
+
|
|
31
42
|
<!-- Listen for changes -->
|
|
32
43
|
<rel-radio id="preferenceRadio"></rel-radio>
|
|
33
44
|
<script>
|
|
@@ -49,6 +60,8 @@ A radio button group component that allows users to select a single option from
|
|
|
49
60
|
|------|------|---------|-------------|
|
|
50
61
|
| `options` | `RadioOption[]` | `[]` | Array of radio options (see RadioOption interface below) |
|
|
51
62
|
| `name` | `string` | `"radio-group"` | Name attribute for the radio group (for form grouping) |
|
|
63
|
+
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | Layout axis: `"vertical"` stacks options in a column; `"horizontal"` lays them out in a wrapping row (reflected attribute) |
|
|
64
|
+
| `size` | `"small" \| "medium"` | `"medium"` | Control scale: `"medium"` = 20px circle with body-lg labels; `"small"` = 16px circle with body-md labels (reflected attribute) |
|
|
52
65
|
| `selectedValue` | `string \| null` | `null` | Currently selected option value (reflected attribute) |
|
|
53
66
|
| `disabled` | `boolean` | `false` | Disables the entire radio group |
|
|
54
67
|
|
|
@@ -82,7 +95,8 @@ None exposed.
|
|
|
82
95
|
- `--rel-fonts-weight-body-medium` - Font weight for labels
|
|
83
96
|
|
|
84
97
|
### Spacing Tokens
|
|
85
|
-
- `--rel-spacing-xs` - Gap between radio options
|
|
98
|
+
- `--rel-spacing-xs` - Gap between radio options (also the row gap when wrapping horizontally)
|
|
99
|
+
- `--rel-spacing-xl` - Column gap between options in `orientation="horizontal"`
|
|
86
100
|
- `--rel-spacing-3xs` - Gap within each option (label and supporting text)
|
|
87
101
|
- `--rel-spacing-2xs` - Gap between radio circle and label
|
|
88
102
|
|
|
@@ -94,6 +108,7 @@ None exposed.
|
|
|
94
108
|
- `--rel-text-danger` - Error state border and dot color
|
|
95
109
|
|
|
96
110
|
## Accessibility
|
|
111
|
+
- The option list renders with `role="radiogroup"` and `aria-orientation` reflecting the `orientation` prop
|
|
97
112
|
- Uses visually hidden native radio inputs for proper screen reader support
|
|
98
113
|
- Custom visual radio circle with proper focus indicators
|
|
99
114
|
- Focus visible state with outline offset for keyboard navigation
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, css } from "lit";
|
|
2
2
|
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import type { Orientation } from "../rel-stepper/rel-stepper.js";
|
|
3
4
|
|
|
4
5
|
export interface RadioOption {
|
|
5
6
|
value: string;
|
|
@@ -14,6 +15,14 @@ export class RelRadio extends LitElement {
|
|
|
14
15
|
|
|
15
16
|
@property({ type: String }) name = "radio-group";
|
|
16
17
|
|
|
18
|
+
/** Layout axis of the option list. `"vertical"` (default) stacks options in
|
|
19
|
+
* a column; `"horizontal"` lays them out in a wrapping row. */
|
|
20
|
+
@property({ type: String, reflect: true }) orientation: Orientation = "vertical";
|
|
21
|
+
|
|
22
|
+
/** Control scale. `"medium"` (default) = 20px circle with body-lg labels;
|
|
23
|
+
* `"small"` = 16px circle with body-md labels. */
|
|
24
|
+
@property({ type: String, reflect: true }) size: "small" | "medium" = "medium";
|
|
25
|
+
|
|
17
26
|
@property({ type: String, reflect: true })
|
|
18
27
|
selectedValue: string | null = null;
|
|
19
28
|
|
|
@@ -45,6 +54,15 @@ export class RelRadio extends LitElement {
|
|
|
45
54
|
gap: var(--rel-spacing-xs);
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
/* Horizontal orientation — options flow in a wrapping row. Wider column
|
|
58
|
+
gap than the vertical rhythm so adjacent option labels read as separate
|
|
59
|
+
choices; row gap keeps the vertical value for wrapped lines. */
|
|
60
|
+
:host([orientation="horizontal"]) .rel-radio-group {
|
|
61
|
+
flex-direction: row;
|
|
62
|
+
flex-wrap: wrap;
|
|
63
|
+
gap: var(--rel-spacing-xs) var(--rel-spacing-xl);
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
.rel-radio-option {
|
|
49
67
|
display: inline-flex;
|
|
50
68
|
flex-direction: column;
|
|
@@ -147,6 +165,34 @@ export class RelRadio extends LitElement {
|
|
|
147
165
|
|
|
148
166
|
}
|
|
149
167
|
|
|
168
|
+
/* Size variant (medium is the base scale above). Supporting-text indent =
|
|
169
|
+
circle width + the radio-main gap, so it stays aligned under the label. */
|
|
170
|
+
:host([size="small"]) .radio-custom {
|
|
171
|
+
width: 16px;
|
|
172
|
+
height: 16px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
:host([size="small"]) input:checked + .radio-custom::after {
|
|
176
|
+
inset: 3px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
:host([size="small"]) .radio-label {
|
|
180
|
+
font-family: var(--rel-body-md-regular-font-family);
|
|
181
|
+
font-weight: var(--rel-body-md-regular-font-weight);
|
|
182
|
+
font-size: var(--rel-body-md-regular-font-size);
|
|
183
|
+
line-height: var(--rel-body-md-regular-line-height);
|
|
184
|
+
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
:host([size="small"]) .radio-supporting {
|
|
188
|
+
margin-left: 24px;
|
|
189
|
+
font-family: var(--rel-body-xs-regular-font-family);
|
|
190
|
+
font-weight: var(--rel-body-xs-regular-font-weight);
|
|
191
|
+
font-size: var(--rel-body-xs-regular-font-size);
|
|
192
|
+
line-height: var(--rel-body-xs-regular-line-height);
|
|
193
|
+
letter-spacing: var(--rel-body-xs-regular-letter-spacing);
|
|
194
|
+
}
|
|
195
|
+
|
|
150
196
|
/* Error state (Figma State=Error, both Is Checked values) */
|
|
151
197
|
.rel-radio-option.error-unchecked .radio-custom,
|
|
152
198
|
.rel-radio-option.error-checked .radio-custom {
|
|
@@ -243,7 +289,12 @@ export class RelRadio extends LitElement {
|
|
|
243
289
|
|
|
244
290
|
render() {
|
|
245
291
|
return html`
|
|
246
|
-
<div
|
|
292
|
+
<div
|
|
293
|
+
part="base"
|
|
294
|
+
class="rel-radio-group"
|
|
295
|
+
role="radiogroup"
|
|
296
|
+
aria-orientation="${this.orientation}"
|
|
297
|
+
>
|
|
247
298
|
${this.options.map((o) => this.renderOption(o))}
|
|
248
299
|
</div>
|
|
249
300
|
${this.error && this.errorMessage
|
|
@@ -52,6 +52,8 @@ export class RelRangePicker extends LitElement {
|
|
|
52
52
|
@state() private hoverDate: string | null = null;
|
|
53
53
|
|
|
54
54
|
@query(".trigger-button") private triggerButton?: HTMLButtonElement;
|
|
55
|
+
@query(".popover") private popoverEl?: HTMLDivElement;
|
|
56
|
+
@query(".backdrop") private backdropEl?: HTMLDivElement;
|
|
55
57
|
|
|
56
58
|
static styles = css`
|
|
57
59
|
:host {
|
|
@@ -113,6 +115,17 @@ export class RelRangePicker extends LitElement {
|
|
|
113
115
|
|
|
114
116
|
/* ── Popover shell ── */
|
|
115
117
|
.popover {
|
|
118
|
+
/* Reset native [popover] UA styles first (the shell is promoted to
|
|
119
|
+
the top layer so transformed / overflow:hidden ancestors — e.g. a
|
|
120
|
+
centered rel-modal panel — can't hijack or clip it). While in the
|
|
121
|
+
top layer, JS overrides the anchored position below with inline
|
|
122
|
+
fixed coordinates; the CSS anchoring is the no-Popover-API
|
|
123
|
+
fallback. */
|
|
124
|
+
margin: 0;
|
|
125
|
+
padding: 0;
|
|
126
|
+
width: auto;
|
|
127
|
+
height: auto;
|
|
128
|
+
color: inherit;
|
|
116
129
|
position: absolute;
|
|
117
130
|
top: calc(100% + 4px);
|
|
118
131
|
left: 0;
|
|
@@ -137,6 +150,13 @@ export class RelRangePicker extends LitElement {
|
|
|
137
150
|
.backdrop {
|
|
138
151
|
position: fixed;
|
|
139
152
|
inset: 0;
|
|
153
|
+
/* Reset native [popover] UA styles — the backdrop is promoted to the
|
|
154
|
+
top layer right before the panel so the panel stacks above it. */
|
|
155
|
+
margin: 0;
|
|
156
|
+
padding: 0;
|
|
157
|
+
width: auto;
|
|
158
|
+
height: auto;
|
|
159
|
+
border: none;
|
|
140
160
|
z-index: 2147483646;
|
|
141
161
|
background: rgba(0,0,0,0.4);
|
|
142
162
|
}
|
|
@@ -448,17 +468,111 @@ export class RelRangePicker extends LitElement {
|
|
|
448
468
|
super.disconnectedCallback();
|
|
449
469
|
document.removeEventListener("click", this.handleClickOutside);
|
|
450
470
|
document.removeEventListener("keydown", this.handleGlobalKeydown);
|
|
471
|
+
window.removeEventListener("resize", this.onReposition);
|
|
472
|
+
window.removeEventListener("scroll", this.onReposition, true);
|
|
473
|
+
this.hideTopLayer();
|
|
451
474
|
}
|
|
452
475
|
|
|
453
|
-
updated() {
|
|
476
|
+
updated(changed: Map<string, unknown>) {
|
|
454
477
|
this.style.width = this.width;
|
|
455
478
|
this.style.minWidth = this.minWidth;
|
|
456
479
|
this.style.maxWidth = this.maxWidth;
|
|
457
480
|
this.style.height = this.height;
|
|
458
481
|
this.style.minHeight = this.minHeight;
|
|
459
482
|
this.style.maxHeight = this.maxHeight;
|
|
483
|
+
|
|
484
|
+
// `open` is a public reflected property (consumers may set it directly,
|
|
485
|
+
// not only via the trigger), so top-layer promotion syncs here rather
|
|
486
|
+
// than inside openPopover/cancelPopover.
|
|
487
|
+
if (changed.has("open")) {
|
|
488
|
+
if (this.open) {
|
|
489
|
+
this.showTopLayer();
|
|
490
|
+
window.addEventListener("resize", this.onReposition);
|
|
491
|
+
window.addEventListener("scroll", this.onReposition, true);
|
|
492
|
+
} else {
|
|
493
|
+
window.removeEventListener("resize", this.onReposition);
|
|
494
|
+
window.removeEventListener("scroll", this.onReposition, true);
|
|
495
|
+
this.hideTopLayer();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* A transformed ancestor (e.g. rel-modal's centered panel with
|
|
501
|
+
* translate(-50%,-50%)) becomes the containing block for position:fixed
|
|
502
|
+
* descendants, and its overflow:hidden clips position:absolute ones —
|
|
503
|
+
* either way the picker shell breaks inside a modal. Promoting shell
|
|
504
|
+
* (and popup-mode backdrop) to the browser top layer sidesteps both —
|
|
505
|
+
* same fix as rel-popover / rel-autocomplete / rel-select. `manual`
|
|
506
|
+
* keeps outside-click and Escape handling under this component's own
|
|
507
|
+
* control. While in the top layer the CSS anchoring (absolute, below
|
|
508
|
+
* the trigger) is meaningless, so popover mode gets inline fixed
|
|
509
|
+
* coordinates from updatePopoverPosition(). */
|
|
510
|
+
|
|
511
|
+
private showTopLayer() {
|
|
512
|
+
// Backdrop first — later-shown popovers stack above earlier ones.
|
|
513
|
+
for (const el of [this.mode === "popup" ? this.backdropEl : undefined, this.popoverEl]) {
|
|
514
|
+
if (!el || typeof el.showPopover !== "function") continue;
|
|
515
|
+
try {
|
|
516
|
+
if (!el.matches(":popover-open")) el.showPopover();
|
|
517
|
+
} catch {
|
|
518
|
+
/* older engines / detached element — CSS fallback applies */
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (this.mode === "popover") {
|
|
522
|
+
requestAnimationFrame(() => this.updatePopoverPosition());
|
|
523
|
+
}
|
|
460
524
|
}
|
|
461
525
|
|
|
526
|
+
private hideTopLayer() {
|
|
527
|
+
for (const el of [this.popoverEl, this.backdropEl]) {
|
|
528
|
+
if (!el || typeof el.hidePopover !== "function") continue;
|
|
529
|
+
try {
|
|
530
|
+
if (el.matches(":popover-open")) el.hidePopover();
|
|
531
|
+
} catch {
|
|
532
|
+
/* already hidden or unsupported — nothing to do */
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (this.popoverEl) {
|
|
536
|
+
this.popoverEl.style.position = "";
|
|
537
|
+
this.popoverEl.style.top = "";
|
|
538
|
+
this.popoverEl.style.left = "";
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private onReposition = () => {
|
|
543
|
+
if (this.open && this.mode === "popover") this.updatePopoverPosition();
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
private updatePopoverPosition = () => {
|
|
547
|
+
const panel = this.popoverEl;
|
|
548
|
+
if (!panel || !this.open || this.mode !== "popover") return;
|
|
549
|
+
|
|
550
|
+
// Host rect anchors both trigger variants (default button and icon).
|
|
551
|
+
const t = this.getBoundingClientRect();
|
|
552
|
+
const c = panel.getBoundingClientRect();
|
|
553
|
+
|
|
554
|
+
let top = t.bottom + 4;
|
|
555
|
+
if (top + c.height > window.innerHeight - 8) {
|
|
556
|
+
top = Math.max(8, t.top - c.height - 4);
|
|
557
|
+
}
|
|
558
|
+
let left = t.left;
|
|
559
|
+
if (left + c.width > window.innerWidth - 8) {
|
|
560
|
+
left = Math.max(8, window.innerWidth - c.width - 8);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Without top-layer support a transformed ancestor may be the containing
|
|
564
|
+
// block for position:fixed, shifting the shell away from the viewport
|
|
565
|
+
// coordinates computed above. Apply, re-measure, and cancel the offset.
|
|
566
|
+
panel.style.position = "fixed";
|
|
567
|
+
panel.style.top = `${top}px`;
|
|
568
|
+
panel.style.left = `${left}px`;
|
|
569
|
+
const applied = panel.getBoundingClientRect();
|
|
570
|
+
top -= applied.top - top;
|
|
571
|
+
left -= applied.left - left;
|
|
572
|
+
panel.style.top = `${top}px`;
|
|
573
|
+
panel.style.left = `${left}px`;
|
|
574
|
+
};
|
|
575
|
+
|
|
462
576
|
// ── Open / close ──
|
|
463
577
|
|
|
464
578
|
private handleClickOutside = (e: MouseEvent) => {
|
|
@@ -916,11 +1030,12 @@ export class RelRangePicker extends LitElement {
|
|
|
916
1030
|
`}
|
|
917
1031
|
|
|
918
1032
|
${isPopup && this.open
|
|
919
|
-
? html`<div part="overlay" class="backdrop" @click=${this.cancelPopover}></div>`
|
|
1033
|
+
? html`<div part="overlay" class="backdrop" popover="manual" @click=${this.cancelPopover}></div>`
|
|
920
1034
|
: nothing}
|
|
921
1035
|
|
|
922
1036
|
<div
|
|
923
1037
|
part="panel" class="popover ${this.open ? "open" : ""} ${isPopup ? "popup" : ""}"
|
|
1038
|
+
popover="manual"
|
|
924
1039
|
role="dialog"
|
|
925
1040
|
aria-label="Select date range"
|
|
926
1041
|
>
|
|
@@ -34,6 +34,9 @@ A custom combobox-style select component built on the same textfield visual lang
|
|
|
34
34
|
| Name | Type | Default | Description |
|
|
35
35
|
|------|------|---------|-------------|
|
|
36
36
|
| `label` | `string` | `""` | Label text displayed above the select |
|
|
37
|
+
| `label-style` | `"default" \| "floating" \| "none"` | `"default"` | Label presentation. `"default"` sits above the trigger, `"floating"` rests over the placeholder and animates onto the border line, `"none"` renders no label at all |
|
|
38
|
+
| `field-aria-label` | `string` | - | Accessible name for the trigger. Required with `label-style="none"` |
|
|
39
|
+
| `field-aria-labelledby` | `string` | - | IDREF list of external element(s) naming the trigger. Wins over `field-aria-label` |
|
|
37
40
|
| `options` | `(string \| SelectOptionObject)[]` | `[]` | Options to display in the dropdown. Each entry is either a plain string or an object: `{ value, label?, supportingText?, icon? }`. `icon` is a `rel-icon` name rendered as a prefix (leading icon) on the menu item — before the label in single-select rows, and between the checkbox and label in multi-select rows |
|
|
38
41
|
| `selected` | `string` | `""` | Currently selected option value |
|
|
39
42
|
| `state` | `"default" \| "error" \| "disabled"` | `"default"` | Visual and interaction state of the select |
|
|
@@ -80,6 +83,9 @@ None exposed.
|
|
|
80
83
|
### Border Tokens
|
|
81
84
|
- `--rel-border-radius-xs` - Rounded border radius (default `border="rounded"`)
|
|
82
85
|
|
|
86
|
+
### Layout Tokens
|
|
87
|
+
- `--rel-field-label-reserve` - Height held for the absent label row under `label-style="none"`, so the trigger bottom-aligns with labelled siblings. Defaults to the label line-height plus the wrapper gap; set to `0` to collapse the row entirely in compact contexts (toolbars, table cells)
|
|
88
|
+
|
|
83
89
|
## Accessibility
|
|
84
90
|
- Trigger input uses `role="combobox"` with `aria-haspopup="listbox"`, `aria-expanded`, and `aria-controls`
|
|
85
91
|
- Dropdown panel uses `role="listbox"`; each option uses `role="option"` with `aria-selected`
|
|
@@ -113,3 +119,4 @@ None exposed.
|
|
|
113
119
|
- Typed text that doesn't exactly match (case-insensitive) any option reverts to the last committed `selected` value on blur, Escape, or Tab
|
|
114
120
|
- The ghost-text suggestion shows the remainder of the first option whose name starts with the typed text (case-insensitive); it has no effect on filtering, which matches substrings anywhere in an option
|
|
115
121
|
- Dropdown panel is positioned via `position: fixed` and flips above the trigger if there isn't enough viewport space below
|
|
122
|
+
- The open listbox 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
|