@latty-ds/web 0.10.3 → 1.0.0

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.
Files changed (101) hide show
  1. package/custom-elements.json +1970 -229
  2. package/dist/components/accordion/index.js +36 -2
  3. package/dist/components/alert/alert.d.ts +29 -6
  4. package/dist/components/alert/alert.types.d.ts +5 -0
  5. package/dist/components/alert/index.js +127 -20
  6. package/dist/components/avatar/avatar.d.ts +2 -0
  7. package/dist/components/avatar/index.js +10 -3
  8. package/dist/components/badge/index.js +1 -1
  9. package/dist/components/breadcrumb/breadcrumb.d.ts +4 -0
  10. package/dist/components/breadcrumb/breadcrumb.types.d.ts +5 -0
  11. package/dist/components/breadcrumb/index.js +20 -3
  12. package/dist/components/button/button.d.ts +33 -0
  13. package/dist/components/button/index.js +138 -15
  14. package/dist/components/calendar/calendar.d.ts +15 -7
  15. package/dist/components/calendar/calendar.types.d.ts +17 -1
  16. package/dist/components/calendar/index.js +80 -27
  17. package/dist/components/checkbox/checkbox.d.ts +14 -2
  18. package/dist/components/checkbox/checkbox.types.d.ts +5 -0
  19. package/dist/components/checkbox/index.js +82 -5
  20. package/dist/components/chip/chip.d.ts +4 -1
  21. package/dist/components/chip/chip.types.d.ts +5 -0
  22. package/dist/components/chip/index.js +29 -1
  23. package/dist/components/color-input/color-input.d.ts +11 -2
  24. package/dist/components/color-input/color-input.types.d.ts +5 -0
  25. package/dist/components/color-input/index.js +92 -8
  26. package/dist/components/combobox/combobox.d.ts +18 -2
  27. package/dist/components/combobox/combobox.types.d.ts +7 -0
  28. package/dist/components/combobox/index.js +164 -23
  29. package/dist/components/date-input/date-input.d.ts +22 -7
  30. package/dist/components/date-input/date-input.types.d.ts +7 -0
  31. package/dist/components/date-input/index.js +218 -55
  32. package/dist/components/datepicker/datepicker.d.ts +9 -1
  33. package/dist/components/datepicker/index.js +108 -15
  34. package/dist/components/dialog/dialog.d.ts +39 -19
  35. package/dist/components/dialog/dialog.types.d.ts +5 -0
  36. package/dist/components/dialog/index.js +240 -93
  37. package/dist/components/divider/index.js +2 -2
  38. package/dist/components/dropdown/dropdown-item.d.ts +1 -1
  39. package/dist/components/dropdown/dropdown.d.ts +4 -0
  40. package/dist/components/dropdown/index.js +43 -3
  41. package/dist/components/icon-button/icon-button.d.ts +4 -0
  42. package/dist/components/icon-button/index.js +39 -4
  43. package/dist/components/image/image.d.ts +7 -7
  44. package/dist/components/image/index.js +6 -11
  45. package/dist/components/list/index.js +14 -4
  46. package/dist/components/nav/index.js +37 -17
  47. package/dist/components/nav/nav-item.d.ts +8 -8
  48. package/dist/components/pagination/index.js +57 -16
  49. package/dist/components/pagination/pagination.d.ts +8 -1
  50. package/dist/components/pagination/pagination.types.d.ts +15 -0
  51. package/dist/components/progress/index.js +44 -6
  52. package/dist/components/progress/progress.d.ts +12 -0
  53. package/dist/components/radio/index.js +148 -9
  54. package/dist/components/radio/radio.d.ts +34 -2
  55. package/dist/components/radio/radio.types.d.ts +5 -0
  56. package/dist/components/radio-group/index.js +120 -29
  57. package/dist/components/radio-group/radio-group.d.ts +27 -2
  58. package/dist/components/select/index.js +197 -44
  59. package/dist/components/select/select.d.ts +38 -4
  60. package/dist/components/select/select.types.d.ts +5 -0
  61. package/dist/components/shared/aria-forwarding.d.ts +1 -1
  62. package/dist/components/shared/form-default.d.ts +30 -0
  63. package/dist/components/shared/form-disabled.d.ts +40 -0
  64. package/dist/components/shared/form-field.d.ts +7 -0
  65. package/dist/components/shared/form-validity.d.ts +17 -0
  66. package/dist/components/shared/roving.d.ts +11 -0
  67. package/dist/components/shared/scroll-lock.d.ts +2 -0
  68. package/dist/components/sidepanel/index.js +278 -75
  69. package/dist/components/sidepanel/sidepanel.d.ts +32 -10
  70. package/dist/components/sidepanel/sidepanel.types.d.ts +10 -1
  71. package/dist/components/skeleton/index.js +3 -8
  72. package/dist/components/slider/index.js +307 -75
  73. package/dist/components/slider/slider.d.ts +16 -3
  74. package/dist/components/snackbar/index.js +82 -9
  75. package/dist/components/snackbar/snackbar.d.ts +17 -1
  76. package/dist/components/snackbar/snackbar.types.d.ts +5 -0
  77. package/dist/components/surface/index.js +8 -0
  78. package/dist/components/switch/index.js +101 -5
  79. package/dist/components/switch/switch.d.ts +13 -2
  80. package/dist/components/switch/switch.types.d.ts +5 -0
  81. package/dist/components/tab/index.js +15 -14
  82. package/dist/components/tab/tab.d.ts +0 -1
  83. package/dist/components/tab-group/index.js +102 -30
  84. package/dist/components/tab-group/tab-group.d.ts +17 -2
  85. package/dist/components/table/index.js +105 -61
  86. package/dist/components/table/table.d.ts +8 -2
  87. package/dist/components/table/table.types.d.ts +4 -3
  88. package/dist/components/text/index.js +16 -1
  89. package/dist/components/text/text.d.ts +1 -1
  90. package/dist/components/textfield/index.js +243 -35
  91. package/dist/components/textfield/textfield.d.ts +24 -2
  92. package/dist/components/textfield/textfield.types.d.ts +11 -0
  93. package/dist/components/tooltip/index.js +176 -49
  94. package/dist/components/tooltip/tooltip.d.ts +17 -0
  95. package/dist/components/tooltip/tooltip.types.d.ts +5 -1
  96. package/dist/index.cjs +2467 -692
  97. package/dist/index.js +2478 -699
  98. package/dist/manifest.json +202 -32
  99. package/dist/utils/dev-warn.d.ts +5 -0
  100. package/dist/utils/index.d.ts +1 -0
  101. package/package.json +4 -8
