@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
@@ -9,8 +9,12 @@ var __decorateClass = (decorators, target, key, kind) => {
9
9
  return result;
10
10
  };
11
11
 
12
+ // src/components/shared/form-field.ts
13
+ var fieldCount = 0;
14
+ var createFieldIds = () => ({ helper: `lt-field-${fieldCount++}-helper` });
15
+
12
16
  // src/components/select/select.ts
13
- import { html as html3 } from "lit";
17
+ import { html as html3, nothing } from "lit";
14
18
 
15
19
  // src/base/themeable-element.ts
16
20
  import { LitElement } from "lit";
@@ -1448,8 +1452,8 @@ var selectStyles = [
1448
1452
  }
1449
1453
 
1450
1454
  .required-indicator {
1451
- color: var(--lt-color-error-500);
1452
- margin-left: var(--lt-spacing-1);
1455
+ color: var(--lt-text-error);
1456
+ margin-inline-start: var(--lt-spacing-1);
1453
1457
  }
1454
1458
 
1455
1459
  .select-container {
@@ -1531,7 +1535,7 @@ var selectStyles = [
1531
1535
  cursor: pointer;
1532
1536
  display: flex;
1533
1537
  font-family: inherit;
1534
- text-align: left;
1538
+ text-align: start;
1535
1539
  transition: background-color 80ms ease;
1536
1540
  width: 100%;
1537
1541
  }
@@ -1540,7 +1544,7 @@ var selectStyles = [
1540
1544
  background: var(--lt-bg-surface);
1541
1545
  }
1542
1546
 
1543
- .option:focus-visible {
1547
+ .option[data-active='true'] {
1544
1548
  background: var(--lt-bg-surface);
1545
1549
  outline: 2px solid var(--lt-interactive-primary-bg);
1546
1550
  outline-offset: -2px;
@@ -1624,6 +1628,16 @@ var selectStyles = [
1624
1628
  :host([variant='default']) .helper-text {
1625
1629
  color: var(--lt-text-subtle);
1626
1630
  }
1631
+
1632
+ /* ── Reduced motion ───────────────────────────────────────────────────── */
1633
+
1634
+ @media (prefers-reduced-motion: reduce) {
1635
+ .select-trigger,
1636
+ .icon-end,
1637
+ .option {
1638
+ transition: none;
1639
+ }
1640
+ }
1627
1641
  `
1628
1642
  ];
1629
1643
 
@@ -1702,6 +1716,14 @@ var surfaceStyles = css3`
1702
1716
  :host([appearance='outlined']:hover) .surface {
1703
1717
  border-color: var(--lt-border-strong);
1704
1718
  }
1719
+
1720
+ /* ── Reduced motion ─────────────────────────────────────────────────────── */
1721
+
1722
+ @media (prefers-reduced-motion: reduce) {
1723
+ .surface {
1724
+ transition: none;
1725
+ }
1726
+ }
1705
1727
  `;
1706
1728
 
1707
1729
  // src/components/surface/surface.ts
@@ -1904,6 +1926,20 @@ var textStyles = css4`
1904
1926
  `;
1905
1927
 
1906
1928
  // src/components/text/text.ts
1929
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
1930
+ "h1",
1931
+ "h2",
1932
+ "h3",
1933
+ "h4",
1934
+ "h5",
1935
+ "h6",
1936
+ "p",
1937
+ "span",
1938
+ "div",
1939
+ "label",
1940
+ "strong",
1941
+ "em"
1942
+ ]);
1907
1943
  var VARIANT_TAG = {
1908
1944
  "display-2xl": "p",
1909
1945
  "display-xl": "p",
@@ -1928,7 +1964,8 @@ var Text = class extends ThemeableElement {
1928
1964
  this.as = "";
1929
1965
  }
1930
1966
  render() {
1931
- const tag = unsafeStatic(this.as || VARIANT_TAG[this.variant] || "p");
1967
+ const requested = this.as && ALLOWED_TAGS.has(this.as) ? this.as : "";
1968
+ const tag = unsafeStatic(requested || VARIANT_TAG[this.variant] || "p");
1932
1969
  return html2`<${tag} part="base"><slot></slot></${tag}>`;
1933
1970
  }
1934
1971
  };
@@ -1943,13 +1980,59 @@ Text = __decorateClass([
1943
1980
  customElement2("lt-text")
1944
1981
  ], Text);
1945
1982
 
1983
+ // src/components/shared/form-default.ts
1984
+ var FormDefaultController = class {
1985
+ constructor(host, get) {
1986
+ this._captured = false;
1987
+ this._get = get;
1988
+ host.addController(this);
1989
+ }
1990
+ hostConnected() {
1991
+ if (this._captured) return;
1992
+ this._captured = true;
1993
+ this.value = this._get();
1994
+ }
1995
+ };
1996
+
1997
+ // src/components/shared/form-disabled.ts
1998
+ var FormDisabledController = class {
1999
+ constructor(host) {
2000
+ this._fieldsetDisabled = false;
2001
+ this._host = host;
2002
+ host.addController(this);
2003
+ }
2004
+ // No-op — this controller has no connect-time work of its own (formDisabledCallback drives it instead), but
2005
+ // ReactiveController's members are all optional, and a class satisfying an all-optional interface with no
2006
+ // members in common is a TS error (TS2559); this gives it one.
2007
+ hostConnected() {
2008
+ }
2009
+ /** Whether an ancestor `<fieldset>` is currently disabled. */
2010
+ get fieldsetDisabled() {
2011
+ return this._fieldsetDisabled;
2012
+ }
2013
+ setFieldsetDisabled(disabled) {
2014
+ if (this._fieldsetDisabled === disabled) return;
2015
+ this._fieldsetDisabled = disabled;
2016
+ this._host.requestUpdate();
2017
+ }
2018
+ };
2019
+
1946
2020
  // src/components/select/select.ts
2021
+ var selectCount = 0;
2022
+ var DEFAULT_LABELS = {
2023
+ validationMessage: "Please select an option"
2024
+ };
1947
2025
  var Select = class extends ThemeableElement {
1948
2026
  constructor() {
1949
2027
  super();
2028
+ this._default = new FormDefaultController(this, () => this.value);
2029
+ this._fieldsetDisabled = new FormDisabledController(this);
2030
+ // Scopes generated option/listbox ids to this instance, so two selects on the same page don't collide.
2031
+ this._uid = `lt-select-${selectCount++}`;
1950
2032
  this.variant = "default";
1951
2033
  this.size = "md";
1952
2034
  this.value = "";
2035
+ this.multiple = false;
1953
2036
  this.placeholder = "Select an option";
1954
2037
  this.label = "";
1955
2038
  this.name = "";
@@ -1957,17 +2040,42 @@ var Select = class extends ThemeableElement {
1957
2040
  this.disabled = false;
1958
2041
  this.required = false;
1959
2042
  this.options = [];
2043
+ this.labels = {};
1960
2044
  this.isOpen = false;
2045
+ this.activeIndex = -1;
1961
2046
  this._cleanupClickOutside = null;
1962
2047
  this._floatingCleanup = null;
2048
+ this._fieldIds = createFieldIds();
1963
2049
  this._internals = this.attachInternals();
1964
2050
  }
2051
+ get _selected() {
2052
+ if (Array.isArray(this.value)) return this.value;
2053
+ return this.value ? [this.value] : [];
2054
+ }
2055
+ get _labels() {
2056
+ return { ...DEFAULT_LABELS, ...this.labels };
2057
+ }
2058
+ get enabledOptions() {
2059
+ return this.options.filter((o) => !o.disabled);
2060
+ }
2061
+ // Ids are derived from position in the full option list (disabled options included), since that's what's
2062
+ // actually rendered.
2063
+ _idFor(option) {
2064
+ return `${this._uid}-option-${this.options.indexOf(option)}`;
2065
+ }
2066
+ // Clicking an option button would otherwise focus it first (the default mousedown action) — a button that
2067
+ // momentarily takes focus, then vanishes as the dropdown closes, drops focus to nothing sensible. Suppressing
2068
+ // just that default action leaves the click itself (and selectOption) untouched, and the trigger — which
2069
+ // never lost focus to begin with — stays the active element throughout.
2070
+ _preventFocusSteal(e) {
2071
+ e.preventDefault();
2072
+ }
1965
2073
  /**
1966
2074
  * Toggles the dropdown open/closed state.
1967
2075
  * @private
1968
2076
  */
1969
2077
  toggleDropdown() {
1970
- if (!this.disabled) {
2078
+ if (!this._isDisabled) {
1971
2079
  this.isOpen = !this.isOpen;
1972
2080
  }
1973
2081
  }
@@ -1987,6 +2095,12 @@ var Select = class extends ThemeableElement {
1987
2095
  */
1988
2096
  selectOption(option) {
1989
2097
  if (option.disabled) return;
2098
+ if (this.multiple) {
2099
+ const current = this._selected;
2100
+ this.value = current.includes(option.value) ? current.filter((v) => v !== option.value) : [...current, option.value];
2101
+ dispatch(this, "change", { value: this.value });
2102
+ return;
2103
+ }
1990
2104
  this.value = option.value;
1991
2105
  this.closeDropdown();
1992
2106
  dispatch(this, "change", { value: this.value });
@@ -1999,32 +2113,34 @@ var Select = class extends ThemeableElement {
1999
2113
  * @private
2000
2114
  */
2001
2115
  handleKeydown(e) {
2002
- if (this.disabled) return;
2116
+ if (this._isDisabled) return;
2003
2117
  const { key } = e;
2004
- if (!this.isOpen && (key === "Enter" || key === " " || key === "ArrowDown" || key === "ArrowUp")) {
2005
- e.preventDefault();
2006
- this.isOpen = true;
2118
+ if (!this.isOpen) {
2119
+ if (key === "Enter" || key === " " || key === "ArrowDown" || key === "ArrowUp") {
2120
+ e.preventDefault();
2121
+ this.isOpen = true;
2122
+ }
2007
2123
  return;
2008
2124
  }
2009
- if (this.isOpen) {
2010
- if (key === "Escape") {
2125
+ const enabled = this.enabledOptions;
2126
+ switch (key) {
2127
+ case "Escape":
2011
2128
  e.preventDefault();
2012
2129
  this.closeDropdown();
2013
- return;
2014
- }
2015
- if (key === "ArrowDown" || key === "ArrowUp") {
2130
+ break;
2131
+ case "ArrowDown":
2016
2132
  e.preventDefault();
2017
- const options = this.shadowRoot?.querySelectorAll(".option:not([disabled])");
2018
- if (!options || options.length === 0) return;
2019
- const currentIndex = Array.from(options).findIndex((opt) => opt === this.shadowRoot?.activeElement);
2020
- let nextIndex = currentIndex;
2021
- if (key === "ArrowDown") {
2022
- nextIndex = currentIndex < options.length - 1 ? currentIndex + 1 : 0;
2023
- } else {
2024
- nextIndex = currentIndex > 0 ? currentIndex - 1 : options.length - 1;
2025
- }
2026
- options[nextIndex]?.focus();
2027
- }
2133
+ if (enabled.length > 0) this.activeIndex = this.activeIndex < enabled.length - 1 ? this.activeIndex + 1 : 0;
2134
+ break;
2135
+ case "ArrowUp":
2136
+ e.preventDefault();
2137
+ if (enabled.length > 0) this.activeIndex = this.activeIndex > 0 ? this.activeIndex - 1 : enabled.length - 1;
2138
+ break;
2139
+ case "Enter":
2140
+ case " ":
2141
+ e.preventDefault();
2142
+ if (enabled[this.activeIndex]) this.selectOption(enabled[this.activeIndex]);
2143
+ break;
2028
2144
  }
2029
2145
  }
2030
2146
  connectedCallback() {
@@ -2043,19 +2159,33 @@ var Select = class extends ThemeableElement {
2043
2159
  * @private
2044
2160
  */
2045
2161
  getSelectedLabel() {
2046
- const selected = this.options.find((opt) => opt.value === this.value);
2162
+ if (this.multiple) {
2163
+ return this._selected.map((v) => this.options.find((opt) => opt.value === v)?.label).filter((label) => Boolean(label)).join(", ");
2164
+ }
2165
+ const selected = this.options.find((opt) => opt.value === this._selected[0]);
2047
2166
  return selected ? selected.label : "";
2048
2167
  }
2168
+ willUpdate(changed) {
2169
+ super.willUpdate(changed);
2170
+ if (this.multiple && !Array.isArray(this.value)) this.value = this._selected;
2171
+ }
2049
2172
  updated(changedProperties) {
2050
- this._internals.setFormValue(this.value || null);
2173
+ if (this.multiple) {
2174
+ const formData = new FormData();
2175
+ if (this.name) for (const v of this._selected) formData.append(this.name, v);
2176
+ this._internals.setFormValue(formData);
2177
+ } else {
2178
+ this._internals.setFormValue(this._selected[0] || null);
2179
+ }
2051
2180
  const trigger = this.shadowRoot?.querySelector(".select-trigger");
2052
- if (this.required && !this.value && trigger) {
2053
- this._internals.setValidity({ valueMissing: true }, "Please select an option", trigger);
2181
+ if (this.required && this._selected.length === 0 && trigger) {
2182
+ this._internals.setValidity({ valueMissing: true }, this._labels.validationMessage, trigger);
2054
2183
  } else {
2055
2184
  this._internals.setValidity({});
2056
2185
  }
2057
2186
  if (!changedProperties.has("isOpen")) return;
2058
2187
  if (this.isOpen) {
2188
+ this.activeIndex = -1;
2059
2189
  this.setAttribute("open", "");
2060
2190
  const selectTrigger = this.shadowRoot.querySelector(".select-trigger");
2061
2191
  const listbox = this.shadowRoot.querySelector("lt-surface.dropdown");
@@ -2071,7 +2201,14 @@ var Select = class extends ThemeableElement {
2071
2201
  }
2072
2202
  }
2073
2203
  formResetCallback() {
2074
- this.value = "";
2204
+ this.value = this._default.value;
2205
+ }
2206
+ formDisabledCallback(disabled) {
2207
+ this._fieldsetDisabled.setFieldsetDisabled(disabled);
2208
+ }
2209
+ /** `this.disabled` combined with an ancestor `<fieldset>`'s own disabled state (see `FormDisabledController`). */
2210
+ get _isDisabled() {
2211
+ return this.disabled || this._fieldsetDisabled.fieldsetDisabled;
2075
2212
  }
2076
2213
  checkValidity() {
2077
2214
  return this._internals.checkValidity();
@@ -2081,7 +2218,9 @@ var Select = class extends ThemeableElement {
2081
2218
  }
2082
2219
  render() {
2083
2220
  const selectedLabel = this.getSelectedLabel();
2084
- const hasValue = Boolean(this.value && selectedLabel);
2221
+ const hasValue = Boolean(this._selected.length && selectedLabel);
2222
+ const activeOption = this.enabledOptions[this.activeIndex];
2223
+ const listboxId = `${this._uid}-listbox`;
2085
2224
  return html3`
2086
2225
  <div class="wrapper">
2087
2226
  ${this.label ? html3`
@@ -2096,9 +2235,12 @@ var Select = class extends ThemeableElement {
2096
2235
  role="combobox"
2097
2236
  aria-haspopup="listbox"
2098
2237
  aria-expanded=${this.isOpen}
2099
- aria-disabled=${this.disabled}
2238
+ aria-disabled=${this._isDisabled}
2100
2239
  aria-label=${this.label || this.placeholder}
2101
- tabindex=${this.disabled ? "-1" : "0"}
2240
+ aria-describedby=${this.helperText ? this._fieldIds.helper : nothing}
2241
+ aria-controls=${this.isOpen ? listboxId : nothing}
2242
+ aria-activedescendant=${this.isOpen && activeOption ? this._idFor(activeOption) : nothing}
2243
+ tabindex=${this._isDisabled ? "-1" : "0"}
2102
2244
  @click=${this.toggleDropdown}
2103
2245
  @keydown=${this.handleKeydown}
2104
2246
  >
@@ -2108,29 +2250,29 @@ var Select = class extends ThemeableElement {
2108
2250
  <lt-icon class="icon-end" name="caret-down"></lt-icon>
2109
2251
  </div>
2110
2252
  <lt-surface
2253
+ id=${listboxId}
2111
2254
  class="dropdown"
2112
2255
  popover="manual"
2113
2256
  elevation="2"
2114
2257
  appearance="outlined"
2115
2258
  background="--lt-bg-default"
2116
2259
  role="listbox"
2260
+ aria-multiselectable=${this.multiple ? "true" : nothing}
2117
2261
  part="listbox"
2118
2262
  >
2119
2263
  <div class="options-container">
2120
2264
  ${this.options.map(
2121
2265
  (option) => html3`
2122
2266
  <button
2267
+ id=${this._idFor(option)}
2123
2268
  class="option"
2124
2269
  role="option"
2125
- aria-selected=${option.value === this.value}
2270
+ tabindex="-1"
2271
+ aria-selected=${this._selected.includes(option.value)}
2126
2272
  ?disabled=${option.disabled}
2273
+ data-active=${option === activeOption}
2274
+ @mousedown=${this._preventFocusSteal}
2127
2275
  @click=${() => this.selectOption(option)}
2128
- @keydown=${(e) => {
2129
- if (e.key === "Enter" || e.key === " ") {
2130
- e.preventDefault();
2131
- this.selectOption(option);
2132
- }
2133
- }}
2134
2276
  >
2135
2277
  ${option.label}
2136
2278
  </button>
@@ -2139,7 +2281,9 @@ var Select = class extends ThemeableElement {
2139
2281
  </div>
2140
2282
  </lt-surface>
2141
2283
  </div>
2142
- ${this.helperText ? html3`<lt-text variant="caption" as="span" class="helper-text">${this.helperText}</lt-text>` : ""}
2284
+ ${this.helperText ? html3`<lt-text id=${this._fieldIds.helper} variant="caption" as="span" class="helper-text"
2285
+ >${this.helperText}</lt-text
2286
+ >` : ""}
2143
2287
  </div>
2144
2288
  `;
2145
2289
  }
@@ -2153,8 +2297,11 @@ __decorateClass([
2153
2297
  property4({ reflect: true })
2154
2298
  ], Select.prototype, "size", 2);
2155
2299
  __decorateClass([
2156
- property4({ reflect: true })
2300
+ property4()
2157
2301
  ], Select.prototype, "value", 2);
2302
+ __decorateClass([
2303
+ property4({ type: Boolean, reflect: true })
2304
+ ], Select.prototype, "multiple", 2);
2158
2305
  __decorateClass([
2159
2306
  property4({ reflect: true })
2160
2307
  ], Select.prototype, "placeholder", 2);
@@ -2176,9 +2323,15 @@ __decorateClass([
2176
2323
  __decorateClass([
2177
2324
  property4({ type: Array })
2178
2325
  ], Select.prototype, "options", 2);
2326
+ __decorateClass([
2327
+ property4({ type: Object })
2328
+ ], Select.prototype, "labels", 2);
2179
2329
  __decorateClass([
2180
2330
  state()
2181
2331
  ], Select.prototype, "isOpen", 2);
2332
+ __decorateClass([
2333
+ state()
2334
+ ], Select.prototype, "activeIndex", 2);
2182
2335
  Select = __decorateClass([
2183
2336
  customElement3("lt-select")
2184
2337
  ], Select);
@@ -1,5 +1,6 @@
1
+ import { type PropertyValues } from 'lit';
1
2
  import { ThemeableElement } from '../../base';
2
- import { SelectOption, SelectSize, SelectVariant } from './select.types';
3
+ import { SelectOption, SelectSize, SelectVariant, type SelectLabels } from './select.types';
3
4
  import '@latty-ds/icons';
4
5
  import '../surface/';
5
6
  import '../text/text';
@@ -8,7 +9,8 @@ import '../text/text';
8
9
  *
9
10
  * @element lt-select
10
11
  *
11
- * @fires {CustomEvent<{value: string}>} change - Dispatched when the selected value changes
12
+ * @fires {CustomEvent<{value: string | string[]}>} change - Dispatched when the selected value changes. With `multiple`,
13
+ * `value` is the array of selected values.
12
14
  *
13
15
  * @example
14
16
  * ```html
@@ -38,6 +40,9 @@ export declare class Select extends ThemeableElement {
38
40
  static styles: import("lit").CSSResult[];
39
41
  static formAssociated: boolean;
40
42
  private _internals;
43
+ private _default;
44
+ private _fieldsetDisabled;
45
+ private readonly _uid;
41
46
  constructor();
42
47
  /**
43
48
  * Visual variant that determines styling.
@@ -50,10 +55,20 @@ export declare class Select extends ThemeableElement {
50
55
  */
51
56
  size: SelectSize;
52
57
  /**
53
- * Current selected value.
58
+ * Current selected value. A string for a single select; with `multiple`, an array of the selected values. Not
59
+ * reflected as an attribute — read the property, or the `change` event's `detail.value`, instead of
60
+ * `getAttribute('value')`/`[value]` (consistent with the rest of the form controls; see `lt-textfield`'s
61
+ * equivalent doc comment for why).
54
62
  * @default ''
55
63
  */
56
- value: string;
64
+ value: string | string[];
65
+ /**
66
+ * Allow more than one option to be selected. The option list stays open while picking, and `value` becomes an
67
+ * array of the selected values.
68
+ * @default false
69
+ */
70
+ multiple: boolean;
71
+ private get _selected();
57
72
  /**
58
73
  * Placeholder text shown when no option is selected.
59
74
  * @default 'Select an option'
@@ -89,13 +104,28 @@ export declare class Select extends ThemeableElement {
89
104
  * @default []
90
105
  */
91
106
  options: SelectOption[];
107
+ /** Overridable UI text (the required-field validation message), for i18n. English defaults — override only the keys you need. */
108
+ labels: Partial<SelectLabels>;
109
+ private get _labels();
92
110
  /**
93
111
  * Internal state tracking whether the dropdown is open.
94
112
  * @private
95
113
  */
96
114
  private isOpen;
115
+ /**
116
+ * Index of the highlighted option, into `enabledOptions` below (not `options`) — the same list the render
117
+ * and the keyboard handler both use, so they can never disagree about which option is highlighted (they used
118
+ * to: the index came from the enabled-only list, but the previous focus-based highlight came from whichever
119
+ * option actually held DOM focus, decided independently).
120
+ * @private
121
+ */
122
+ private activeIndex;
97
123
  private _cleanupClickOutside;
98
124
  private _floatingCleanup;
125
+ private readonly _fieldIds;
126
+ private get enabledOptions();
127
+ private _idFor;
128
+ private _preventFocusSteal;
99
129
  /**
100
130
  * Toggles the dropdown open/closed state.
101
131
  * @private
@@ -129,8 +159,12 @@ export declare class Select extends ThemeableElement {
129
159
  * @private
130
160
  */
131
161
  private getSelectedLabel;
162
+ willUpdate(changed: PropertyValues<this>): void;
132
163
  updated(changedProperties: Map<string, unknown>): void;
133
164
  formResetCallback(): void;
165
+ formDisabledCallback(disabled: boolean): void;
166
+ /** `this.disabled` combined with an ancestor `<fieldset>`'s own disabled state (see `FormDisabledController`). */
167
+ private get _isDisabled();
134
168
  checkValidity(): boolean;
135
169
  reportValidity(): boolean;
136
170
  render(): import("lit").TemplateResult<1>;
@@ -35,3 +35,8 @@ export interface SelectOption {
35
35
  */
36
36
  disabled?: boolean;
37
37
  }
38
+ /** Overridable UI text for `lt-select`, for i18n. */
39
+ export interface SelectLabels {
40
+ /** Validation message shown when a required select has no option chosen. */
41
+ validationMessage: string;
42
+ }
@@ -12,7 +12,7 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
12
12
  * element lives in the same tree as the internal control, so cross-shadow references
13
13
  * are forwarded but may be ignored by assistive technology.
14
14
  */
15
- export declare const FORWARDED_ARIA_ATTRIBUTES: readonly ["aria-pressed", "aria-expanded", "aria-haspopup", "aria-controls", "aria-current"];
15
+ export declare const FORWARDED_ARIA_ATTRIBUTES: readonly ["aria-pressed", "aria-expanded", "aria-haspopup", "aria-controls", "aria-current", "aria-description"];
16
16
  type AriaForwardingHost = ReactiveControllerHost & Element;
17
17
  /**
18
18
  * Reactive controller that forwards {@link FORWARDED_ARIA_ATTRIBUTES} from the
@@ -0,0 +1,30 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /**
3
+ * Captures a form-associated control's initial value the first time it connects — before any user interaction,
4
+ * or the control's own reflection, can overwrite it — so `formResetCallback` can restore what the markup
5
+ * actually asked for, the way a native `<input>`/`<select>` resets to its `defaultValue`/`defaultChecked`
6
+ * instead of a hard-coded empty one.
7
+ *
8
+ * A reconnect (an already-upgraded element moved in the DOM) does not recapture: only the very first connect
9
+ * counts as "the default", matching a real form control, which keeps its `defaultValue` fixed for its whole
10
+ * lifetime regardless of how many times it's detached and reattached.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * class Textfield extends ThemeableElement {
15
+ * private _default = new FormDefaultController(this, () => this.value);
16
+ *
17
+ * formResetCallback() {
18
+ * this.value = this._default.value;
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ export declare class FormDefaultController<T> implements ReactiveController {
24
+ private _get;
25
+ private _captured;
26
+ /** The captured default. Only meaningful once the host has connected at least once. */
27
+ value: T;
28
+ constructor(host: ReactiveControllerHost, get: () => T);
29
+ hostConnected(): void;
30
+ }
@@ -0,0 +1,40 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /**
3
+ * Tracks whether this form control is disabled because it's a descendant of a disabled `<fieldset>` — kept
4
+ * separate from the component's own explicit `disabled` property/attribute, which a consumer controls directly
5
+ * and which the fieldset toggling on or off must never overwrite in either direction. This mirrors how a native
6
+ * `<input>`'s `.disabled` IDL property only ever reflects its own attribute: a disabled fieldset makes it match
7
+ * `:disabled` and stop responding, but never touches the attribute itself, so re-enabling the fieldset can't
8
+ * accidentally un-disable an input the page explicitly disabled.
9
+ *
10
+ * The browser already makes the host match the `:disabled`/`:enabled` CSS pseudo-classes automatically for any
11
+ * form-associated custom element the moment a fieldset ancestor's `disabled` changes — nothing has to be done
12
+ * for that (verified in Chromium). This controller is for the *functional* side, which the platform does not
13
+ * wire up on its own: call {@link setFieldsetDisabled} from the component's own `formDisabledCallback`, and
14
+ * combine {@link fieldsetDisabled} with the component's own `disabled` wherever that gates interaction or the
15
+ * inner native control's own `disabled`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * class Textfield extends ThemeableElement {
20
+ * private _fieldsetDisabled = new FormDisabledController(this);
21
+ *
22
+ * formDisabledCallback(disabled: boolean) {
23
+ * this._fieldsetDisabled.setFieldsetDisabled(disabled);
24
+ * }
25
+ *
26
+ * private get _isDisabled(): boolean {
27
+ * return this.disabled || this._fieldsetDisabled.fieldsetDisabled;
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ export declare class FormDisabledController implements ReactiveController {
33
+ private _host;
34
+ private _fieldsetDisabled;
35
+ constructor(host: ReactiveControllerHost);
36
+ hostConnected(): void;
37
+ /** Whether an ancestor `<fieldset>` is currently disabled. */
38
+ get fieldsetDisabled(): boolean;
39
+ setFieldsetDisabled(disabled: boolean): void;
40
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Stable id for a field's helper text, so the control can point `aria-describedby` at it. Ids only need to be
3
+ * unique within one shadow root, but a counter keeps them unique across the page too.
4
+ */
5
+ export declare const createFieldIds: () => {
6
+ helper: string;
7
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Copies a live `ValidityState` into a plain object with its own enumerable properties, suitable for passing to
3
+ * `ElementInternals.setValidity()`.
4
+ *
5
+ * `ValidityState`'s flags (`valueMissing`, `typeMismatch`, etc.) are getters on its prototype, not own properties
6
+ * on the instance — spreading it (`{ ...validity }`) or `Object.keys(validity)` sees none of them. Real browsers'
7
+ * WebIDL dictionary conversion reads each named member individually, so passing the live object to `setValidity`
8
+ * happens to work there; jsdom's implementation enumerates instead, so it silently sees an empty flags object and
9
+ * treats the control as always valid. Converting explicitly avoids relying on which enumeration strategy the
10
+ * runtime uses, and is correct either way.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * this._internals.setValidity(toValidityFlags(input.validity), input.validationMessage, input);
15
+ * ```
16
+ */
17
+ export declare function toValidityFlags(validity: ValidityState): ValidityStateFlags;
@@ -0,0 +1,11 @@
1
+ /** Items a user can reach with the keyboard: everything not disabled, in order. */
2
+ export declare const enabledItems: <T extends {
3
+ disabled: boolean;
4
+ }>(items: readonly T[]) => T[];
5
+ /** The one item that's a Tab stop: the one matching `value`, else the first enabled item. */
6
+ export declare const rovingTargetOf: <T extends {
7
+ disabled: boolean;
8
+ value: string;
9
+ }>(enabled: readonly T[], value: string) => T | undefined;
10
+ /** One step forward or back, wrapping at both ends. An index of -1 (not in the list) steps from the start. */
11
+ export declare const stepIndex: (index: number, length: number, delta: 1 | -1) => number;
@@ -0,0 +1,2 @@
1
+ export declare function lockBodyScroll(): void;
2
+ export declare function unlockBodyScroll(): void;