@reliability-design/web 1.4.5 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/rel-alert/rel-alert.ts +1 -1
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
- package/dist/components/rel-badge/rel-badge.ts +6 -1
- package/dist/components/rel-calendar/rel-calendar.ts +64 -18
- package/dist/components/rel-carousel/rel-carousel.ts +25 -2
- package/dist/components/rel-chip/rel-chip.ts +31 -20
- package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
- package/dist/components/rel-date-picker/date-parse.ts +178 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +529 -90
- package/dist/components/rel-drawer/rel-drawer.ts +6 -3
- package/dist/components/rel-menu/rel-menu.ts +6 -0
- package/dist/components/rel-otp/rel-otp.ts +12 -2
- package/dist/components/rel-pagination/rel-pagination.ts +13 -0
- package/dist/components/rel-popover/rel-popover.ts +33 -2
- package/dist/components/rel-progress/rel-progress.ts +13 -4
- package/dist/components/rel-radio/README.md +6 -3
- package/dist/components/rel-radio/rel-radio.ts +236 -60
- package/dist/components/rel-range-picker/rel-range-picker.ts +452 -95
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +5 -1
- package/dist/components/rel-select/rel-select.ts +44 -13
- package/dist/components/rel-sheet/rel-sheet.ts +163 -18
- package/dist/components/rel-sidebar/rel-sidebar.ts +7 -1
- package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
- package/dist/components/rel-slider/rel-slider.ts +6 -3
- package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
- package/dist/components/rel-stepper/rel-stepper.ts +3 -1
- package/dist/components/rel-switch/rel-switch.ts +3 -1
- package/dist/components/rel-textfield/rel-textfield.ts +263 -75
- package/dist/components/rel-time-picker/rel-time-picker.ts +450 -94
- package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
- package/dist/components/rel-time-picker/time-parse.ts +85 -0
- package/dist/components/rel-toast/rel-toast.ts +4 -2
- package/dist/components/rel-tooltip/README.md +13 -5
- package/dist/components/rel-tooltip/rel-tooltip.ts +3 -0
- package/dist/components/rel-upload/rel-upload.ts +25 -0
- package/dist/index.d.mts +241 -11
- package/dist/index.d.ts +241 -11
- package/dist/index.js +2573 -598
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2614 -640
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -467,6 +467,7 @@ export class RelStepper extends LitElement {
|
|
|
467
467
|
viewBox="0 0 18 14"
|
|
468
468
|
fill="none"
|
|
469
469
|
class="check-icon"
|
|
470
|
+
aria-hidden="true"
|
|
470
471
|
>
|
|
471
472
|
<path
|
|
472
473
|
d="M6.44479 14L0.5 7.36383L1.9862 5.70478L6.44479 10.6819L16.0138 0L17.5 1.65904L6.44479 14Z"
|
|
@@ -498,13 +499,14 @@ export class RelStepper extends LitElement {
|
|
|
498
499
|
const isTextRight = placement !== "inverted";
|
|
499
500
|
|
|
500
501
|
return html`
|
|
501
|
-
<div class="${stepperClasses}">
|
|
502
|
+
<div class="${stepperClasses}" role="group" aria-label="Progress steps">
|
|
502
503
|
${this.steps.map(
|
|
503
504
|
(step, idx) => html`
|
|
504
505
|
<div
|
|
505
506
|
class="${this.getStepClasses(step)}"
|
|
506
507
|
role="button"
|
|
507
508
|
tabindex="${this.isDisabled(step) ? -1 : 0}"
|
|
509
|
+
aria-label="${step.title}, step ${idx + 1} of ${this.steps.length}${this.isDone(step) ? ", completed" : ""}"
|
|
508
510
|
aria-disabled="${this.isDisabled(step) ? "true" : nothing}"
|
|
509
511
|
aria-current="${this.isProgress(step) ? "step" : nothing}"
|
|
510
512
|
@click="${() => this.onStepClick(step, idx)}"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LitElement, html, css } from "lit";
|
|
1
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property, state } from "lit/decorators.js";
|
|
3
3
|
|
|
4
4
|
@customElement("rel-switch")
|
|
@@ -6,6 +6,7 @@ export class RelSwitch extends LitElement {
|
|
|
6
6
|
@property({ type: Boolean }) checked = false;
|
|
7
7
|
@property({ type: Boolean }) disabled = false;
|
|
8
8
|
@property({ type: String }) value = "";
|
|
9
|
+
@property({ type: String, attribute: "field-aria-label" }) fieldAriaLabel?: string;
|
|
9
10
|
|
|
10
11
|
private _onChange: (value: boolean) => void = () => {};
|
|
11
12
|
private _onTouched: () => void = () => {};
|
|
@@ -182,6 +183,7 @@ export class RelSwitch extends LitElement {
|
|
|
182
183
|
type="checkbox"
|
|
183
184
|
role="switch"
|
|
184
185
|
aria-checked="${this.checked}"
|
|
186
|
+
aria-label="${this.fieldAriaLabel || nothing}"
|
|
185
187
|
?disabled=${this.disabled}
|
|
186
188
|
.checked=${this.checked}
|
|
187
189
|
@change=${(e: Event) => this.toggle(e)}
|
|
@@ -5,6 +5,13 @@ import "@reliability-design/icons/rel-icon";
|
|
|
5
5
|
export type TextFieldState = "default" | "error" | "disabled";
|
|
6
6
|
export type TextFieldSize = "small" | "medium" | "large";
|
|
7
7
|
export type TextFieldVariant = "default" | "underline" | "legend";
|
|
8
|
+
/**
|
|
9
|
+
* How the prefix block is drawn (Figma `.Preffix` → Style):
|
|
10
|
+
* - `"clear"` — bare icon/text, no separator (the legacy `leftIcon` look)
|
|
11
|
+
* - `"divider"` — hairline between the prefix and the input
|
|
12
|
+
* - `"fill"` — tinted block flush with the control's left edge
|
|
13
|
+
*/
|
|
14
|
+
export type TextFieldPrefixStyle = "clear" | "divider" | "fill";
|
|
8
15
|
|
|
9
16
|
/**
|
|
10
17
|
* How a field presents its label. Shared by `<rel-textfield>`, `<rel-select>`
|
|
@@ -29,6 +36,18 @@ export class RelTextfield extends LitElement {
|
|
|
29
36
|
@property({ type: Boolean, attribute: "clickable-right-icon" }) clickableRightIcon = false;
|
|
30
37
|
/** Accessible label for the trailing icon when it is clickable. */
|
|
31
38
|
@property({ type: String, attribute: "right-icon-aria-label" }) rightIconAriaLabel?: string;
|
|
39
|
+
// ── Prefix / suffix labels (Figma .Preffix / .Suffix) ───────────────────────
|
|
40
|
+
/** Short text rendered before the input, e.g. `"http://"` or `"$"`. Sits in
|
|
41
|
+
* the same prefix block as `prefixIcon` and is styled by `prefixStyle`. */
|
|
42
|
+
@property({ type: String, attribute: "prefix-text" }) prefixText?: string;
|
|
43
|
+
/** Visual treatment of the prefix block — applies to the prefix icon, text
|
|
44
|
+
* and slotted `prefix` content alike. See `TextFieldPrefixStyle`. */
|
|
45
|
+
@property({ type: String, attribute: "prefix-style" }) prefixStyle: TextFieldPrefixStyle = "clear";
|
|
46
|
+
/** Short text rendered after the input, e.g. `"kg"` or `".00"`. Shares the
|
|
47
|
+
* suffix block with `rightIcon` and slotted `suffix` content. */
|
|
48
|
+
@property({ type: String, attribute: "suffix-text" }) suffixText?: string;
|
|
49
|
+
/** Draws a hairline between the input and the suffix block. */
|
|
50
|
+
@property({ type: Boolean, attribute: "suffix-divider" }) suffixDivider = false;
|
|
32
51
|
@property({ type: String }) supportingText?: string;
|
|
33
52
|
@property({ type: String }) state: TextFieldState = "default";
|
|
34
53
|
@property({ type: String }) size: TextFieldSize = "medium";
|
|
@@ -69,7 +88,10 @@ export class RelTextfield extends LitElement {
|
|
|
69
88
|
@property({ type: Boolean, reflect: true }) fullWidth = true;
|
|
70
89
|
|
|
71
90
|
@state() private internalState: "default" | "focused" = "default";
|
|
91
|
+
/** Whether the `prefix` / `suffix` named slots currently have content. */
|
|
92
|
+
@state() private slotted = { prefix: false, suffix: false };
|
|
72
93
|
private _wasError = false;
|
|
94
|
+
private _prefixObserver?: ResizeObserver;
|
|
73
95
|
|
|
74
96
|
private uniqueId = `textfield-${Math.random().toString(36).slice(2)}`;
|
|
75
97
|
|
|
@@ -227,17 +249,16 @@ export class RelTextfield extends LitElement {
|
|
|
227
249
|
padding-bottom: var(--rel-spacing-sm);
|
|
228
250
|
}
|
|
229
251
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
:host([floatinglabel]) .textfield-wrapper.
|
|
239
|
-
|
|
240
|
-
left: var(--rel-spacing-sm);
|
|
252
|
+
/* The floating label lives inside .textfield-field (the input's own flex
|
|
253
|
+
column), so at rest it naturally sits after any prefix block. Once
|
|
254
|
+
floated, a "clear" prefix (bare icon — the legacy leftIcon look) lets the
|
|
255
|
+
label slide back over the control's left edge, matching Figma's
|
|
256
|
+
"Floating Label" frame at x=12; --_rel-tf-prefix-w is the measured
|
|
257
|
+
prefix width (ResizeObserver). Divider/fill prefixes are solid blocks,
|
|
258
|
+
so the floated label stays in the field column, after them. */
|
|
259
|
+
:host([floatinglabel]) .textfield-wrapper.focused .prefix-clear .textfield-label,
|
|
260
|
+
:host([floatinglabel]) .textfield-wrapper.has-value .prefix-clear .textfield-label {
|
|
261
|
+
left: calc(var(--rel-spacing-sm) - var(--_rel-tf-prefix-w, 0px));
|
|
241
262
|
}
|
|
242
263
|
|
|
243
264
|
:host([floatinglabel]) .textfield-wrapper.disabled .textfield-label {
|
|
@@ -266,9 +287,22 @@ export class RelTextfield extends LitElement {
|
|
|
266
287
|
align-items: flex-start;
|
|
267
288
|
}
|
|
268
289
|
|
|
290
|
+
/* The input's own column: prefix | field | suffix. Positioned so the
|
|
291
|
+
floating label anchors to the input area, not the whole control. */
|
|
292
|
+
.textfield-field {
|
|
293
|
+
position: relative;
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
align-self: stretch;
|
|
297
|
+
flex: 1;
|
|
298
|
+
min-width: 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
269
301
|
/* Input */
|
|
270
302
|
.textfield-input {
|
|
271
303
|
flex: 1;
|
|
304
|
+
min-width: 0;
|
|
305
|
+
width: 100%;
|
|
272
306
|
border: none;
|
|
273
307
|
outline: none;
|
|
274
308
|
padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
|
|
@@ -305,24 +339,113 @@ export class RelTextfield extends LitElement {
|
|
|
305
339
|
border-radius: 2px;
|
|
306
340
|
}
|
|
307
341
|
|
|
308
|
-
.
|
|
309
|
-
|
|
342
|
+
/* ── Prefix / suffix blocks (Figma .Preffix / .Suffix) ──
|
|
343
|
+
Flex children of the control, stretched to its height. Each holds an
|
|
344
|
+
icon and/or a short text label plus a named slot for custom content
|
|
345
|
+
(a unit dropdown, an action button…). Colour follows Figma: text-default
|
|
346
|
+
for both icon and text, like rel-select's trigger icons. */
|
|
347
|
+
.textfield-affix {
|
|
310
348
|
display: flex;
|
|
311
|
-
|
|
312
|
-
|
|
349
|
+
align-items: center;
|
|
350
|
+
align-self: stretch;
|
|
351
|
+
flex: none;
|
|
352
|
+
gap: var(--rel-spacing-xs);
|
|
353
|
+
box-sizing: border-box;
|
|
354
|
+
color: var(--rel-text-default);
|
|
355
|
+
white-space: nowrap;
|
|
356
|
+
transition: border-color 0.2s, background 0.2s;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.textfield-affix.hidden {
|
|
360
|
+
display: none;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.textfield-affix rel-icon {
|
|
364
|
+
--rel-icon-size: 18px;
|
|
365
|
+
flex: none;
|
|
366
|
+
}
|
|
367
|
+
.textfield-wrapper.small .textfield-affix rel-icon {
|
|
368
|
+
--rel-icon-size: 16px;
|
|
369
|
+
}
|
|
370
|
+
.textfield-wrapper.large .textfield-affix rel-icon {
|
|
371
|
+
--rel-icon-size: 20px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.affix-text {
|
|
375
|
+
font-family: var(--rel-body-md-medium-font-family);
|
|
376
|
+
font-weight: var(--rel-body-md-medium-font-weight);
|
|
377
|
+
font-size: var(--rel-body-md-medium-font-size);
|
|
378
|
+
line-height: var(--rel-body-md-medium-line-height);
|
|
379
|
+
letter-spacing: var(--rel-body-md-medium-letter-spacing);
|
|
380
|
+
}
|
|
381
|
+
.textfield-wrapper.small .affix-text {
|
|
382
|
+
font-family: var(--rel-body-sm-medium-font-family);
|
|
383
|
+
font-weight: var(--rel-body-sm-medium-font-weight);
|
|
384
|
+
font-size: var(--rel-body-sm-medium-font-size);
|
|
385
|
+
line-height: var(--rel-body-sm-medium-line-height);
|
|
386
|
+
letter-spacing: var(--rel-body-sm-medium-letter-spacing);
|
|
387
|
+
}
|
|
388
|
+
.textfield-wrapper.large .affix-text {
|
|
389
|
+
font-family: var(--rel-body-lg-medium-font-family);
|
|
390
|
+
font-weight: var(--rel-body-lg-medium-font-weight);
|
|
391
|
+
font-size: var(--rel-body-lg-medium-font-size);
|
|
392
|
+
line-height: var(--rel-body-lg-medium-line-height);
|
|
393
|
+
letter-spacing: var(--rel-body-lg-medium-letter-spacing);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* Prefix — Style=Clear: bare; icon-only keeps the input's own left padding
|
|
397
|
+
as the gap, text pads both sides. */
|
|
398
|
+
.textfield-affix.prefix {
|
|
399
|
+
padding-inline: var(--rel-spacing-sm) 0;
|
|
400
|
+
}
|
|
401
|
+
.textfield-affix.prefix.has-text {
|
|
402
|
+
padding-inline: var(--rel-spacing-sm);
|
|
403
|
+
}
|
|
404
|
+
/* Style=Divider: hairline on the right */
|
|
405
|
+
.textfield-affix.prefix.divider {
|
|
406
|
+
padding-inline: var(--rel-spacing-sm);
|
|
407
|
+
border-right: 1px solid var(--rel-border-default);
|
|
408
|
+
}
|
|
409
|
+
/* Style=Fill: tinted block flush with the control's edge, sm gap after */
|
|
410
|
+
.textfield-affix.prefix.fill {
|
|
411
|
+
padding-inline: var(--rel-spacing-sm);
|
|
412
|
+
margin-right: var(--rel-spacing-sm);
|
|
413
|
+
background: var(--rel-bg-neutral-elevated-2);
|
|
414
|
+
border-radius: calc(var(--rel-border-radius-xs) - 1px) 0 0
|
|
415
|
+
calc(var(--rel-border-radius-xs) - 1px);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* Suffix — Divider=False: pl xs / pr sm; Divider=True: hairline on the left */
|
|
419
|
+
.textfield-affix.suffix {
|
|
420
|
+
padding-inline: var(--rel-spacing-xs) var(--rel-spacing-sm);
|
|
421
|
+
}
|
|
422
|
+
.textfield-affix.suffix.divider {
|
|
423
|
+
padding-inline: var(--rel-spacing-sm);
|
|
424
|
+
border-left: 1px solid var(--rel-border-default);
|
|
313
425
|
}
|
|
314
426
|
|
|
315
|
-
|
|
316
|
-
|
|
427
|
+
/* Size=S tightens every inline pad to xs */
|
|
428
|
+
.textfield-wrapper.small .textfield-affix.prefix {
|
|
429
|
+
padding-inline: var(--rel-spacing-xs) 0;
|
|
430
|
+
}
|
|
431
|
+
.textfield-wrapper.small .textfield-affix.prefix.has-text,
|
|
432
|
+
.textfield-wrapper.small .textfield-affix.prefix.divider,
|
|
433
|
+
.textfield-wrapper.small .textfield-affix.prefix.fill,
|
|
434
|
+
.textfield-wrapper.small .textfield-affix.suffix,
|
|
435
|
+
.textfield-wrapper.small .textfield-affix.suffix.divider {
|
|
436
|
+
padding-inline: var(--rel-spacing-xs);
|
|
317
437
|
}
|
|
318
438
|
|
|
319
|
-
.
|
|
320
|
-
|
|
439
|
+
.textfield-wrapper.disabled .textfield-affix {
|
|
440
|
+
color: var(--rel-text-disabled);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.icon {
|
|
444
|
+
display: flex;
|
|
321
445
|
}
|
|
322
446
|
|
|
323
447
|
/* Interactive trailing icon (e.g. password show/hide) */
|
|
324
448
|
.icon.right.clickable {
|
|
325
|
-
pointer-events: auto;
|
|
326
449
|
cursor: pointer;
|
|
327
450
|
border-radius: var(--rel-border-radius-xs, 4px);
|
|
328
451
|
}
|
|
@@ -365,15 +488,6 @@ export class RelTextfield extends LitElement {
|
|
|
365
488
|
letter-spacing: var(--rel-body-lg-regular-letter-spacing);
|
|
366
489
|
}
|
|
367
490
|
|
|
368
|
-
/* Adjust padding when icons are present */
|
|
369
|
-
.textfield-wrapper .textfield-input.has-left-icon {
|
|
370
|
-
padding-left: var(--rel-spacing-2xl);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
.textfield-wrapper .textfield-input.has-right-icon {
|
|
374
|
-
padding-right: var(--rel-spacing-2xl);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
491
|
/* States */
|
|
378
492
|
/* Hover (Figma State=Hover): border-strong at 1px — no focus ring */
|
|
379
493
|
.textfield-wrapper:not(.focused):not(.error):not(.disabled) .textfield-container:hover {
|
|
@@ -428,6 +542,10 @@ export class RelTextfield extends LitElement {
|
|
|
428
542
|
color: var(--rel-text-danger);
|
|
429
543
|
}
|
|
430
544
|
|
|
545
|
+
.textfield-support.empty {
|
|
546
|
+
margin-top: calc(-1 * var(--rel-spacing-2xs));
|
|
547
|
+
}
|
|
548
|
+
|
|
431
549
|
/* Placeholder */
|
|
432
550
|
.textfield-input::placeholder {
|
|
433
551
|
color: var(--rel-text-placeholder);
|
|
@@ -536,15 +654,15 @@ export class RelTextfield extends LitElement {
|
|
|
536
654
|
display: none;
|
|
537
655
|
}
|
|
538
656
|
|
|
539
|
-
/*
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
657
|
+
/* Legend: prefix | field | suffix sit in a flex row under the legend. The
|
|
658
|
+
fieldset's content box starts below the legend's midline, so a fill
|
|
659
|
+
prefix can't reach the top border here — square it off instead. */
|
|
660
|
+
:host([variant="legend"]) .field-row {
|
|
661
|
+
display: flex;
|
|
662
|
+
align-items: center;
|
|
544
663
|
}
|
|
545
|
-
:host([variant="legend"]) .
|
|
546
|
-
|
|
547
|
-
transform: none;
|
|
664
|
+
:host([variant="legend"]) .textfield-affix.prefix.fill {
|
|
665
|
+
border-radius: 0;
|
|
548
666
|
}
|
|
549
667
|
|
|
550
668
|
/* Hover */
|
|
@@ -625,6 +743,48 @@ export class RelTextfield extends LitElement {
|
|
|
625
743
|
if (leftIcon) this.leftIcon = leftIcon;
|
|
626
744
|
const rightIcon = this.getAttribute('righticon') || this.getAttribute('rightIcon');
|
|
627
745
|
if (rightIcon) this.rightIcon = rightIcon;
|
|
746
|
+
// Seed slot presence from static markup; slotchange keeps it current after.
|
|
747
|
+
this.slotted = {
|
|
748
|
+
prefix: !!this.querySelector(':scope > [slot="prefix"]'),
|
|
749
|
+
suffix: !!this.querySelector(':scope > [slot="suffix"]'),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
disconnectedCallback() {
|
|
754
|
+
super.disconnectedCallback();
|
|
755
|
+
this._prefixObserver?.disconnect();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private handleSlotChange(e: Event) {
|
|
759
|
+
const slot = e.target as HTMLSlotElement;
|
|
760
|
+
const key = slot.name as "prefix" | "suffix";
|
|
761
|
+
const has = slot
|
|
762
|
+
.assignedNodes({ flatten: true })
|
|
763
|
+
.some((n) => n.nodeType === Node.ELEMENT_NODE || (n.textContent ?? "").trim() !== "");
|
|
764
|
+
if (this.slotted[key] !== has) this.slotted = { ...this.slotted, [key]: has };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Clicking the (non-interactive) prefix/suffix area focuses the input, as a
|
|
768
|
+
* label would. Clickable icons and slotted controls keep their own clicks. */
|
|
769
|
+
private handleAffixClick(e: Event) {
|
|
770
|
+
const target = e.target as HTMLElement;
|
|
771
|
+
if (target.closest(".clickable") || target.assignedSlot || target.closest("[slot]")) return;
|
|
772
|
+
this.focus();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Publishes the prefix block's width as --_rel-tf-prefix-w on its parent so
|
|
776
|
+
* the floated label can slide back over a clear-style prefix (see CSS). */
|
|
777
|
+
private observePrefix() {
|
|
778
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
779
|
+
const prefix = this.renderRoot.querySelector<HTMLElement>(".textfield-affix.prefix");
|
|
780
|
+
this._prefixObserver ??= new ResizeObserver((entries) => {
|
|
781
|
+
for (const entry of entries) {
|
|
782
|
+
const el = entry.target as HTMLElement;
|
|
783
|
+
el.parentElement?.style.setProperty("--_rel-tf-prefix-w", `${el.offsetWidth}px`);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
this._prefixObserver.disconnect();
|
|
787
|
+
if (prefix) this._prefixObserver.observe(prefix);
|
|
628
788
|
}
|
|
629
789
|
|
|
630
790
|
willUpdate(changed: PropertyValues<this>) {
|
|
@@ -639,6 +799,7 @@ export class RelTextfield extends LitElement {
|
|
|
639
799
|
private _warnedNoName = false;
|
|
640
800
|
|
|
641
801
|
updated() {
|
|
802
|
+
this.observePrefix();
|
|
642
803
|
if (this._warnedNoName || !this.labelHidden) return;
|
|
643
804
|
if (this.fieldAriaLabel || this.fieldAriaLabelledby || this.getAttribute("aria-label")) return;
|
|
644
805
|
this._warnedNoName = true;
|
|
@@ -712,7 +873,7 @@ export class RelTextfield extends LitElement {
|
|
|
712
873
|
// errorMessage replaces the helper only when in error AND explicitly set;
|
|
713
874
|
// otherwise the (deprecated) supportingText/helperText shows as before.
|
|
714
875
|
const belowText = isError && this.errorMessage ? this.errorMessage : this.helper;
|
|
715
|
-
const describedById =
|
|
876
|
+
const describedById = `${inputId}-desc`;
|
|
716
877
|
|
|
717
878
|
const wrapperClasses = [
|
|
718
879
|
"textfield-wrapper",
|
|
@@ -723,13 +884,14 @@ export class RelTextfield extends LitElement {
|
|
|
723
884
|
this.value ? "has-value" : "",
|
|
724
885
|
].filter(Boolean).join(" ");
|
|
725
886
|
|
|
726
|
-
const inputClasses =
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
887
|
+
const inputClasses = "textfield-input";
|
|
888
|
+
|
|
889
|
+
// Prefix / suffix blocks. Hidden (not removed) in multiline mode so the
|
|
890
|
+
// named slots stay mounted and slotchange keeps tracking content.
|
|
891
|
+
const affixesShown = !this.multiline;
|
|
892
|
+
const hasPrefix = affixesShown && !!(leadingIcon || this.prefixText || this.slotted.prefix);
|
|
893
|
+
const hasSuffix = affixesShown && !!(this.rightIcon || this.suffixText || this.slotted.suffix);
|
|
894
|
+
const prefixClear = hasPrefix && this.prefixStyle === "clear";
|
|
733
895
|
|
|
734
896
|
const labelStyle = this.resolvedLabelStyle;
|
|
735
897
|
const labelHidden = labelStyle === "none";
|
|
@@ -768,7 +930,7 @@ export class RelTextfield extends LitElement {
|
|
|
768
930
|
aria-required="${this.required}"
|
|
769
931
|
aria-label="${ariaLabel ?? nothing}"
|
|
770
932
|
aria-labelledby="${ariaLabelledby ?? nothing}"
|
|
771
|
-
aria-describedby="${describedById
|
|
933
|
+
aria-describedby="${describedById}"
|
|
772
934
|
@focus="${this.handleFocus}"
|
|
773
935
|
@blur="${this.handleBlur}"
|
|
774
936
|
@input="${this.handleInput}"
|
|
@@ -788,7 +950,7 @@ export class RelTextfield extends LitElement {
|
|
|
788
950
|
aria-required="${this.required}"
|
|
789
951
|
aria-label="${ariaLabel ?? nothing}"
|
|
790
952
|
aria-labelledby="${ariaLabelledby ?? nothing}"
|
|
791
|
-
aria-describedby="${describedById
|
|
953
|
+
aria-describedby="${describedById}"
|
|
792
954
|
@focus="${this.handleFocus}"
|
|
793
955
|
@blur="${this.handleBlur}"
|
|
794
956
|
@input="${this.handleInput}"
|
|
@@ -797,7 +959,6 @@ export class RelTextfield extends LitElement {
|
|
|
797
959
|
const leadingIconTemplate = leadingIcon
|
|
798
960
|
? html`<rel-icon
|
|
799
961
|
name="${leadingIcon}"
|
|
800
|
-
size="sm"
|
|
801
962
|
class="icon left"
|
|
802
963
|
role="${this.prefixIconAriaLabel ? "img" : nothing}"
|
|
803
964
|
aria-label="${this.prefixIconAriaLabel ?? nothing}"
|
|
@@ -805,12 +966,50 @@ export class RelTextfield extends LitElement {
|
|
|
805
966
|
></rel-icon>`
|
|
806
967
|
: "";
|
|
807
968
|
|
|
808
|
-
const
|
|
809
|
-
? html`<
|
|
810
|
-
|
|
811
|
-
|
|
969
|
+
const trailingIconTemplate = this.rightIcon
|
|
970
|
+
? html`<rel-icon
|
|
971
|
+
part="right-icon"
|
|
972
|
+
name="${this.rightIcon}"
|
|
973
|
+
class="icon right ${this.clickableRightIcon ? "clickable" : ""}"
|
|
974
|
+
role="${this.clickableRightIcon ? "button" : nothing}"
|
|
975
|
+
tabindex="${this.clickableRightIcon ? "0" : nothing}"
|
|
976
|
+
aria-label="${this.clickableRightIcon ? (this.rightIconAriaLabel ?? nothing) : nothing}"
|
|
977
|
+
aria-hidden="${this.clickableRightIcon ? nothing : "true"}"
|
|
978
|
+
@click="${(e: Event) => this.handleRightIconClick(e)}"
|
|
979
|
+
@keydown="${(e: KeyboardEvent) => this.handleRightIconKeydown(e)}"
|
|
980
|
+
></rel-icon>`
|
|
812
981
|
: "";
|
|
813
982
|
|
|
983
|
+
// Order inside each block: slotted content sits outermost (a unit dropdown
|
|
984
|
+
// at the far left, an action button at the far right), then icon, then text
|
|
985
|
+
// nearest the input.
|
|
986
|
+
const prefixClasses = [
|
|
987
|
+
"textfield-affix",
|
|
988
|
+
"prefix",
|
|
989
|
+
this.prefixStyle,
|
|
990
|
+
this.prefixText || this.slotted.prefix ? "has-text" : "",
|
|
991
|
+
hasPrefix ? "" : "hidden",
|
|
992
|
+
].filter(Boolean).join(" ");
|
|
993
|
+
const prefixTemplate = html`<div part="prefix" class="${prefixClasses}" @click="${this.handleAffixClick}">
|
|
994
|
+
<slot name="prefix" @slotchange="${this.handleSlotChange}"></slot>
|
|
995
|
+
${hasPrefix ? leadingIconTemplate : ""}
|
|
996
|
+
${hasPrefix && this.prefixText ? html`<span class="affix-text">${this.prefixText}</span>` : ""}
|
|
997
|
+
</div>`;
|
|
998
|
+
|
|
999
|
+
const suffixClasses = [
|
|
1000
|
+
"textfield-affix",
|
|
1001
|
+
"suffix",
|
|
1002
|
+
this.suffixDivider ? "divider" : "",
|
|
1003
|
+
hasSuffix ? "" : "hidden",
|
|
1004
|
+
].filter(Boolean).join(" ");
|
|
1005
|
+
const suffixTemplate = html`<div part="suffix" class="${suffixClasses}" @click="${this.handleAffixClick}">
|
|
1006
|
+
${hasSuffix && this.suffixText ? html`<span class="affix-text">${this.suffixText}</span>` : ""}
|
|
1007
|
+
${hasSuffix ? trailingIconTemplate : ""}
|
|
1008
|
+
<slot name="suffix" @slotchange="${this.handleSlotChange}"></slot>
|
|
1009
|
+
</div>`;
|
|
1010
|
+
|
|
1011
|
+
const belowTemplate = html`<div part="helper" class="${["textfield-support", isError ? "error" : "", belowText ? "" : "empty"].filter(Boolean).join(" ")}" id="${describedById}" aria-live="polite">${belowText ?? ""}</div>`;
|
|
1012
|
+
|
|
814
1013
|
if (this.variant === "legend") {
|
|
815
1014
|
const fieldsetClass = ["field-fieldset", this.multiline ? "multiline" : ""].filter(Boolean).join(" ");
|
|
816
1015
|
// The <legend> MUST be the fieldset's first child with no preceding
|
|
@@ -820,8 +1019,11 @@ export class RelTextfield extends LitElement {
|
|
|
820
1019
|
return html`
|
|
821
1020
|
<div class="${wrapperClasses}">
|
|
822
1021
|
<fieldset part="control" class="${fieldsetClass}"><legend class="field-legend" id="${legendId}">${labelHidden ? "" : this.label ?? ""}${this.required && !labelHidden ? html`<span class="required-mark" aria-hidden="true">*</span>` : ""}</legend>
|
|
823
|
-
|
|
824
|
-
|
|
1022
|
+
<div class="field-row">
|
|
1023
|
+
${prefixTemplate}
|
|
1024
|
+
<div class="textfield-field">${inputTemplate}</div>
|
|
1025
|
+
${suffixTemplate}
|
|
1026
|
+
</div>
|
|
825
1027
|
</fieldset>
|
|
826
1028
|
${belowTemplate}
|
|
827
1029
|
</div>
|
|
@@ -836,27 +1038,13 @@ export class RelTextfield extends LitElement {
|
|
|
836
1038
|
? labelTemplate()
|
|
837
1039
|
: ""}
|
|
838
1040
|
|
|
839
|
-
<div part="control" class="${["textfield-container", this.isDisabled ? "disabled" : "", this.multiline ? "multiline" : ""].filter(Boolean).join(" ")}">
|
|
840
|
-
${
|
|
841
|
-
|
|
842
|
-
: ""}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
${
|
|
846
|
-
|
|
847
|
-
${!this.multiline && this.rightIcon
|
|
848
|
-
? html`<rel-icon
|
|
849
|
-
part="right-icon"
|
|
850
|
-
name="${this.rightIcon}"
|
|
851
|
-
size="${this.clickableRightIcon ? "md" : "sm"}"
|
|
852
|
-
class="icon right ${this.clickableRightIcon ? "clickable" : ""}"
|
|
853
|
-
role="${this.clickableRightIcon ? "button" : nothing}"
|
|
854
|
-
tabindex="${this.clickableRightIcon ? "0" : nothing}"
|
|
855
|
-
aria-label="${this.clickableRightIcon ? (this.rightIconAriaLabel ?? nothing) : nothing}"
|
|
856
|
-
@click="${(e: Event) => this.handleRightIconClick(e)}"
|
|
857
|
-
@keydown="${(e: KeyboardEvent) => this.handleRightIconKeydown(e)}"
|
|
858
|
-
></rel-icon>`
|
|
859
|
-
: ""}
|
|
1041
|
+
<div part="control" class="${["textfield-container", this.isDisabled ? "disabled" : "", this.multiline ? "multiline" : "", prefixClear ? "prefix-clear" : ""].filter(Boolean).join(" ")}">
|
|
1042
|
+
${prefixTemplate}
|
|
1043
|
+
<div class="textfield-field">
|
|
1044
|
+
${labelStyle === "floating" ? labelTemplate() : ""}
|
|
1045
|
+
${inputTemplate}
|
|
1046
|
+
</div>
|
|
1047
|
+
${suffixTemplate}
|
|
860
1048
|
</div>
|
|
861
1049
|
|
|
862
1050
|
${belowTemplate}
|