@@ -27,7 +27,7 @@ export declare class Text extends ThemeableElement {
27
27
  static styles: import("lit").CSSResult;
28
28
  /** Typographic style to apply. */
29
29
  variant: TextVariant;
30
- /** HTML tag to render. Defaults to the semantic tag for the chosen variant. */
30
+ /** HTML tag to render. Defaults to the semantic tag for the chosen variant; anything outside `TextTag` is ignored. */
31
31
  as: TextTag | '';
32
32
  render(): import("lit").TemplateResult;
33
33
  }
@@ -10,7 +10,8 @@ var __decorateClass = (decorators, target, key, kind) => {
10
10
  };
11
11
 
12
12
  // src/components/textfield/textfield.ts
13
- import { html as html2 } from "lit";
13
+ import { html as html2, nothing } from "lit";
14
+ import { classMap } from "lit/directives/class-map.js";
14
15
  import { ifDefined } from "lit/directives/if-defined.js";
15
16
 
16
17
  // src/base/themeable-element.ts
@@ -75,16 +76,25 @@ var textfieldStyles = css`
75
76
  }
76
77
 
77
78
  .icon-start {
78
- left: var(--lt-spacing-3);
79
+ inset-inline-start: var(--lt-spacing-3);
79
80
  }
80
81
 
81
82
  .icon-end {
82
- right: var(--lt-spacing-3);
83
+ inset-inline-end: var(--lt-spacing-3);
83
84
  }
84
85
 
86
+ /* Unlike .variant-icon (a decorative lt-icon), the password toggle is a real <button> — reset the native
87
+ chrome so it reads as an inline icon, and re-declare align-items since it's no longer a plain absolutely-
88
+ positioned element inheriting the .icon-start/.icon-end flex-adjacent layout by accident. */
85
89
  .password-toggle {
86
- pointer-events: auto;
90
+ align-items: center;
91
+ background: none;
92
+ border: none;
87
93
  cursor: pointer;
94
+ display: inline-flex;
95
+ font: inherit;
96
+ padding: 0;
97
+ pointer-events: auto;
88
98
  transition: color 120ms ease;
89
99
  }
90
100
 
@@ -92,6 +102,16 @@ var textfieldStyles = css`
92
102
  color: var(--lt-text-default);
