@latty-ds/web 0.10.4 → 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.
- package/custom-elements.json +1969 -228
- package/dist/components/accordion/index.js +36 -2
- package/dist/components/alert/alert.d.ts +29 -6
- package/dist/components/alert/alert.types.d.ts +5 -0
- package/dist/components/alert/index.js +124 -17
- package/dist/components/avatar/avatar.d.ts +2 -0
- package/dist/components/avatar/index.js +10 -3
- package/dist/components/breadcrumb/breadcrumb.d.ts +4 -0
- package/dist/components/breadcrumb/breadcrumb.types.d.ts +5 -0
- package/dist/components/breadcrumb/index.js +20 -3
- package/dist/components/button/button.d.ts +33 -0
- package/dist/components/button/index.js +135 -12
- package/dist/components/calendar/calendar.d.ts +15 -7
- package/dist/components/calendar/calendar.types.d.ts +17 -1
- package/dist/components/calendar/index.js +78 -25
- package/dist/components/checkbox/checkbox.d.ts +14 -2
- package/dist/components/checkbox/checkbox.types.d.ts +5 -0
- package/dist/components/checkbox/index.js +82 -5
- package/dist/components/chip/chip.d.ts +4 -1
- package/dist/components/chip/chip.types.d.ts +5 -0
- package/dist/components/chip/index.js +29 -1
- package/dist/components/color-input/color-input.d.ts +11 -2
- package/dist/components/color-input/color-input.types.d.ts +5 -0
- package/dist/components/color-input/index.js +92 -8
- package/dist/components/combobox/combobox.d.ts +18 -2
- package/dist/components/combobox/combobox.types.d.ts +7 -0
- package/dist/components/combobox/index.js +164 -23
- package/dist/components/date-input/date-input.d.ts +22 -7
- package/dist/components/date-input/date-input.types.d.ts +7 -0
- package/dist/components/date-input/index.js +216 -53
- package/dist/components/datepicker/datepicker.d.ts +9 -1
- package/dist/components/datepicker/index.js +108 -15
- package/dist/components/dialog/dialog.d.ts +39 -19
- package/dist/components/dialog/dialog.types.d.ts +5 -0
- package/dist/components/dialog/index.js +237 -90
- package/dist/components/divider/index.js +2 -2
- package/dist/components/dropdown/dropdown.d.ts +4 -0
- package/dist/components/dropdown/index.js +43 -3
- package/dist/components/icon-button/icon-button.d.ts +4 -0
- package/dist/components/icon-button/index.js +36 -1
- package/dist/components/image/image.d.ts +7 -7
- package/dist/components/image/index.js +6 -11
- package/dist/components/list/index.js +14 -4
- package/dist/components/nav/index.js +37 -17
- package/dist/components/nav/nav-item.d.ts +8 -8
- package/dist/components/pagination/index.js +57 -16
- package/dist/components/pagination/pagination.d.ts +8 -1
- package/dist/components/pagination/pagination.types.d.ts +15 -0
- package/dist/components/progress/index.js +44 -6
- package/dist/components/progress/progress.d.ts +12 -0
- package/dist/components/radio/index.js +148 -9
- package/dist/components/radio/radio.d.ts +34 -2
- package/dist/components/radio/radio.types.d.ts +5 -0
- package/dist/components/radio-group/index.js +120 -29
- package/dist/components/radio-group/radio-group.d.ts +27 -2
- package/dist/components/select/index.js +197 -44
- package/dist/components/select/select.d.ts +38 -4
- package/dist/components/select/select.types.d.ts +5 -0
- package/dist/components/shared/aria-forwarding.d.ts +1 -1
- package/dist/components/shared/form-default.d.ts +30 -0
- package/dist/components/shared/form-disabled.d.ts +40 -0
- package/dist/components/shared/form-field.d.ts +7 -0
- package/dist/components/shared/form-validity.d.ts +17 -0
- package/dist/components/shared/roving.d.ts +11 -0
- package/dist/components/shared/scroll-lock.d.ts +2 -0
- package/dist/components/sidepanel/index.js +275 -72
- package/dist/components/sidepanel/sidepanel.d.ts +32 -10
- package/dist/components/sidepanel/sidepanel.types.d.ts +10 -1
- package/dist/components/skeleton/index.js +3 -8
- package/dist/components/slider/index.js +307 -75
- package/dist/components/slider/slider.d.ts +16 -3
- package/dist/components/snackbar/index.js +82 -9
- package/dist/components/snackbar/snackbar.d.ts +17 -1
- package/dist/components/snackbar/snackbar.types.d.ts +5 -0
- package/dist/components/surface/index.js +8 -0
- package/dist/components/switch/index.js +101 -5
- package/dist/components/switch/switch.d.ts +13 -2
- package/dist/components/switch/switch.types.d.ts +5 -0
- package/dist/components/tab/index.js +15 -14
- package/dist/components/tab/tab.d.ts +0 -1
- package/dist/components/tab-group/index.js +102 -30
- package/dist/components/tab-group/tab-group.d.ts +17 -2
- package/dist/components/table/index.js +105 -61
- package/dist/components/table/table.d.ts +8 -2
- package/dist/components/table/table.types.d.ts +4 -3
- package/dist/components/text/index.js +16 -1
- package/dist/components/text/text.d.ts +1 -1
- package/dist/components/textfield/index.js +243 -35
- package/dist/components/textfield/textfield.d.ts +24 -2
- package/dist/components/textfield/textfield.types.d.ts +11 -0
- package/dist/components/tooltip/index.js +176 -49
- package/dist/components/tooltip/tooltip.d.ts +17 -0
- package/dist/components/tooltip/tooltip.types.d.ts +5 -1
- package/dist/index.cjs +2458 -683
- package/dist/index.js +2469 -690
- package/dist/manifest.json +202 -32
- package/dist/utils/dev-warn.d.ts +5 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +3 -7
|
@@ -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-
|
|
1452
|
-
margin-
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
2116
|
+
if (this._isDisabled) return;
|
|
2003
2117
|
const { key } = e;
|
|
2004
|
-
if (!this.isOpen
|
|
2005
|
-
|
|
2006
|
-
|
|
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
|
-
|
|
2010
|
-
|
|
2125
|
+
const enabled = this.enabledOptions;
|
|
2126
|
+
switch (key) {
|
|
2127
|
+
case "Escape":
|
|
2011
2128
|
e.preventDefault();
|
|
2012
2129
|
this.closeDropdown();
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
if (key === "ArrowDown" || key === "ArrowUp") {
|
|
2130
|
+
break;
|
|
2131
|
+
case "ArrowDown":
|
|
2016
2132
|
e.preventDefault();
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
if (
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
2053
|
-
this._internals.setValidity({ valueMissing: true },
|
|
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.
|
|
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.
|
|
2238
|
+
aria-disabled=${this._isDisabled}
|
|
2100
2239
|
aria-label=${this.label || this.placeholder}
|
|
2101
|
-
|
|
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
|
-
|
|
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"
|
|
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(
|
|
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;
|