@reliability-design/web 1.4.1 → 1.4.3
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 +49 -22
- package/dist/components/rel-autocomplete/README.md +96 -0
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +634 -0
- package/dist/components/rel-button/rel-button.ts +17 -3
- package/dist/components/rel-checkbox/README.md +1 -0
- package/dist/components/rel-checkbox/rel-checkbox.ts +34 -0
- package/dist/components/rel-drawer/rel-drawer.ts +9 -0
- package/dist/components/rel-popover/rel-popover.ts +56 -0
- package/dist/components/rel-radio/README.md +16 -1
- package/dist/components/rel-radio/rel-radio.ts +64 -1
- package/dist/components/rel-select/README.md +10 -2
- package/dist/components/rel-select/rel-select.ts +238 -15
- package/dist/components/rel-sheet/rel-sheet.ts +20 -0
- package/dist/components/rel-textfield/README.md +16 -0
- package/dist/components/rel-textfield/rel-textfield.ts +109 -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/index.d.mts +377 -77
- package/dist/index.d.ts +377 -77
- package/dist/index.js +4585 -2608
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3428 -1453
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +866 -866
- package/dist/{vars-BYCEEKWJ.css → vars-XNMTSRVN.css} +866 -866
- package/package.json +1 -1
|
@@ -4,16 +4,20 @@ import { repeat } from "lit/directives/repeat.js";
|
|
|
4
4
|
import "@reliability-design/icons/rel-icon";
|
|
5
5
|
import "../rel-chip/rel-chip.js";
|
|
6
6
|
import "../rel-checkbox/rel-checkbox.js";
|
|
7
|
+
import type { FieldLabelStyle } from "../rel-textfield/rel-textfield.js";
|
|
7
8
|
|
|
8
9
|
export type SelectState = "default" | "error" | "disabled";
|
|
9
10
|
export type SelectSize = "small" | "medium" | "large";
|
|
10
11
|
export type SelectBorder = "rounded" | "sharp";
|
|
11
|
-
|
|
12
|
+
/** Alias of the shared field type — kept so existing imports keep resolving. */
|
|
13
|
+
export type SelectLabelStyle = FieldLabelStyle;
|
|
12
14
|
|
|
13
15
|
export interface SelectOptionObject {
|
|
14
16
|
value: string;
|
|
15
17
|
label?: string;
|
|
16
18
|
supportingText?: string;
|
|
19
|
+
/** rel-icon name rendered as a prefix (leading icon) on the menu item. */
|
|
20
|
+
icon?: string;
|
|
17
21
|
}
|
|
18
22
|
export type SelectOption = string | SelectOptionObject;
|
|
19
23
|
|
|
@@ -21,11 +25,17 @@ interface NormalizedOption {
|
|
|
21
25
|
value: string;
|
|
22
26
|
label: string;
|
|
23
27
|
supportingText: string;
|
|
28
|
+
icon: string;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
function normalizeOption(o: SelectOption): NormalizedOption {
|
|
27
|
-
if (typeof o === "string") return { value: o, label: o, supportingText: "" };
|
|
28
|
-
return {
|
|
32
|
+
if (typeof o === "string") return { value: o, label: o, supportingText: "", icon: "" };
|
|
33
|
+
return {
|
|
34
|
+
value: o.value,
|
|
35
|
+
label: o.label ?? o.value,
|
|
36
|
+
supportingText: o.supportingText ?? "",
|
|
37
|
+
icon: o.icon ?? "",
|
|
38
|
+
};
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
@customElement("rel-select")
|
|
@@ -36,7 +46,16 @@ export class RelSelect extends LitElement {
|
|
|
36
46
|
// hyphens) — e.g. labelStyle would otherwise only respond to "labelstyle".
|
|
37
47
|
// Every multi-word property below gets an explicit kebab-case `attribute`
|
|
38
48
|
// so it works the way HTML usage actually looks.
|
|
39
|
-
@property({ type: String, attribute: "label-style"
|
|
49
|
+
@property({ type: String, attribute: "label-style", reflect: true })
|
|
50
|
+
labelStyle: SelectLabelStyle = "default";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Accessible name for the trigger. Required when `labelStyle="none"`, since no
|
|
54
|
+
* `<label>` renders and a placeholder is not a valid naming mechanism.
|
|
55
|
+
*/
|
|
56
|
+
@property({ type: String, attribute: "field-aria-label" }) fieldAriaLabel?: string;
|
|
57
|
+
/** IDREF list of external element(s) naming the trigger. Wins over `fieldAriaLabel`. */
|
|
58
|
+
@property({ type: String, attribute: "field-aria-labelledby" }) fieldAriaLabelledby?: string;
|
|
40
59
|
|
|
41
60
|
@property({ type: Array }) options: SelectOption[] = [];
|
|
42
61
|
|
|
@@ -67,8 +86,12 @@ export class RelSelect extends LitElement {
|
|
|
67
86
|
|
|
68
87
|
@property({ type: String, attribute: "select-all-label" }) selectAllLabel: string = "Select all";
|
|
69
88
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Optional hard cap on visible chips. By default (no cap) the visible
|
|
91
|
+
* count adapts to the trigger's width: chips collapse into the "+N" badge
|
|
92
|
+
* one by one instead of wrapping to a second line.
|
|
93
|
+
*/
|
|
94
|
+
@property({ type: Number, attribute: "max-visible-chips" }) maxVisibleChips: number = Infinity;
|
|
72
95
|
|
|
73
96
|
// Boolean props default true and toggled off via ="false" are a classic Lit
|
|
74
97
|
// trap: for type:Boolean, attribute *presence* (not its string value)
|
|
@@ -84,9 +107,19 @@ export class RelSelect extends LitElement {
|
|
|
84
107
|
@state() private searchValue = "";
|
|
85
108
|
@state() private internalState: "default" | "focused" = "default";
|
|
86
109
|
|
|
110
|
+
/** How many chips currently fit in the trigger width (width-adaptive). */
|
|
111
|
+
@state() private visibleChipCount = Infinity;
|
|
112
|
+
|
|
87
113
|
@query(".select-trigger") private triggerEl!: HTMLButtonElement;
|
|
88
114
|
@query(".search-input") private searchInputEl!: HTMLInputElement;
|
|
89
115
|
@query(".select-listbox") private listboxEl!: HTMLDivElement;
|
|
116
|
+
@query(".select-value") private valueEl!: HTMLElement;
|
|
117
|
+
@query(".select-chips-measure") private chipsMeasureEl!: HTMLElement;
|
|
118
|
+
|
|
119
|
+
/** Must match the `gap` of .select-chips / .select-chips-measure. */
|
|
120
|
+
private static readonly CHIP_GAP = 6;
|
|
121
|
+
|
|
122
|
+
private chipResizeObserver?: ResizeObserver;
|
|
90
123
|
|
|
91
124
|
private uniqueId = `select-${Math.random().toString(36).slice(2)}`;
|
|
92
125
|
|
|
@@ -127,6 +160,22 @@ export class RelSelect extends LitElement {
|
|
|
127
160
|
|
|
128
161
|
}
|
|
129
162
|
|
|
163
|
+
/* ── labelStyle="none" ──
|
|
164
|
+
No label renders. An empty spacer holds the label row's full height so the
|
|
165
|
+
trigger still bottom-aligns with labelled siblings in a form row.
|
|
166
|
+
The reserve covers label line-height + the wrapper gap, and the negative
|
|
167
|
+
margin cancels the flex gap that would otherwise sit below it — so
|
|
168
|
+
--rel-field-label-reserve: 0 collapses the row to genuinely zero
|
|
169
|
+
(toolbars, table cells), not just to the leftover gap. */
|
|
170
|
+
.label-reserve {
|
|
171
|
+
flex: none;
|
|
172
|
+
height: var(
|
|
173
|
+
--rel-field-label-reserve,
|
|
174
|
+
calc(var(--rel-body-sm-regular-line-height) + var(--rel-spacing-2xs))
|
|
175
|
+
);
|
|
176
|
+
margin-bottom: calc(-1 * var(--rel-spacing-2xs));
|
|
177
|
+
}
|
|
178
|
+
|
|
130
179
|
/* Error only colors the border/focus ring and the supporting text below
|
|
131
180
|
the field — label, floating label, placeholder, and icons stay normal. */
|
|
132
181
|
|
|
@@ -232,7 +281,7 @@ export class RelSelect extends LitElement {
|
|
|
232
281
|
display: flex;
|
|
233
282
|
align-items: center;
|
|
234
283
|
gap: 6px;
|
|
235
|
-
flex-wrap:
|
|
284
|
+
flex-wrap: nowrap;
|
|
236
285
|
color: var(--rel-select-value-color, var(--rel-text-default));
|
|
237
286
|
overflow: hidden;
|
|
238
287
|
}
|
|
@@ -251,11 +300,33 @@ export class RelSelect extends LitElement {
|
|
|
251
300
|
white-space: nowrap;
|
|
252
301
|
}
|
|
253
302
|
|
|
303
|
+
/* Chips never wrap — the number of rendered chips adapts to the
|
|
304
|
+
available width instead (see computeVisibleChips), collapsing the
|
|
305
|
+
remainder into a trailing "+N" chip. overflow:hidden guards the one
|
|
306
|
+
frame between a resize and the recomputation catching up. */
|
|
254
307
|
.select-chips {
|
|
255
308
|
display: flex;
|
|
256
309
|
align-items: center;
|
|
257
310
|
gap: 6px;
|
|
258
|
-
flex-wrap:
|
|
311
|
+
flex-wrap: nowrap;
|
|
312
|
+
min-width: 0;
|
|
313
|
+
overflow: hidden;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* Invisible copy of ALL selected chips (plus a worst-case "+N" chip),
|
|
317
|
+
laid out unconstrained so computeVisibleChips can read each chip's
|
|
318
|
+
natural width — including chips currently collapsed into "+N", whose
|
|
319
|
+
widths are needed again when the field grows. */
|
|
320
|
+
.select-chips-measure {
|
|
321
|
+
position: absolute;
|
|
322
|
+
top: 0;
|
|
323
|
+
left: 0;
|
|
324
|
+
display: flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
gap: 6px;
|
|
327
|
+
width: max-content;
|
|
328
|
+
visibility: hidden;
|
|
329
|
+
pointer-events: none;
|
|
259
330
|
}
|
|
260
331
|
|
|
261
332
|
/* rel-chip's outer element is a tabindex="0" span, not a native button —
|
|
@@ -430,6 +501,21 @@ export class RelSelect extends LitElement {
|
|
|
430
501
|
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
431
502
|
}
|
|
432
503
|
|
|
504
|
+
/* Groups the optional prefix icon with the label so the trailing
|
|
505
|
+
check indicator stays pinned to the right edge of the row. */
|
|
506
|
+
.select-option-content {
|
|
507
|
+
flex: 1;
|
|
508
|
+
min-width: 0;
|
|
509
|
+
display: flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
gap: var(--rel-spacing-xs);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.select-option-icon {
|
|
515
|
+
flex-shrink: 0;
|
|
516
|
+
color: var(--rel-select-icon-color, var(--rel-text-default));
|
|
517
|
+
}
|
|
518
|
+
|
|
433
519
|
.select-option-label {
|
|
434
520
|
overflow: hidden;
|
|
435
521
|
text-overflow: ellipsis;
|
|
@@ -480,9 +566,23 @@ export class RelSelect extends LitElement {
|
|
|
480
566
|
|
|
481
567
|
`;
|
|
482
568
|
|
|
569
|
+
connectedCallback() {
|
|
570
|
+
super.connectedCallback();
|
|
571
|
+
// firstUpdated only runs once — re-observe if the element is re-attached.
|
|
572
|
+
if (this.chipResizeObserver && this.triggerEl) {
|
|
573
|
+
this.chipResizeObserver.observe(this.triggerEl);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
483
577
|
disconnectedCallback() {
|
|
484
578
|
super.disconnectedCallback();
|
|
485
579
|
this.removeGlobalListeners();
|
|
580
|
+
this.chipResizeObserver?.disconnect();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
protected firstUpdated() {
|
|
584
|
+
this.chipResizeObserver = new ResizeObserver(() => this.computeVisibleChips());
|
|
585
|
+
this.chipResizeObserver.observe(this.triggerEl);
|
|
486
586
|
}
|
|
487
587
|
|
|
488
588
|
protected willUpdate(changed: PropertyValues<this>) {
|
|
@@ -491,6 +591,83 @@ export class RelSelect extends LitElement {
|
|
|
491
591
|
}
|
|
492
592
|
}
|
|
493
593
|
|
|
594
|
+
private _warnedNoName = false;
|
|
595
|
+
|
|
596
|
+
protected updated(changed: PropertyValues<this>) {
|
|
597
|
+
if (
|
|
598
|
+
!this._warnedNoName &&
|
|
599
|
+
this.labelStyle === "none" &&
|
|
600
|
+
!this.fieldAriaLabel &&
|
|
601
|
+
!this.fieldAriaLabelledby &&
|
|
602
|
+
!this.getAttribute("aria-label")
|
|
603
|
+
) {
|
|
604
|
+
this._warnedNoName = true;
|
|
605
|
+
console.warn(
|
|
606
|
+
'<rel-select label-style="none"> has no accessible name. Set ' +
|
|
607
|
+
"`field-aria-label` or `field-aria-labelledby` — a placeholder is not a " +
|
|
608
|
+
"valid naming mechanism.",
|
|
609
|
+
this
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (
|
|
614
|
+
changed.has("selectedValues") ||
|
|
615
|
+
changed.has("options") ||
|
|
616
|
+
changed.has("multiple") ||
|
|
617
|
+
changed.has("size")
|
|
618
|
+
) {
|
|
619
|
+
// rAF: rel-chip renders in its own (microtask) update cycle, so its
|
|
620
|
+
// width isn't measurable synchronously here — by the next frame it is.
|
|
621
|
+
requestAnimationFrame(() => this.computeVisibleChips());
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Width-adaptive chip collapsing: measures every selected chip in the
|
|
627
|
+
* hidden .select-chips-measure row and works out how many fit in the value
|
|
628
|
+
* area, always reserving room for the trailing "+N" chip when not all fit.
|
|
629
|
+
* At least one chip stays visible no matter how narrow the field gets.
|
|
630
|
+
*/
|
|
631
|
+
private computeVisibleChips() {
|
|
632
|
+
if (!this.isConnected || !this.multiple || this.selectedValues.length === 0) return;
|
|
633
|
+
const measure = this.chipsMeasureEl;
|
|
634
|
+
const container = this.valueEl;
|
|
635
|
+
if (!measure || !container) return;
|
|
636
|
+
|
|
637
|
+
const available = container.clientWidth;
|
|
638
|
+
if (available <= 0) return; // hidden (display:none) — RO will refire when visible
|
|
639
|
+
|
|
640
|
+
const children = Array.from(measure.children) as HTMLElement[];
|
|
641
|
+
if (children.length < 2) return;
|
|
642
|
+
|
|
643
|
+
const gap = RelSelect.CHIP_GAP;
|
|
644
|
+
// Last measure child is the worst-case "+N" chip (N = total selected,
|
|
645
|
+
// i.e. the widest label the badge can ever need).
|
|
646
|
+
const overflowWidth = children[children.length - 1].offsetWidth;
|
|
647
|
+
const chipWidths = children.slice(0, -1).map((c) => c.offsetWidth);
|
|
648
|
+
const total = chipWidths.reduce((sum, w) => sum + w, 0) + gap * (chipWidths.length - 1);
|
|
649
|
+
|
|
650
|
+
let fit: number;
|
|
651
|
+
if (total <= available) {
|
|
652
|
+
fit = chipWidths.length;
|
|
653
|
+
} else {
|
|
654
|
+
fit = 0;
|
|
655
|
+
let used = 0;
|
|
656
|
+
for (let i = 0; i < chipWidths.length; i++) {
|
|
657
|
+
const w = chipWidths[i] + (i > 0 ? gap : 0);
|
|
658
|
+
if (used + w + gap + overflowWidth <= available) {
|
|
659
|
+
used += w;
|
|
660
|
+
fit = i + 1;
|
|
661
|
+
} else {
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
fit = Math.max(1, fit);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (fit !== this.visibleChipCount) this.visibleChipCount = fit;
|
|
669
|
+
}
|
|
670
|
+
|
|
494
671
|
private get normalizedOptions(): NormalizedOption[] {
|
|
495
672
|
return this.options.map(normalizeOption);
|
|
496
673
|
}
|
|
@@ -719,7 +896,7 @@ export class RelSelect extends LitElement {
|
|
|
719
896
|
|
|
720
897
|
private renderChips() {
|
|
721
898
|
const values = this.selectedValues;
|
|
722
|
-
const visible = values.slice(0, this.maxVisibleChips);
|
|
899
|
+
const visible = values.slice(0, Math.min(this.visibleChipCount, this.maxVisibleChips));
|
|
723
900
|
const overflow = values.length - visible.length;
|
|
724
901
|
const opts = this.normalizedOptions;
|
|
725
902
|
|
|
@@ -758,11 +935,42 @@ export class RelSelect extends LitElement {
|
|
|
758
935
|
`;
|
|
759
936
|
}
|
|
760
937
|
|
|
938
|
+
/**
|
|
939
|
+
* Hidden, unconstrained copy of every selected chip used purely for width
|
|
940
|
+
* measurement (see computeVisibleChips). Chips mirror the visible ones'
|
|
941
|
+
* width-affecting props (removable adds the ✕ icon). The trailing chip is
|
|
942
|
+
* the widest "+N" badge that could ever be needed.
|
|
943
|
+
*/
|
|
944
|
+
private renderChipsMeasure() {
|
|
945
|
+
const values = this.selectedValues;
|
|
946
|
+
const opts = this.normalizedOptions;
|
|
947
|
+
return html`
|
|
948
|
+
<span class="select-chips-measure" aria-hidden="true">
|
|
949
|
+
${values.map((v) => {
|
|
950
|
+
const opt = opts.find((o) => o.value === v);
|
|
951
|
+
return html`
|
|
952
|
+
<rel-chip
|
|
953
|
+
label="${opt?.label ?? v}"
|
|
954
|
+
size="sm"
|
|
955
|
+
variant="subtle"
|
|
956
|
+
border="default"
|
|
957
|
+
?removable=${this.state !== "disabled"}
|
|
958
|
+
></rel-chip>
|
|
959
|
+
`;
|
|
960
|
+
})}
|
|
961
|
+
<rel-chip label="+${values.length}" size="sm" variant="fill" border="default"></rel-chip>
|
|
962
|
+
</span>
|
|
963
|
+
`;
|
|
964
|
+
}
|
|
965
|
+
|
|
761
966
|
render() {
|
|
762
967
|
const opts = this.normalizedOptions;
|
|
763
968
|
const filtered = this.filteredOptions;
|
|
764
969
|
const isDisabled = this.state === "disabled";
|
|
970
|
+
const labelHidden = this.labelStyle === "none";
|
|
765
971
|
const showFloatingChrome = this.labelStyle === "floating" && !!this.label;
|
|
972
|
+
// With no visible label the ARIA name comes from fieldAriaLabel instead.
|
|
973
|
+
const accessibleName = (labelHidden ? this.fieldAriaLabel : this.label) || "";
|
|
766
974
|
|
|
767
975
|
const wrapperClasses = [
|
|
768
976
|
"select-wrapper",
|
|
@@ -785,9 +993,11 @@ export class RelSelect extends LitElement {
|
|
|
785
993
|
|
|
786
994
|
return html`
|
|
787
995
|
<div class="${wrapperClasses}">
|
|
788
|
-
${
|
|
789
|
-
? html`<
|
|
790
|
-
:
|
|
996
|
+
${labelHidden
|
|
997
|
+
? html`<div class="label-reserve" aria-hidden="true"></div>`
|
|
998
|
+
: !showFloatingChrome && this.label
|
|
999
|
+
? html`<label part="label" class="select-label" for="${this.uniqueId}">${this.label}</label>`
|
|
1000
|
+
: ""}
|
|
791
1001
|
|
|
792
1002
|
<div class="select-container">
|
|
793
1003
|
${showFloatingChrome
|
|
@@ -806,7 +1016,8 @@ export class RelSelect extends LitElement {
|
|
|
806
1016
|
aria-controls="${listboxId}"
|
|
807
1017
|
aria-activedescendant="${activeOptionId}"
|
|
808
1018
|
aria-invalid="${this.state === "error"}"
|
|
809
|
-
aria-label="${this.
|
|
1019
|
+
aria-label="${this.fieldAriaLabelledby ? nothing : accessibleName || this.placeholder}"
|
|
1020
|
+
aria-labelledby="${this.fieldAriaLabelledby ?? nothing}"
|
|
810
1021
|
?disabled="${isDisabled}"
|
|
811
1022
|
@click="${this.onTriggerClick}"
|
|
812
1023
|
@keydown="${this.onTriggerKeyDown}"
|
|
@@ -835,6 +1046,8 @@ export class RelSelect extends LitElement {
|
|
|
835
1046
|
class="icon ${this.rightIcon ? "" : "chevron"}"
|
|
836
1047
|
></rel-icon>
|
|
837
1048
|
</button>
|
|
1049
|
+
|
|
1050
|
+
${this.multiple && this.selectedValues.length > 0 ? this.renderChipsMeasure() : nothing}
|
|
838
1051
|
</div>
|
|
839
1052
|
|
|
840
1053
|
${this.supportingText
|
|
@@ -849,7 +1062,7 @@ export class RelSelect extends LitElement {
|
|
|
849
1062
|
part="panel list" class="select-listbox"
|
|
850
1063
|
role="listbox"
|
|
851
1064
|
aria-multiselectable="${this.multiple}"
|
|
852
|
-
aria-label="${
|
|
1065
|
+
aria-label="${accessibleName || "Options"}"
|
|
853
1066
|
style="${this.panelStyle}"
|
|
854
1067
|
?data-open="${this.open}"
|
|
855
1068
|
>
|
|
@@ -899,6 +1112,7 @@ export class RelSelect extends LitElement {
|
|
|
899
1112
|
>
|
|
900
1113
|
<rel-checkbox
|
|
901
1114
|
label="${option.label}"
|
|
1115
|
+
icon="${option.icon}"
|
|
902
1116
|
supportingtext="${option.supportingText}"
|
|
903
1117
|
status="${this.selectedValues.includes(option.value) ? "checked" : "unchecked"}"
|
|
904
1118
|
@rel-change=${(e: Event) => {
|
|
@@ -926,7 +1140,16 @@ export class RelSelect extends LitElement {
|
|
|
926
1140
|
@click="${() => this.selectSingleOption(option)}"
|
|
927
1141
|
@pointerenter="${() => (this.highlightedIndex = index)}"
|
|
928
1142
|
>
|
|
929
|
-
<span class="select-option-
|
|
1143
|
+
<span class="select-option-content">
|
|
1144
|
+
${option.icon
|
|
1145
|
+
? html`<rel-icon
|
|
1146
|
+
name="${option.icon}"
|
|
1147
|
+
size="sm"
|
|
1148
|
+
class="select-option-icon"
|
|
1149
|
+
></rel-icon>`
|
|
1150
|
+
: nothing}
|
|
1151
|
+
<span class="select-option-label">${option.label}</span>
|
|
1152
|
+
</span>
|
|
930
1153
|
${option.value === this.selected
|
|
931
1154
|
? html`<rel-icon name="check" size="sm" class="select-option-check"></rel-icon>`
|
|
932
1155
|
: ""}
|
|
@@ -41,10 +41,19 @@ export class RelModal extends LitElement {
|
|
|
41
41
|
bottom: 0;
|
|
42
42
|
z-index: 1000;
|
|
43
43
|
pointer-events: none;
|
|
44
|
+
/* pointer-events:none only blocks the mouse — the closed modal's
|
|
45
|
+
close button and slotted content would otherwise stay in the Tab
|
|
46
|
+
order and swallow keyboard focus into invisible UI. visibility
|
|
47
|
+
also removes them from the tab order; the transition delays the
|
|
48
|
+
flip to hidden until the close animation has played. */
|
|
49
|
+
visibility: hidden;
|
|
50
|
+
transition: visibility 0s linear var(--modal-transition-duration);
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
.modal-container.open {
|
|
47
54
|
pointer-events: auto;
|
|
55
|
+
visibility: visible;
|
|
56
|
+
transition-delay: 0s;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
.overlay {
|
|
@@ -291,6 +300,17 @@ export class RelModal extends LitElement {
|
|
|
291
300
|
document.removeEventListener("keydown", this.handleKeydown);
|
|
292
301
|
}
|
|
293
302
|
|
|
303
|
+
protected willUpdate() {
|
|
304
|
+
// `title` doubles as HTML's global tooltip attribute: leaving it on the
|
|
305
|
+
// host shows a native tooltip anywhere over the modal, including the
|
|
306
|
+
// full-viewport backdrop. Sync it into the property, then strip it.
|
|
307
|
+
if (this.hasAttribute("title")) {
|
|
308
|
+
const value = this.getAttribute("title") ?? "";
|
|
309
|
+
this.removeAttribute("title");
|
|
310
|
+
this.title = value;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
updated(changed: Map<string, unknown>) {
|
|
295
315
|
if (changed.has("open")) {
|
|
296
316
|
if (this.open) {
|
|
@@ -18,6 +18,13 @@ A text input field component with support for labels, icons, validation states,
|
|
|
18
18
|
supportingText="Must be at least 8 characters">
|
|
19
19
|
</rel-textfield>
|
|
20
20
|
|
|
21
|
+
<!-- No label — an accessible name is required in its place -->
|
|
22
|
+
<rel-textfield
|
|
23
|
+
label-style="none"
|
|
24
|
+
placeholder="Search orders"
|
|
25
|
+
field-aria-label="Search orders">
|
|
26
|
+
</rel-textfield>
|
|
27
|
+
|
|
21
28
|
<!-- Setting value via JavaScript -->
|
|
22
29
|
<rel-textfield id="myField" label="Username"></rel-textfield>
|
|
23
30
|
<script>
|
|
@@ -33,6 +40,10 @@ A text input field component with support for labels, icons, validation states,
|
|
|
33
40
|
| Name | Type | Default | Description |
|
|
34
41
|
|------|------|---------|-------------|
|
|
35
42
|
| `label` | `string` | - | Label text displayed above the input |
|
|
43
|
+
| `label-style` | `"default" \| "floating" \| "none"` | - | Label presentation. `"none"` renders no label at all. Unset, the deprecated `floatingLabel` boolean decides |
|
|
44
|
+
| `floatingLabel` | `boolean` | `false` | **Deprecated** — use `label-style="floating"`. Kept permanently as an alias |
|
|
45
|
+
| `field-aria-label` | `string` | - | Accessible name for the input. Required with `label-style="none"` |
|
|
46
|
+
| `field-aria-labelledby` | `string` | - | IDREF list of external element(s) naming the input. Wins over `field-aria-label` |
|
|
36
47
|
| `placeholder` | `string` | `""` | Placeholder text |
|
|
37
48
|
| `type` | `string` | `"text"` | Input type (text, email, password, etc.) |
|
|
38
49
|
| `value` | `string` | `""` | Current input value |
|
|
@@ -72,10 +83,15 @@ None exposed.
|
|
|
72
83
|
- `--rel-bg-neutral-elevated-1`: Disabled background
|
|
73
84
|
- `--rel-spacing-*`: Spacing tokens
|
|
74
85
|
- `--rel-border-radius-sm`: Border radius
|
|
86
|
+
- `--rel-field-label-reserve`: Height held for the absent label row under `label-style="none"`, so the input 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)
|
|
75
87
|
|
|
76
88
|
## Accessibility
|
|
77
89
|
|
|
78
90
|
- Associates `<label>` with input via `for`/`id`
|
|
91
|
+
- With `label-style="none"` no `<label>` renders, so `field-aria-label` (or
|
|
92
|
+
`field-aria-labelledby`) must supply the accessible name — a placeholder is
|
|
93
|
+
not a valid naming mechanism. The component logs a console warning when both
|
|
94
|
+
are missing.
|
|
79
95
|
- Supports native HTML5 input attributes (`required`, `disabled`, `readonly`, `maxlength`)
|
|
80
96
|
- Focus ring visible on keyboard navigation (`:focus-visible`)
|
|
81
97
|
- Error state communicated visually via border color
|