93
103
  }
94
104
 
105
+ .password-toggle:disabled {
106
+ cursor: not-allowed;
107
+ opacity: 0.6;
108
+ }
109
+
110
+ .password-toggle:focus-visible {
111
+ outline: 2px solid var(--lt-border-focus);
112
+ outline-offset: 2px;
113
+ }
114
+
95
115
  .variant-icon {
96
116
  pointer-events: none;
97
117
  }
@@ -164,12 +184,12 @@ var textfieldStyles = css`
164
184
 
165
185
  :host([size='sm']) input.has-start-icon,
166
186
  :host([size='sm']) textarea.has-start-icon {
167
- padding-left: calc(var(--lt-spacing-3) + 20px + var(--lt-spacing-2));
187
+ padding-inline-start: calc(var(--lt-spacing-3) + 20px + var(--lt-spacing-2));
168
188
  }
169
189
 
170
190
  :host([size='sm']) input.has-end-icon,
171
191
  :host([size='sm']) textarea.has-end-icon {
172
- padding-right: calc(var(--lt-spacing-3) + 20px + var(--lt-spacing-2));
192
+ padding-inline-end: calc(var(--lt-spacing-3) + 20px + var(--lt-spacing-2));
173
193
  }
174
194
 
175
195
  :host([size='md']) input,
@@ -181,12 +201,12 @@ var textfieldStyles = css`
181
201
 
182
202
  :host([size='md']) input.has-start-icon,
183
203
  :host([size='md']) textarea.has-start-icon {
184
- padding-left: calc(var(--lt-spacing-4) + 20px + var(--lt-spacing-2));
204
+ padding-inline-start: calc(var(--lt-spacing-4) + 20px + var(--lt-spacing-2));
185
205
  }
186
206
 
187
207
  :host([size='md']) input.has-end-icon,
188
208
  :host([size='md']) textarea.has-end-icon {
189
- padding-right: calc(var(--lt-spacing-4) + 20px + var(--lt-spacing-2));
209
+ padding-inline-end: calc(var(--lt-spacing-4) + 20px + var(--lt-spacing-2));
190
210
  }
191
211
 
192
212
  :host([size='lg']) input,
@@ -198,12 +218,12 @@ var textfieldStyles = css`
198
218
 
199
219
  :host([size='lg']) input.has-start-icon,
200
220
  :host([size='lg']) textarea.has-start-icon {
201
- padding-left: calc(var(--lt-spacing-5) + 24px + var(--lt-spacing-2));
221
+ padding-inline-start: calc(var(--lt-spacing-5) + 24px + var(--lt-spacing-2));
202
222
  }
203
223
 
204
224
  :host([size='lg']) input.has-end-icon,
205
225
  :host([size='lg']) textarea.has-end-icon {
206
- padding-right: calc(var(--lt-spacing-5) + 24px + var(--lt-spacing-2));
226
+ padding-inline-end: calc(var(--lt-spacing-5) + 24px + var(--lt-spacing-2));
207
227
  }
208
228
 
209
229
  :host([size='lg']) .icon-start,
@@ -258,6 +278,25 @@ var textfieldStyles = css`
258
278
  font-weight: 200;
259
279
  }
260
280
 
281
+ /* .icon-end disables pointer events so decorative icons never swallow clicks; the clear button is the one real
282
+ control in that slot, so it opts back in and resets native button chrome. */
283
+ .clear-button {
284
+ align-items: center;
285
+ background: none;
286
+ border: 0;
287
+ border-radius: var(--lt-border-radius, 4px);
288
+ cursor: pointer;
289
+ display: flex;
290
+ padding: 0;
291
+ pointer-events: auto;
292
+ }
293
+
294
+ .counter {
295
+ color: var(--lt-text-subtle);
296
+ display: block;
297
+ text-align: end;
298
+ }
299
+
261
300
  :host([variant='success']) .helper-text {
262
301
  color: var(--lt-text-success);
263
302
  }
@@ -272,7 +311,7 @@ var textfieldStyles = css`
272
311
  }
273
312
 
274
313
  .required-indicator {
275
- color: var(--lt-color-error-500);
314
+ color: var(--lt-text-error);
276
315
  }
277
316
 
278
317
  :host([uppercase]) label lt-text::part(base) {
@@ -280,8 +319,71 @@ var textfieldStyles = css`
280
319
  letter-spacing: 0.05rem;
281
320
  text-transform: lowercase;
282
321
  }
322
+
323
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
324
+
325
+ @media (prefers-reduced-motion: reduce) {
326
+ .password-toggle,
327
+ input,
328
+ textarea {
329
+ transition: none;
330
+ }
331
+ }
283
332
  `;
284
333
 
334
+ // src/components/shared/form-default.ts
335
+ var FormDefaultController = class {
336
+ constructor(host, get) {
337
+ this._captured = false;
338
+ this._get = get;
339
+ host.addController(this);
340
+ }
341
+ hostConnected() {
342
+ if (this._captured) return;
343
+ this._captured = true;
344
+ this.value = this._get();
345
+ }
346
+ };
347
+
348
+ // src/components/shared/form-disabled.ts
349
+ var FormDisabledController = class {
350
+ constructor(host) {
351
+ this._fieldsetDisabled = false;
352
+ this._host = host;
353
+ host.addController(this);
354
+ }
355
+ // No-op — this controller has no connect-time work of its own (formDisabledCallback drives it instead), but
356
+ // ReactiveController's members are all optional, and a class satisfying an all-optional interface with no
357
+ // members in common is a TS error (TS2559); this gives it one.
358
+ hostConnected() {
359
+ }
360
+ /** Whether an ancestor `<fieldset>` is currently disabled. */
361
+ get fieldsetDisabled() {
362
+ return this._fieldsetDisabled;
363
+ }
364
+ setFieldsetDisabled(disabled) {
365
+ if (this._fieldsetDisabled === disabled) return;
366
+ this._fieldsetDisabled = disabled;
367
+ this._host.requestUpdate();
368
+ }
369
+ };
370
+
371
+ // src/components/shared/form-validity.ts
372
+ function toValidityFlags(validity) {
373
+ return {
374
+ valueMissing: validity.valueMissing,
375
+ typeMismatch: validity.typeMismatch,
376
+ patternMismatch: validity.patternMismatch,
377
+ tooLong: validity.tooLong,
378
+ tooShort: validity.tooShort,
379
+ rangeUnderflow: validity.rangeUnderflow,
380
+ rangeOverflow: validity.rangeOverflow,
381
+ stepMismatch: validity.stepMismatch,
382
+ badInput: validity.badInput,
383
+ customError: validity.customError
384
+ };
385
+ }
386
+
285
387
  // src/components/textfield/textfield.ts
286
388
  import "@latty-ds/icons";
287
389
 
@@ -454,6 +556,20 @@ var textStyles = css2`
454
556
  `;
455
557
 
456
558
  // src/components/text/text.ts
559
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
560
+ "h1",
561
+ "h2",
562
+ "h3",
563
+ "h4",
564
+ "h5",
565
+ "h6",
566
+ "p",
567
+ "span",
568
+ "div",
569
+ "label",
570
+ "strong",
571
+ "em"
572
+ ]);
457
573
  var VARIANT_TAG = {
458
574
  "display-2xl": "p",
459
575
  "display-xl": "p",
@@ -478,7 +594,8 @@ var Text = class extends ThemeableElement {
478
594
  this.as = "";
479
595
  }
480
596
  render() {
481
- const tag = unsafeStatic(this.as || VARIANT_TAG[this.variant] || "p");
597
+ const requested = this.as && ALLOWED_TAGS.has(this.as) ? this.as : "";
598
+ const tag = unsafeStatic(requested || VARIANT_TAG[this.variant] || "p");
482
599
  return html`<${tag} part="base"><slot></slot></${tag}>`;
483
600
  }
484
601
  };
@@ -494,9 +611,23 @@ Text = __decorateClass([
494
611
  ], Text);
495
612
 
496
613
  // src/components/textfield/textfield.ts
614
+ var textfieldCount = 0;
615
+ var DEFAULT_LABELS = {
616
+ validationMessage: "Please match the requested format",
617
+ showPassword: "Show password",
618
+ hidePassword: "Hide password",
619
+ clear: "Clear"
620
+ };
497
621
  var Textfield = class extends ThemeableElement {
498
622
  constructor() {
499
623
  super();
624
+ this._default = new FormDefaultController(this, () => this.value);
625
+ this._fieldsetDisabled = new FormDisabledController(this);
626
+ // Stable per-instance ids for `label for` and `aria-describedby` (TFD-4) — not the host's own `id` attribute,
627
+ // which a consumer may set for their own purposes (or leave unset).
628
+ this._uid = `lt-textfield-${textfieldCount++}`;
629
+ this._inputId = `${this._uid}-input`;
630
+ this._helperId = `${this._uid}-helper`;
500
631
  this.variant = "default";
501
632
  this.size = "md";
502
633
  this.type = "text";
@@ -513,6 +644,9 @@ var Textfield = class extends ThemeableElement {
513
644
  this.min = null;
514
645
  this.max = null;
515
646
  this.uppercase = false;
647
+ this.clearable = false;
648
+ this.counter = false;
649
+ this.labels = {};
516
650
  this.isPasswordVisible = false;
517
651
  this._touched = false;
518
652
  this._autoError = false;
@@ -522,8 +656,19 @@ var Textfield = class extends ThemeableElement {
522
656
  this._touched = true;
523
657
  this._autoError = !this._validate();
524
658
  };
659
+ // A native single-line <input> inside a <form> implicitly submits the form on Enter; the inner input here is
660
+ // in the shadow DOM, so the browser never wires that up on its own (TFD-2). Not on the multiline textarea —
661
+ // a native <textarea> never implicit-submits on Enter either, since Enter there means "new line".
662
+ this._onKeydown = (e) => {
663
+ if (e.key !== "Enter" || e.isComposing || this._isDisabled) return;
664
+ const form = this.closest("form");
665
+ form?.requestSubmit();
666
+ };
525
667
  this._internals = this.attachInternals();
526
668
  }
669
+ get _labels() {
670
+ return { ...DEFAULT_LABELS, ...this.labels };
671
+ }
527
672
  /**
528
673
  * Handles input events from the native input or textarea element.
529
674
  * Updates the component's value property and dispatches a custom input event.
@@ -574,6 +719,19 @@ var Textfield = class extends ThemeableElement {
574
719
  * @param e - The native change event
575
720
  * @private
576
721
  */
722
+ get _showClear() {
723
+ return this.clearable && !!this.value && !this._isDisabled && !this.readonly && this.type !== "password";
724
+ }
725
+ _clear() {
726
+ this.value = "";
727
+ if (this._touched) this._autoError = !this._validate();
728
+ const detail = { value: "" };
729
+ this.dispatchEvent(new CustomEvent("input", { detail, bubbles: true, composed: true }));
730
+ this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true, composed: true }));
731
+ this.updateComplete.then(() => {
732
+ this.shadowRoot?.querySelector("input, textarea")?.focus();
733
+ });
734
+ }
577
735
  handleChange(e) {
578
736
  e.stopPropagation();
579
737
  const input = e.target;
@@ -595,6 +753,12 @@ var Textfield = class extends ThemeableElement {
595
753
  togglePasswordVisibility() {
596
754
  this.isPasswordVisible = !this.isPasswordVisible;
597
755
  }
756
+ // Clicking the toggle would otherwise focus the button first (the default mousedown action), moving the
757
+ // caret out of the field. Suppressing just that default action leaves the click itself untouched, and the
758
+ // input stays focused throughout.
759
+ _preventFocusSteal(e) {
760
+ e.preventDefault();
761
+ }
598
762
  /**
599
763
  * Determines the appropriate end icon based on input type and variant.
600
764
  *
@@ -623,15 +787,27 @@ var Textfield = class extends ThemeableElement {
623
787
  super.updated(changed);
624
788
  this.toggleAttribute("data-invalid", this._autoError);
625
789
  this._internals.setFormValue(this.value || null);
626
- const anchor = this.shadowRoot.querySelector("input") ?? this.shadowRoot.querySelector("textarea");
627
- if (this.required && !this.value && anchor) {
628
- this._internals.setValidity({ valueMissing: true }, "Please fill in this field", anchor);
790
+ const inner = this.shadowRoot.querySelector("input") ?? this.shadowRoot.querySelector("textarea");
791
+ if (!inner) return;
792
+ if (!inner.validity.valid) {
793
+ this._internals.setValidity(toValidityFlags(inner.validity), inner.validationMessage, inner);
794
+ } else if (!this._validate()) {
795
+ this._internals.setValidity({ customError: true }, this._labels.validationMessage, inner);
629
796
  } else {
630
797
  this._internals.setValidity({});
631
798
  }
632
799
  }
633
800
  formResetCallback() {
634
- this.value = "";
801
+ this.value = this._default.value;
802
+ this._touched = false;
803
+ this._autoError = false;
804
+ }
805
+ formDisabledCallback(disabled) {
806
+ this._fieldsetDisabled.setFieldsetDisabled(disabled);
807
+ }
808
+ /** `this.disabled` combined with an ancestor `<fieldset>`'s own disabled state (see `FormDisabledController`). */
809
+ get _isDisabled() {
810
+ return this.disabled || this._fieldsetDisabled.fieldsetDisabled;
635
811
  }
636
812
  checkValidity() {
637
813
  return this._internals.checkValidity();
@@ -645,14 +821,16 @@ var Textfield = class extends ThemeableElement {
645
821
  const isMultiline = this.type === "multiline";
646
822
  const isNumber = this.type === "number";
647
823
  const endIconName = this.getEndIconName();
648
- const hasEndIcon = Boolean(endIconName);
824
+ const showClear = this._showClear;
825
+ const hasEndIcon = Boolean(endIconName) || showClear;
649
826
  const actualInputType = isPasswordField && this.isPasswordVisible ? "text" : this.type;
650
827
  const isInvalid = this._autoError || this.variant === "error";
651
828
  const resolvedHelperText = typeof this.helperText === "function" ? this.helperText(isInvalid) : this.helperText;
829
+ const ariaLabel = this.label ? void 0 : this.placeholder || void 0;
652
830
  return html2`
653
831
  <div class="wrapper">
654
832
  ${this.label ? html2`
655
- <label>
833
+ <label for=${this._inputId}>
656
834
  <lt-text variant="label" as="span">${this.label}</lt-text>
657
835
  ${this.required ? html2`<span class="required-indicator">*</span>` : ""}
658
836
  </label>
@@ -661,25 +839,27 @@ var Textfield = class extends ThemeableElement {
661
839
  ${hasStartIcon ? html2`<lt-icon class="icon-start" name=${this.iconStart}></lt-icon>` : ""}
662
840
  ${isMultiline ? html2`
663
841
  <textarea
842
+ id=${this._inputId}
664
843
  .value=${this.value}
665
844
  name=${ifDefined(this.name || void 0)}
666
845
  placeholder=${this.placeholder}
667
846
  rows=${this.rows}
668
847
  minlength=${ifDefined(this.min !== null ? this.min : void 0)}
669
848
  maxlength=${ifDefined(this.max !== null ? this.max : void 0)}
670
- ?disabled=${this.disabled}
849
+ ?disabled=${this._isDisabled}
671
850
  ?required=${this.required}
672
851
  ?readonly=${this.readonly}
673
852
  @input=${this.handleInput}
674
853
  @change=${this.handleChange}
675
854
  @blur=${this._onBlur}
676
- aria-label=${this.label || this.placeholder}
855
+ aria-label=${ifDefined(ariaLabel)}
677
856
  aria-invalid=${isInvalid ? "true" : "false"}
678
- class=${hasStartIcon ? "has-start-icon" : ""}
679
- ${hasEndIcon ? "has-end-icon" : ""}
857
+ aria-describedby=${resolvedHelperText ? this._helperId : nothing}
858
+ class=${classMap({ "has-start-icon": hasStartIcon, "has-end-icon": hasEndIcon })}
680
859
  ></textarea>
681
860
  ` : html2`
682
861
  <input
862
+ id=${this._inputId}
683
863
  type=${actualInputType}
684
864
  .value=${this.value}
685
865
  name=${ifDefined(this.name || void 0)}
@@ -688,27 +868,46 @@ var Textfield = class extends ThemeableElement {
688
868
  max=${ifDefined(isNumber && this.max !== null ? this.max : void 0)}
689
869
  minlength=${ifDefined(!isNumber && this.min !== null ? this.min : void 0)}
690
870
  maxlength=${ifDefined(!isNumber && this.max !== null ? this.max : void 0)}
691
- ?disabled=${this.disabled}
871
+ ?disabled=${this._isDisabled}
692
872
  ?required=${this.required}
693
873
  ?readonly=${this.readonly}
694
874
  @input=${this.handleInput}
695
875
  @change=${this.handleChange}
696
876
  @blur=${this._onBlur}
697
- aria-label=${this.label || this.placeholder}
877
+ @keydown=${this._onKeydown}
878
+ aria-label=${ifDefined(ariaLabel)}
698
879
  aria-invalid=${isInvalid ? "true" : "false"}
699
- class=${hasStartIcon ? "has-start-icon" : ""}
700
- ${hasEndIcon ? "has-end-icon" : ""}
880
+ aria-describedby=${resolvedHelperText ? this._helperId : nothing}
881
+ class=${classMap({ "has-start-icon": hasStartIcon, "has-end-icon": hasEndIcon })}
701
882
  />
702
883
  `}
703
- ${hasEndIcon ? html2`
704
- <lt-icon
705
- class="icon-end ${isPasswordField ? "password-toggle" : "variant-icon"}"
706
- name=${endIconName}
707
- @click=${isPasswordField ? this.togglePasswordVisibility : null}
708
- ></lt-icon>
709
- ` : ""}
884
+ ${showClear ? html2`<button
885
+ type="button"
886
+ class="icon-end clear-button"
887
+ aria-label=${this._labels.clear}
888
+ @mousedown=${this._preventFocusSteal}
889
+ @click=${this._clear}
890
+ >
891
+ <lt-icon name="xmark"></lt-icon>
892
+ </button>` : ""}
893
+ ${endIconName ? isPasswordField ? html2`
894
+ <button
895
+ type="button"
896
+ class="icon-end password-toggle"
897
+ ?disabled=${this._isDisabled}
898
+ aria-label=${this.isPasswordVisible ? this._labels.hidePassword : this._labels.showPassword}
899
+ aria-pressed=${this.isPasswordVisible ? "true" : "false"}
900
+ @mousedown=${this._preventFocusSteal}
901
+ @click=${this.togglePasswordVisibility}
902
+ >
903
+ <lt-icon name=${endIconName}></lt-icon>
904
+ </button>
905
+ ` : html2`<lt-icon class="icon-end variant-icon" name=${endIconName}></lt-icon>` : ""}
710
906
  </div>
711
- ${resolvedHelperText ? html2`<lt-text variant="caption" as="span" class="helper-text">${resolvedHelperText}</lt-text>` : ""}
907
+ ${resolvedHelperText ? html2`<lt-text id=${this._helperId} variant="caption" as="span" class="helper-text"
908
+ >${resolvedHelperText}</lt-text
909
+ >` : ""}
910
+ ${this.counter && !isNumber && this.max !== null ? html2`<lt-text variant="caption" as="span" class="counter">${this.value.length} / ${this.max}</lt-text>` : ""}
712
911
  </div>
713
912
  `;
714
913
  }
@@ -725,7 +924,7 @@ __decorateClass([
725
924
  property3({ reflect: true })
726
925
  ], Textfield.prototype, "type", 2);
727
926
  __decorateClass([
728
- property3({ reflect: true })
927
+ property3()
729
928
  ], Textfield.prototype, "value", 2);
730
929
  __decorateClass([
731
930
  property3({ reflect: true })
@@ -763,6 +962,15 @@ __decorateClass([
763
962
  __decorateClass([
764
963
  property3({ type: Boolean, reflect: true })
765
964
  ], Textfield.prototype, "uppercase", 2);
965
+ __decorateClass([
966
+ property3({ type: Boolean, reflect: true })
967
+ ], Textfield.prototype, "clearable", 2);
968
+ __decorateClass([
969
+ property3({ type: Boolean, reflect: true })
970
+ ], Textfield.prototype, "counter", 2);
971
+ __decorateClass([
972
+ property3({ type: Object })
973
+ ], Textfield.prototype, "labels", 2);
766
974
  __decorateClass([
767
975
  state()
768
976
  ], Textfield.prototype, "isPasswordVisible", 2);
@@ -1,6 +1,6 @@
1
1
  import { PropertyValues } from 'lit';
2
2
  import { ThemeableElement } from '../../base';
3
- import { HelperText, TextfieldSize, TextfieldType, TextfieldVariant } from './textfield.types';
3
+ import { HelperText, TextfieldSize, TextfieldType, TextfieldVariant, type TextfieldLabels } from './textfield.types';
4
4
  import '@latty-ds/icons';
5
5
  import '../text/text';
6
6
  /**
@@ -46,6 +46,11 @@ export declare class Textfield extends ThemeableElement {
46
46
  static styles: import("lit").CSSResult;
47
47
  static formAssociated: boolean;
48
48
  private _internals;
49
+ private _default;
50
+ private _fieldsetDisabled;
51
+ private readonly _uid;
52
+ private readonly _inputId;
53
+ private readonly _helperId;
49
54
  constructor();
50
55
  /**
51
56
  * Visual variant that determines styling and automatic end icon.
@@ -63,7 +68,10 @@ export declare class Textfield extends ThemeableElement {
63
68
  */
64
69
  type: TextfieldType;
65
70
  /**
66
- * Current value of the input.
71
+ * Current value of the input. Not reflected as an attribute: for `type="password"` that would put whatever
72
+ * the user types in plain text in the DOM (`outerHTML`, DevTools, session-replay/DOM-mutation recorders) —
73
+ * visible via `getAttribute('value')`/`[value]` regardless of input type. Read the property, or the
74
+ * `input`/`change` event's `detail.value`, instead of an attribute selector.
67
75
  * @default ''
68
76
  */
69
77
  value: string;
@@ -132,6 +140,13 @@ export declare class Textfield extends ThemeableElement {
132
140
  * @default false
133
141
  */
134
142
  uppercase: boolean;
143
+ /** Overridable UI text (format validation message, password-toggle button labels), for i18n. English defaults — override only the keys you need. */
144
+ /** Shows a clear button while the field has a value. Not shown for password fields, which have their own toggle. */
145
+ clearable: boolean;
146
+ /** Shows the character count as `length / max` below the field. Uses `max` as the limit, which is also enforced natively. */
147
+ counter: boolean;
148
+ labels: Partial<TextfieldLabels>;
149
+ private get _labels();
135
150
  private isPasswordVisible;
136
151
  private _touched;
137
152
  private _autoError;
@@ -152,7 +167,10 @@ export declare class Textfield extends ThemeableElement {
152
167
  * @param e - The native change event
153
168
  * @private
154
169
  */
170
+ private get _showClear();
171
+ private _clear;
155
172
  private handleChange;
173
+ private _onKeydown;
156
174
  /**
157
175
  * Toggles password visibility for password-type inputs.
158
176
  * Switches between showing plain text and masked password.
@@ -160,6 +178,7 @@ export declare class Textfield extends ThemeableElement {
160
178
  * @private
161
179
  */
162
180
  private togglePasswordVisibility;
181
+ private _preventFocusSteal;
163
182
  /**
164
183
  * Determines the appropriate end icon based on input type and variant.
165
184
  *
@@ -173,6 +192,9 @@ export declare class Textfield extends ThemeableElement {
173
192
  private getEndIconName;
174
193
  updated(changed: PropertyValues): void;
175
194
  formResetCallback(): void;
195
+ formDisabledCallback(disabled: boolean): void;
196
+ /** `this.disabled` combined with an ancestor `<fieldset>`'s own disabled state (see `FormDisabledController`). */
197
+ private get _isDisabled();
176
198
  checkValidity(): boolean;
177
199
  reportValidity(): boolean;
178
200
  render(): import("lit").TemplateResult<1>;
@@ -38,3 +38,14 @@ export type TextfieldType = 'text' | 'email' | 'password' | 'tel' | 'url' | 'num
38
38
  * helperText={(error) => error ? 'Enter a valid email' : 'We\'ll never share your email'}
39
39
  */
40
40
  export type HelperText = string | ((error: boolean) => string);
41
+ /** Overridable UI text for `lt-textfield`, for i18n. */
42
+ export interface TextfieldLabels {
43
+ /** Validation message shown when the value fails a format rule HTML has no native constraint for (e.g. `tel`). */
44
+ validationMessage: string;
45
+ /** Accessible label for the password-visibility toggle when the password is hidden. */
46
+ showPassword: string;
47
+ /** Accessible label for the password-visibility toggle when the password is shown. */
48
+ hidePassword: string;
49
+ /** Accessible label for the clear button shown by `clearable`. */
50
+ clear: string;
51
+ }