tyrell-components 1.0.0-TC53 → 1.0.0-TC55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tyrell.js +1 -1
- package/lib/components/calendar-month.js +17 -4
- package/lib/components/calendar.d.ts +5 -3
- package/lib/components/calendar.js +20 -14
- package/lib/components/copy.js +5 -4
- package/lib/components/date-picker.d.ts +7 -0
- package/lib/components/date-picker.js +25 -0
- package/lib/components/option.js +1 -1
- package/lib/components/scroll-container.js +9 -11
- package/lib/components/select.d.ts +70 -11
- package/lib/components/select.js +142 -62
- package/lib/components/selected-tags.js +4 -0
- package/lib/components/tabs.js +23 -18
- package/lib/styles/calendar-month.js +4 -4
- package/lib/styles/calendar-navigation.d.ts +1 -1
- package/lib/styles/calendar-navigation.js +8 -4
- package/lib/styles/calendar.d.ts +1 -1
- package/lib/styles/calendar.js +1 -0
- package/lib/styles/select-base.js +81 -14
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/components/select.js
CHANGED
|
@@ -20,10 +20,10 @@ elementIds.set(element, id);
|
|
|
20
20
|
return id;
|
|
21
21
|
}
|
|
22
22
|
import { REQUIRED_ICON_SVG } from "../utils/icons.js";
|
|
23
|
+
const SEARCH_AUTO_THRESHOLD = 7;
|
|
23
24
|
const CHEVRON_DOWN_SVG = `<svg viewBox="0 0 20 20" fill="currentColor">
|
|
24
25
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
25
26
|
</svg>`;
|
|
26
|
-
const SEARCH_AUTO_THRESHOLD = 7;
|
|
27
27
|
function slugify(text) {
|
|
28
28
|
return text
|
|
29
29
|
.trim()
|
|
@@ -55,11 +55,12 @@ this._clearable = true;
|
|
|
55
55
|
this._externalSearch = false;
|
|
56
56
|
this._allowCreate = false;
|
|
57
57
|
this._createTransform = "none";
|
|
58
|
-
this.
|
|
58
|
+
this._notSearchable = false;
|
|
59
59
|
this._loading = false;
|
|
60
60
|
this._scrollLockId = null;
|
|
61
61
|
this._size = DEFAULT_SIZE;
|
|
62
62
|
this._availableLabel = "Available";
|
|
63
|
+
this._selectedLabel = "Selected";
|
|
63
64
|
this._noOptionsMessage = "No options available";
|
|
64
65
|
this._state = {
|
|
65
66
|
open: false,
|
|
@@ -94,9 +95,7 @@ dialog.close();
|
|
|
94
95
|
}
|
|
95
96
|
});
|
|
96
97
|
this.render();
|
|
97
|
-
requestAnimationFrame(() => {
|
|
98
98
|
this.initializeState();
|
|
99
|
-
});
|
|
100
99
|
this._childObserver = new MutationObserver(() => {
|
|
101
100
|
this.ensureOptionRoles();
|
|
102
101
|
if (this._state.selectedValues.length > 0) {
|
|
@@ -187,8 +186,8 @@ break;
|
|
|
187
186
|
case "externalSearch":
|
|
188
187
|
this._externalSearch = newValue;
|
|
189
188
|
break;
|
|
190
|
-
case "
|
|
191
|
-
this.
|
|
189
|
+
case "notSearchable":
|
|
190
|
+
this._notSearchable = newValue;
|
|
192
191
|
if (this.isConnected && this.shadowRoot)
|
|
193
192
|
this.updateSearchVisibility();
|
|
194
193
|
break;
|
|
@@ -209,6 +208,9 @@ break;
|
|
|
209
208
|
case "available-label":
|
|
210
209
|
this._availableLabel = newValue || "Available";
|
|
211
210
|
break;
|
|
211
|
+
case "selected-label":
|
|
212
|
+
this._selectedLabel = newValue || "Selected";
|
|
213
|
+
break;
|
|
212
214
|
case "no-options-message":
|
|
213
215
|
this._noOptionsMessage = newValue || "No options available";
|
|
214
216
|
break;
|
|
@@ -268,13 +270,23 @@ this.updateSelectionDisplay();
|
|
|
268
270
|
this.updateMobileSelectedState();
|
|
269
271
|
}
|
|
270
272
|
else {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
this.resolveDeclarativeSelection();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
resolveDeclarativeSelection() {
|
|
277
|
+
if (this.getAttribute("value") || this._state.selectedValues.length > 0) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const preSelectedTags = this.getTagElements()
|
|
273
281
|
.filter((tag) => tag.hasAttribute("selected"))
|
|
274
282
|
.map((tag) => this.getTagData(tag).value);
|
|
275
283
|
if (preSelectedTags.length > 0) {
|
|
276
284
|
this.updateComponentValue(preSelectedTags, false);
|
|
285
|
+
return;
|
|
277
286
|
}
|
|
287
|
+
if (document.readyState === "loading") {
|
|
288
|
+
document.addEventListener("DOMContentLoaded", () => { if (this.isConnected)
|
|
289
|
+
this.resolveDeclarativeSelection(); }, { once: true });
|
|
278
290
|
}
|
|
279
291
|
}
|
|
280
292
|
getTagElements() {
|
|
@@ -295,10 +307,7 @@ deselectTag(tag) {
|
|
|
295
307
|
tag.removeAttribute("selected");
|
|
296
308
|
}
|
|
297
309
|
getSelectedValues() {
|
|
298
|
-
return this.
|
|
299
|
-
.filter((tag) => tag.hasAttribute("selected"))
|
|
300
|
-
.map((tag) => this.getTagData(tag).value)
|
|
301
|
-
.filter((value) => value !== "");
|
|
310
|
+
return [...this._state.selectedValues];
|
|
302
311
|
}
|
|
303
312
|
syncSelectedTags(selectedValues) {
|
|
304
313
|
const selectedSet = new Set(selectedValues);
|
|
@@ -612,7 +621,7 @@ this.closeDropdown();
|
|
|
612
621
|
}
|
|
613
622
|
return;
|
|
614
623
|
}
|
|
615
|
-
if (
|
|
624
|
+
if (currentValues.includes(tagValue)) {
|
|
616
625
|
const newValues = currentValues.filter((v) => v !== tagValue);
|
|
617
626
|
this.updateComponentValue(newValues, true, "remove", tagValue);
|
|
618
627
|
}
|
|
@@ -693,6 +702,8 @@ e.stopPropagation();
|
|
|
693
702
|
e.preventDefault();
|
|
694
703
|
}
|
|
695
704
|
handleSearchInput(e) {
|
|
705
|
+
if (this._state.mode === "mobile")
|
|
706
|
+
this.expandMobileSection("available");
|
|
696
707
|
const target = e.target;
|
|
697
708
|
const query = target.value;
|
|
698
709
|
this._state.search = query;
|
|
@@ -725,7 +736,12 @@ switch (e.key) {
|
|
|
725
736
|
case "Escape":
|
|
726
737
|
e.preventDefault();
|
|
727
738
|
e.stopPropagation();
|
|
739
|
+
if (this._state.mode === "mobile") {
|
|
740
|
+
this.closeMobileModal();
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
728
743
|
this.closeDropdown();
|
|
744
|
+
}
|
|
729
745
|
break;
|
|
730
746
|
case "Enter":
|
|
731
747
|
e.preventDefault();
|
|
@@ -741,43 +757,33 @@ break;
|
|
|
741
757
|
case "ArrowUp":
|
|
742
758
|
e.preventDefault();
|
|
743
759
|
e.stopPropagation();
|
|
744
|
-
|
|
745
|
-
if (tagsCount === 0) {
|
|
746
|
-
newIndexUp = -1;
|
|
747
|
-
}
|
|
748
|
-
else if (currentHighlightedIndex === -1) {
|
|
749
|
-
newIndexUp = tagsCount - 1;
|
|
750
|
-
}
|
|
751
|
-
else if (currentHighlightedIndex === 0) {
|
|
752
|
-
newIndexUp = tagsCount - 1;
|
|
753
|
-
}
|
|
754
|
-
else {
|
|
755
|
-
newIndexUp = currentHighlightedIndex - 1;
|
|
756
|
-
}
|
|
760
|
+
const newIndexUp = this.nextEnabledTagIndex(filteredTags, currentHighlightedIndex, -1);
|
|
757
761
|
this._state.highlightedIndex = newIndexUp;
|
|
758
762
|
this.highlightTag(filteredTags, newIndexUp);
|
|
759
763
|
break;
|
|
760
764
|
case "ArrowDown":
|
|
761
765
|
e.preventDefault();
|
|
762
766
|
e.stopPropagation();
|
|
763
|
-
|
|
764
|
-
if (tagsCount === 0) {
|
|
765
|
-
newIndexDown = -1;
|
|
766
|
-
}
|
|
767
|
-
else if (currentHighlightedIndex === -1) {
|
|
768
|
-
newIndexDown = 0;
|
|
769
|
-
}
|
|
770
|
-
else if (currentHighlightedIndex === tagsCount - 1) {
|
|
771
|
-
newIndexDown = 0;
|
|
772
|
-
}
|
|
773
|
-
else {
|
|
774
|
-
newIndexDown = currentHighlightedIndex + 1;
|
|
775
|
-
}
|
|
767
|
+
const newIndexDown = this.nextEnabledTagIndex(filteredTags, currentHighlightedIndex, 1);
|
|
776
768
|
this._state.highlightedIndex = newIndexDown;
|
|
777
769
|
this.highlightTag(filteredTags, newIndexDown);
|
|
778
770
|
break;
|
|
779
771
|
}
|
|
780
772
|
}
|
|
773
|
+
nextEnabledTagIndex(tags, fromIndex, direction) {
|
|
774
|
+
const count = tags.length;
|
|
775
|
+
if (count === 0)
|
|
776
|
+
return -1;
|
|
777
|
+
let index = fromIndex === -1
|
|
778
|
+
? (direction === 1 ? 0 : count - 1)
|
|
779
|
+
: (fromIndex + direction + count) % count;
|
|
780
|
+
for (let i = 0; i < count; i++) {
|
|
781
|
+
if (!tags[index].element.hasAttribute("disabled"))
|
|
782
|
+
return index;
|
|
783
|
+
index = (index + direction + count) % count;
|
|
784
|
+
}
|
|
785
|
+
return -1;
|
|
786
|
+
}
|
|
781
787
|
filterTags(tags, query) {
|
|
782
788
|
if (!query || query.trim() === "") {
|
|
783
789
|
return tags;
|
|
@@ -1093,12 +1099,21 @@ ${CHEVRON_DOWN_SVG}
|
|
|
1093
1099
|
<!-- HEADER (matches dropdown.ts) -->
|
|
1094
1100
|
${searchHeaderHtml}
|
|
1095
1101
|
<!-- BODY: options list with toggleable selected state -->
|
|
1096
|
-
<div class="mobile-body">
|
|
1102
|
+
<div class="mobile-body" data-expanded="available">
|
|
1103
|
+
<!-- SELECTED STRIP (pinned above the list, collapses when empty) -->
|
|
1104
|
+
<div class="mobile-selected-section" data-empty="true">
|
|
1105
|
+
<button class="section-header" type="button" data-section="selected" aria-expanded="false">
|
|
1106
|
+
<span class="section-title">${this._selectedLabel} <span class="section-count">(0)</span></span>
|
|
1107
|
+
${CHEVRON_DOWN_SVG}
|
|
1108
|
+
</button>
|
|
1109
|
+
<div class="section-content mobile-selected-chips"></div>
|
|
1110
|
+
</div>
|
|
1097
1111
|
<!-- AVAILABLE LIST (always visible, takes remaining space) -->
|
|
1098
1112
|
<div class="mobile-available-section" data-empty="false">
|
|
1099
|
-
<
|
|
1113
|
+
<button class="section-header" type="button" data-section="available" aria-expanded="true">
|
|
1100
1114
|
<span class="section-title">${this._availableLabel}</span>
|
|
1101
|
-
|
|
1115
|
+
${CHEVRON_DOWN_SVG}
|
|
1116
|
+
</button>
|
|
1102
1117
|
<div class="section-content dropdown-options-wrapper" id="${listboxId}" role="listbox" ${this._multiple ? 'aria-multiselectable="true"' : ""}>
|
|
1103
1118
|
<slot id="options-slot"></slot>
|
|
1104
1119
|
<div class="empty-state">${this._noOptionsMessage}</div>
|
|
@@ -1121,6 +1136,10 @@ this.setupMobileEventListeners();
|
|
|
1121
1136
|
this.updateSelectionDisplay();
|
|
1122
1137
|
}
|
|
1123
1138
|
setupMobileEventListeners() {
|
|
1139
|
+
if (!this._keyboardHandler) {
|
|
1140
|
+
this._keyboardHandler = this.handleKeyboard.bind(this);
|
|
1141
|
+
document.addEventListener("keydown", this._keyboardHandler);
|
|
1142
|
+
}
|
|
1124
1143
|
const shadow = this.shadowRoot;
|
|
1125
1144
|
const stub = shadow.querySelector(".select-stub");
|
|
1126
1145
|
const optionsSlot = shadow.querySelector("#options-slot");
|
|
@@ -1140,6 +1159,9 @@ optionsSlot.addEventListener("click", (e) => this.handleMobileTagClick(e));
|
|
|
1140
1159
|
if (searchInput) {
|
|
1141
1160
|
searchInput.addEventListener("input", (e) => this.handleSearchInput(e));
|
|
1142
1161
|
}
|
|
1162
|
+
shadow.querySelectorAll(".mobile-body .section-header").forEach((header) => {
|
|
1163
|
+
header.addEventListener("click", () => this.expandMobileSection(header.dataset.section === "selected" ? "selected" : "available"));
|
|
1164
|
+
});
|
|
1143
1165
|
if (closeButton) {
|
|
1144
1166
|
closeButton.addEventListener("click", () => this.closeMobileModal());
|
|
1145
1167
|
}
|
|
@@ -1178,9 +1200,10 @@ updateMobileSelectedState() {
|
|
|
1178
1200
|
if (this._state.mode !== "mobile")
|
|
1179
1201
|
return;
|
|
1180
1202
|
const shadow = this.shadowRoot;
|
|
1203
|
+
this.renderMobileSelectedChips();
|
|
1181
1204
|
const availableSection = shadow.querySelector(".mobile-available-section");
|
|
1182
1205
|
if (availableSection) {
|
|
1183
|
-
const visibleAvailable = this.getTagElements().filter((tag) => !tag.hasAttribute("hidden")).length;
|
|
1206
|
+
const visibleAvailable = this.getTagElements().filter((tag) => !tag.hasAttribute("hidden") && !tag.hasAttribute("selected")).length;
|
|
1184
1207
|
availableSection.setAttribute("data-empty", String(visibleAvailable === 0));
|
|
1185
1208
|
const availableTitleSpan = shadow.querySelector(".mobile-available-section .section-title");
|
|
1186
1209
|
if (availableTitleSpan) {
|
|
@@ -1188,14 +1211,59 @@ availableTitleSpan.textContent = `${this._availableLabel} (${visibleAvailable})`
|
|
|
1188
1211
|
}
|
|
1189
1212
|
}
|
|
1190
1213
|
}
|
|
1214
|
+
expandMobileSection(section) {
|
|
1215
|
+
const shadow = this.shadowRoot;
|
|
1216
|
+
const body = shadow?.querySelector(".mobile-body");
|
|
1217
|
+
if (!body)
|
|
1218
|
+
return;
|
|
1219
|
+
if (section === "selected" && this._state.selectedValues.length === 0)
|
|
1220
|
+
return;
|
|
1221
|
+
body.dataset.expanded = section;
|
|
1222
|
+
shadow.querySelectorAll(".mobile-body .section-header").forEach((header) => {
|
|
1223
|
+
const owns = header.dataset.section === section;
|
|
1224
|
+
header.setAttribute("aria-expanded", String(owns));
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
renderMobileSelectedChips() {
|
|
1228
|
+
const shadow = this.shadowRoot;
|
|
1229
|
+
const section = shadow.querySelector(".mobile-selected-section");
|
|
1230
|
+
const host = shadow.querySelector(".mobile-selected-chips");
|
|
1231
|
+
if (!section || !host)
|
|
1232
|
+
return;
|
|
1233
|
+
const selected = this._state.selectedValues;
|
|
1234
|
+
section.setAttribute("data-empty", String(selected.length === 0));
|
|
1235
|
+
const count = section.querySelector(".section-count");
|
|
1236
|
+
if (count)
|
|
1237
|
+
count.textContent = `(${selected.length})`;
|
|
1238
|
+
if (selected.length === 0)
|
|
1239
|
+
this.expandMobileSection("available");
|
|
1240
|
+
const labelFor = (value) => {
|
|
1241
|
+
const option = this.getTagElements().find((tag) => this.getTagData(tag).value === value);
|
|
1242
|
+
return option ? this.getTagData(option).text : value;
|
|
1243
|
+
};
|
|
1244
|
+
host.textContent = "";
|
|
1245
|
+
for (const value of selected) {
|
|
1246
|
+
const chip = document.createElement("button");
|
|
1247
|
+
chip.type = "button";
|
|
1248
|
+
chip.className = "mobile-selected-chip";
|
|
1249
|
+
chip.dataset.value = value;
|
|
1250
|
+
chip.setAttribute("aria-label", `Remove ${labelFor(value)}`);
|
|
1251
|
+
chip.innerHTML = `<span class="chip-label"></span>${CLEAR_ICON_SVG}`;
|
|
1252
|
+
chip.querySelector(".chip-label").textContent = labelFor(value);
|
|
1253
|
+
chip.addEventListener("click", (e) => {
|
|
1254
|
+
e.preventDefault();
|
|
1255
|
+
e.stopPropagation();
|
|
1256
|
+
this.deselectValue(value);
|
|
1257
|
+
});
|
|
1258
|
+
host.appendChild(chip);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1191
1261
|
shouldShowSearch() {
|
|
1192
1262
|
if (this._externalSearch)
|
|
1193
1263
|
return true;
|
|
1194
1264
|
if (this._allowCreate)
|
|
1195
1265
|
return true;
|
|
1196
|
-
if (this.
|
|
1197
|
-
return true;
|
|
1198
|
-
if (this._searchable === "never")
|
|
1266
|
+
if (this._notSearchable)
|
|
1199
1267
|
return false;
|
|
1200
1268
|
return this.getTagElements().length > SEARCH_AUTO_THRESHOLD;
|
|
1201
1269
|
}
|
|
@@ -1234,10 +1302,11 @@ clearBtn.hidden = !(count > 0 && this._clearable && !this._disabled && !this._re
|
|
|
1234
1302
|
}
|
|
1235
1303
|
const wantClone = !this._multiple && count > 0 ? values[0] : null;
|
|
1236
1304
|
this.syncSelectedClone(wantClone);
|
|
1237
|
-
|
|
1305
|
+
const hasClone = this._selectedClone !== null;
|
|
1306
|
+
stub.classList.toggle("has-clone", hasClone);
|
|
1238
1307
|
const textEl = stub.querySelector(".dropdown-placeholder");
|
|
1239
1308
|
if (textEl) {
|
|
1240
|
-
textEl.hidden =
|
|
1309
|
+
textEl.hidden = hasClone;
|
|
1241
1310
|
const showLabels = count > 0 && this._multiple && !this._compact;
|
|
1242
1311
|
if (showLabels) {
|
|
1243
1312
|
textEl.textContent = this.getSelectedItems(values)
|
|
@@ -1245,6 +1314,10 @@ textEl.textContent = this.getSelectedItems(values)
|
|
|
1245
1314
|
.join(", ");
|
|
1246
1315
|
textEl.classList.remove("placeholder-shown");
|
|
1247
1316
|
}
|
|
1317
|
+
else if (wantClone !== null) {
|
|
1318
|
+
textEl.textContent = values.join(", ");
|
|
1319
|
+
textEl.classList.remove("placeholder-shown");
|
|
1320
|
+
}
|
|
1248
1321
|
else {
|
|
1249
1322
|
textEl.textContent = this._placeholder;
|
|
1250
1323
|
textEl.classList.add("placeholder-shown");
|
|
@@ -1254,9 +1327,14 @@ textEl.classList.add("placeholder-shown");
|
|
|
1254
1327
|
syncSelectedClone(value) {
|
|
1255
1328
|
const existing = this.querySelector(':scope > [cloned][slot="selected"]');
|
|
1256
1329
|
if (existing?.getAttribute("value") === value) {
|
|
1330
|
+
const stale = value !== null &&
|
|
1331
|
+
existing.childNodes.length === 0 &&
|
|
1332
|
+
(this.getTagElements().find((el) => this.getTagData(el).value === value)?.childNodes.length ?? 0) > 0;
|
|
1333
|
+
if (!stale) {
|
|
1257
1334
|
this._selectedClone = existing;
|
|
1258
1335
|
return;
|
|
1259
1336
|
}
|
|
1337
|
+
}
|
|
1260
1338
|
existing?.remove();
|
|
1261
1339
|
if (value === null) {
|
|
1262
1340
|
this._selectedClone = null;
|
|
@@ -1270,6 +1348,10 @@ clone.setAttribute("slot", "selected");
|
|
|
1270
1348
|
clone.setAttribute("cloned", "true");
|
|
1271
1349
|
this.appendChild(clone);
|
|
1272
1350
|
this._selectedClone = clone;
|
|
1351
|
+
if (option.childNodes.length === 0 && document.readyState === "loading") {
|
|
1352
|
+
document.addEventListener("DOMContentLoaded", () => { if (this.isConnected)
|
|
1353
|
+
this.updateSelectionDisplay(); }, { once: true });
|
|
1354
|
+
}
|
|
1273
1355
|
}
|
|
1274
1356
|
else if (this._selectedClone?.getAttribute("value") === value) {
|
|
1275
1357
|
const restored = this._selectedClone.cloneNode(true);
|
|
@@ -1352,11 +1434,11 @@ return this.getProperty("required");
|
|
|
1352
1434
|
set required(value) {
|
|
1353
1435
|
this.setProperty("required", value);
|
|
1354
1436
|
}
|
|
1355
|
-
get
|
|
1356
|
-
return this.getProperty("
|
|
1437
|
+
get notSearchable() {
|
|
1438
|
+
return this.getProperty("notSearchable");
|
|
1357
1439
|
}
|
|
1358
|
-
set
|
|
1359
|
-
this.setProperty("
|
|
1440
|
+
set notSearchable(value) {
|
|
1441
|
+
this.setProperty("notSearchable", value);
|
|
1360
1442
|
}
|
|
1361
1443
|
get externalSearch() {
|
|
1362
1444
|
return this.getProperty("externalSearch");
|
|
@@ -1480,17 +1562,10 @@ default: "none",
|
|
|
1480
1562
|
aliases: { "create-transform": true },
|
|
1481
1563
|
coerce: (v) => (v === "slug" ? "slug" : "none"),
|
|
1482
1564
|
},
|
|
1483
|
-
|
|
1484
|
-
type: "
|
|
1565
|
+
notSearchable: {
|
|
1566
|
+
type: "boolean",
|
|
1485
1567
|
visual: true,
|
|
1486
|
-
default:
|
|
1487
|
-
coerce: (v) => {
|
|
1488
|
-
if (v === true || v === "" || v === "true")
|
|
1489
|
-
return "always";
|
|
1490
|
-
if (v === false || v === "false")
|
|
1491
|
-
return "never";
|
|
1492
|
-
return "auto";
|
|
1493
|
-
},
|
|
1568
|
+
default: false,
|
|
1494
1569
|
},
|
|
1495
1570
|
align: {
|
|
1496
1571
|
type: "string",
|
|
@@ -1533,6 +1608,11 @@ type: "string",
|
|
|
1533
1608
|
visual: true,
|
|
1534
1609
|
default: "Available",
|
|
1535
1610
|
},
|
|
1611
|
+
"selected-label": {
|
|
1612
|
+
type: "string",
|
|
1613
|
+
visual: true,
|
|
1614
|
+
default: "Selected",
|
|
1615
|
+
},
|
|
1536
1616
|
"no-options-message": {
|
|
1537
1617
|
type: "string",
|
|
1538
1618
|
visual: true,
|
|
@@ -29,6 +29,10 @@ if (!this.style.display)
|
|
|
29
29
|
this.style.display = 'contents';
|
|
30
30
|
this.bind();
|
|
31
31
|
this.renderChips();
|
|
32
|
+
if (document.readyState === 'loading') {
|
|
33
|
+
document.addEventListener('DOMContentLoaded', () => { if (this.isConnected)
|
|
34
|
+
this.renderChips(); }, { once: true });
|
|
35
|
+
}
|
|
32
36
|
this._childObserver = new MutationObserver(() => {
|
|
33
37
|
const template = this.template();
|
|
34
38
|
if (template !== this._lastTemplate) {
|
package/lib/components/tabs.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ensureStyles } from '../utils/styles.js';
|
|
|
2
2
|
import { tabsStyles } from '../styles/tabs.js';
|
|
3
3
|
const eventHandlers = new WeakMap();
|
|
4
4
|
const resizeObservers = new WeakMap();
|
|
5
|
+
const pendingInitialSnap = new WeakSet();
|
|
5
6
|
function getTabsAttributes(el) {
|
|
6
7
|
return {
|
|
7
8
|
width: el.getAttribute('width') || '100%',
|
|
@@ -40,6 +41,11 @@ function findTabIndex(tabs, tabId) {
|
|
|
40
41
|
const index = tabs.findIndex(tab => getTabId(tab) === tabId);
|
|
41
42
|
return index >= 0 ? index : undefined;
|
|
42
43
|
}
|
|
44
|
+
function currentActive(el) {
|
|
45
|
+
const tabs = getChildTabs(el);
|
|
46
|
+
const id = getActiveTabId(el, tabs);
|
|
47
|
+
return { id, index: (id ? findTabIndex(tabs, id) : undefined) ?? 0 };
|
|
48
|
+
}
|
|
43
49
|
function getActiveTabId(el, tabs) {
|
|
44
50
|
const activeAttr = el.getAttribute('active');
|
|
45
51
|
if (activeAttr && findTabIndex(tabs, activeAttr) !== undefined) {
|
|
@@ -171,17 +177,13 @@ const button = shadowRoot.querySelector(`[data-tab-id='${activeId}']`);
|
|
|
171
177
|
const buttonsContainer = shadowRoot.querySelector('.tab-buttons');
|
|
172
178
|
if (!button || !buttonsContainer)
|
|
173
179
|
return null;
|
|
174
|
-
const buttonRect = button.getBoundingClientRect();
|
|
175
180
|
const left = button.offsetLeft;
|
|
176
181
|
const top = button.offsetTop;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
top,
|
|
180
|
-
width: buttonRect.width,
|
|
181
|
-
height: buttonRect.height,
|
|
182
|
-
};
|
|
182
|
+
const width = button.offsetWidth;
|
|
183
|
+
const height = button.offsetHeight;
|
|
184
|
+
return { left, top, width, height };
|
|
183
185
|
}
|
|
184
|
-
function updateMarker(el, activeId) {
|
|
186
|
+
function updateMarker(el, activeId, forceSnap = false) {
|
|
185
187
|
const shadowRoot = el.shadowRoot;
|
|
186
188
|
if (!shadowRoot)
|
|
187
189
|
return;
|
|
@@ -191,7 +193,7 @@ return;
|
|
|
191
193
|
const position = calculateMarkerPosition(el, shadowRoot, activeId);
|
|
192
194
|
if (!position)
|
|
193
195
|
return;
|
|
194
|
-
const snap = !marker.style.left || marker.offsetWidth === 0;
|
|
196
|
+
const snap = forceSnap || !marker.style.left || marker.offsetWidth === 0;
|
|
195
197
|
if (snap)
|
|
196
198
|
marker.style.transition = 'none';
|
|
197
199
|
marker.style.left = `${position.left}px`;
|
|
@@ -216,7 +218,7 @@ el.style.setProperty('--active-index', String(newIndex));
|
|
|
216
218
|
updateTransform(el, newIndex);
|
|
217
219
|
updateAriaAttributes(el, shadowRoot, tabId);
|
|
218
220
|
updatePanelInteraction(el, tabId);
|
|
219
|
-
updateMarker(el, tabId);
|
|
221
|
+
updateMarker(el, tabId, pendingInitialSnap.delete(el));
|
|
220
222
|
shadowRoot.querySelectorAll('.tab-overflow-item').forEach((item) => {
|
|
221
223
|
item.setAttribute('data-active', String(item.dataset.tabId === tabId));
|
|
222
224
|
});
|
|
@@ -470,11 +472,12 @@ if (buttons) {
|
|
|
470
472
|
const buttonsHeight = buttons.offsetHeight;
|
|
471
473
|
el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
|
|
472
474
|
}
|
|
473
|
-
|
|
475
|
+
const current = currentActive(el);
|
|
476
|
+
updateTransform(el, current.index);
|
|
474
477
|
updateOverflow(el);
|
|
475
|
-
if (
|
|
476
|
-
updateMarker(el,
|
|
477
|
-
scrollActiveIntoView(el,
|
|
478
|
+
if (current.id) {
|
|
479
|
+
updateMarker(el, current.id);
|
|
480
|
+
scrollActiveIntoView(el, current.id, true);
|
|
478
481
|
}
|
|
479
482
|
});
|
|
480
483
|
if (activeId) {
|
|
@@ -482,6 +485,7 @@ updatePanelInteraction(el, activeId);
|
|
|
482
485
|
}
|
|
483
486
|
}
|
|
484
487
|
else {
|
|
488
|
+
pendingInitialSnap.add(el);
|
|
485
489
|
shadowRoot.innerHTML = `
|
|
486
490
|
<div class="tabs-container" data-placement="${placement}"${fixed ? ' data-fixed' : ''}>
|
|
487
491
|
${renderTabButtons(el, tabs, activeId)}
|
|
@@ -498,11 +502,12 @@ if (buttons) {
|
|
|
498
502
|
const buttonsHeight = buttons.offsetHeight;
|
|
499
503
|
el.style.setProperty('--buttons-height', `${buttonsHeight}px`);
|
|
500
504
|
}
|
|
501
|
-
|
|
505
|
+
const current = currentActive(el);
|
|
506
|
+
updateTransform(el, current.index);
|
|
502
507
|
updateOverflow(el);
|
|
503
|
-
if (
|
|
504
|
-
updateMarker(el,
|
|
505
|
-
scrollActiveIntoView(el,
|
|
508
|
+
if (current.id) {
|
|
509
|
+
updateMarker(el, current.id);
|
|
510
|
+
scrollActiveIntoView(el, current.id, false);
|
|
506
511
|
}
|
|
507
512
|
});
|
|
508
513
|
setupEventListeners(el, shadowRoot, tabs);
|
|
@@ -39,7 +39,7 @@ border-radius: var(--ty-radius-md);
|
|
|
39
39
|
font-family: inherit;
|
|
40
40
|
user-select: none;
|
|
41
41
|
width: var(--calendar-width, fit-content);
|
|
42
|
-
min-width: 280px;
|
|
42
|
+
min-width: var(--ty-calendar-width, 280px);
|
|
43
43
|
max-width: var(--calendar-max-width, none);
|
|
44
44
|
}
|
|
45
45
|
.calendar-row {
|
|
@@ -135,7 +135,7 @@ opacity: 0.7;
|
|
|
135
135
|
}
|
|
136
136
|
.calendar-size-sm {
|
|
137
137
|
padding: 0.5rem;
|
|
138
|
-
min-width: 240px;
|
|
138
|
+
min-width: var(--ty-calendar-width, 240px);
|
|
139
139
|
}
|
|
140
140
|
.calendar-size-sm .calendar-header-cell {
|
|
141
141
|
font-size: 0.625rem;
|
|
@@ -150,7 +150,7 @@ min-height: 1.5rem;
|
|
|
150
150
|
}
|
|
151
151
|
.calendar-size-md {
|
|
152
152
|
padding: 0.75rem;
|
|
153
|
-
min-width: 280px;
|
|
153
|
+
min-width: var(--ty-calendar-width, 280px);
|
|
154
154
|
}
|
|
155
155
|
.calendar-size-md .calendar-header-cell {
|
|
156
156
|
font-size: 0.6875rem;
|
|
@@ -165,7 +165,7 @@ min-height: 2rem;
|
|
|
165
165
|
}
|
|
166
166
|
.calendar-size-lg {
|
|
167
167
|
padding: 1rem;
|
|
168
|
-
min-width: 360px;
|
|
168
|
+
min-width: var(--ty-calendar-width, 360px);
|
|
169
169
|
}
|
|
170
170
|
.calendar-size-lg .calendar-header-cell {
|
|
171
171
|
font-size: 0.75rem;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Calendar Navigation Styles */
|
|
2
|
-
export declare const calendarNavigationStyles = "\n/* Theming tokens applied as var(--ty-calendar-nav-*, <default>) at point of\n use (NOT on :host) so consumers can override them from outside \u2014 this\n component is nested inside ty-calendar's shadow, so a :host default would\n block inherited overrides. --ty-calendar-accent still cascades in. */\n\n:host([data-size=\"md\"]) {\n --nav-btn-size: 2rem;\n --nav-btn-icon-size: 1.25rem;\n --nav-font-size:
|
|
2
|
+
export declare const calendarNavigationStyles = "\n/* Theming tokens applied as var(--ty-calendar-nav-*, <default>) at point of\n use (NOT on :host) so consumers can override them from outside \u2014 this\n component is nested inside ty-calendar's shadow, so a :host default would\n block inherited overrides. --ty-calendar-accent still cascades in. */\n\n:host([data-size=\"md\"]) {\n --nav-btn-size: 2rem;\n --nav-btn-icon-size: 1.25rem;\n --nav-font-size: 0.875rem;\n --nav-padding: 0.5rem 0.75rem;\n --nav-default-width: 280px;\n}\n\n:host([data-size=\"sm\"]) {\n --nav-btn-size: 1.5rem;\n --nav-btn-icon-size: 1rem;\n --nav-font-size: 0.875rem;\n --nav-padding: 0.375rem 0.5rem;\n --nav-default-width: 240px;\n}\n\n:host([data-size=\"lg\"]) {\n --nav-btn-size: 2.5rem;\n --nav-btn-icon-size: 1.5rem;\n --nav-font-size: 0.875rem;\n --nav-padding: 0.625rem 1rem;\n --nav-default-width: 360px;\n}\n\n.calendar-navigation-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--nav-padding);\n font-family: inherit;\n user-select: none;\n /* --ty-calendar-width is the public knob: set it on ty-calendar (or any\n ancestor) and BOTH the nav and the day grid follow it, because custom\n properties inherit through shadow boundaries. --nav-default-width can't\n serve that role \u2014 it's declared on :host([data-size]) inside this shadow\n root, and a declaration on the element always beats a value inherited\n into it, so an outside override of it is silently dead. --nav-width\n (set inline by the width attribute) still wins over both. */\n width: var(--nav-width, var(--ty-calendar-width, var(--nav-default-width)));\n box-sizing: border-box;\n}\n\n/* Navigation groups: left, center, right */\n.nav-group {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.nav-group-left,\n.nav-group-right {\n flex: 0 0 auto;\n}\n\n.nav-group-center {\n flex: 1;\n justify-content: center;\n /* Flex items default to min-width:auto, i.e. they refuse to shrink below\n their content. A long month name (\"September 2026\") therefore widened\n this group and shoved the arrow groups outward \u2014 the nav visibly\n re-laid-out as you paged month to month. min-width:0 lets the centre\n take exactly the leftover space, so the arrows stay pinned whatever the\n label says, and an over-long label ellipsizes instead of pushing. */\n min-width: 0;\n}\n\n.nav-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--nav-btn-size);\n height: var(--nav-btn-size);\n padding: 0;\n border: none;\n border-radius: var(--ty-radius-base);\n background-color: transparent;\n color: var(--ty-calendar-nav-color, var(--ty-color-neutral));\n cursor: pointer;\n transition: var(--ty-local-transition, all 0.15s ease);\n outline: none;\n}\n\n.nav-btn:hover {\n background-color: var(--ty-calendar-nav-hover-bg, var(--ty-bg-neutral-soft));\n color: var(--ty-calendar-nav-hover-color, var(--ty-color-neutral-strong));\n}\n\n.nav-btn:active {\n background-color: var(--ty-calendar-nav-active-bg, var(--ty-bg-neutral));\n transform: scale(0.95);\n}\n\n.nav-btn:disabled {\n opacity: 0.35;\n cursor: default;\n pointer-events: none;\n}\n\n.nav-btn:focus-visible {\n outline: 2px solid var(--ty-calendar-nav-focus-outline, var(--ty-calendar-accent, var(--ty-color-primary)));\n outline-offset: 2px;\n}\n\n.nav-btn svg {\n width: var(--nav-btn-icon-size);\n height: var(--nav-btn-icon-size);\n display: block;\n}\n\n/* 14px uppercase at every size, not a per-size type scale: the label is the\n widest thing in the bar and the only one whose width depends on the data\n (\"May\" vs \"September\"), so scaling it up on lg was what made the arrows\n shuffle around. Small caps read as chrome, stay legible, and leave the\n centre column comfortably wider than the longest localized month name.\n Ellipsis is a backstop for a narrow --nav-width, not the sizing strategy. */\n.month-year-display {\n font-size: var(--nav-font-size);\n font-weight: 600;\n color: var(--ty-calendar-nav-title-color, var(--ty-color-neutral-strong));\n text-align: center;\n white-space: nowrap;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n";
|
|
@@ -2,7 +2,7 @@ export const calendarNavigationStyles = `
|
|
|
2
2
|
:host([data-size="md"]) {
|
|
3
3
|
--nav-btn-size: 2rem;
|
|
4
4
|
--nav-btn-icon-size: 1.25rem;
|
|
5
|
-
--nav-font-size:
|
|
5
|
+
--nav-font-size: 0.875rem;
|
|
6
6
|
--nav-padding: 0.5rem 0.75rem;
|
|
7
7
|
--nav-default-width: 280px;
|
|
8
8
|
}
|
|
@@ -16,7 +16,7 @@ export const calendarNavigationStyles = `
|
|
|
16
16
|
:host([data-size="lg"]) {
|
|
17
17
|
--nav-btn-size: 2.5rem;
|
|
18
18
|
--nav-btn-icon-size: 1.5rem;
|
|
19
|
-
--nav-font-size:
|
|
19
|
+
--nav-font-size: 0.875rem;
|
|
20
20
|
--nav-padding: 0.625rem 1rem;
|
|
21
21
|
--nav-default-width: 360px;
|
|
22
22
|
}
|
|
@@ -27,7 +27,7 @@ justify-content: space-between;
|
|
|
27
27
|
padding: var(--nav-padding);
|
|
28
28
|
font-family: inherit;
|
|
29
29
|
user-select: none;
|
|
30
|
-
width: var(--nav-width, var(--nav-default-width));
|
|
30
|
+
width: var(--nav-width, var(--ty-calendar-width, var(--nav-default-width)));
|
|
31
31
|
box-sizing: border-box;
|
|
32
32
|
}
|
|
33
33
|
.nav-group {
|
|
@@ -42,6 +42,7 @@ flex: 0 0 auto;
|
|
|
42
42
|
.nav-group-center {
|
|
43
43
|
flex: 1;
|
|
44
44
|
justify-content: center;
|
|
45
|
+
min-width: 0;
|
|
45
46
|
}
|
|
46
47
|
.nav-btn {
|
|
47
48
|
display: inline-flex;
|
|
@@ -86,6 +87,9 @@ font-weight: 600;
|
|
|
86
87
|
color: var(--ty-calendar-nav-title-color, var(--ty-color-neutral-strong));
|
|
87
88
|
text-align: center;
|
|
88
89
|
white-space: nowrap;
|
|
89
|
-
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
letter-spacing: 0.04em;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
text-overflow: ellipsis;
|
|
90
94
|
}
|
|
91
95
|
`;
|
package/lib/styles/calendar.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Calendar Orchestration Styles
|
|
3
3
|
* Simple container for navigation + month display
|
|
4
4
|
*/
|
|
5
|
-
export declare const calendarStyles = "\n.calendar-container {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n box-sizing: border-box;\n font-family: inherit;\n}\n\n@media (max-width: 320px) {\n .calendar-container {\n gap: 0.375rem;\n }\n}\n";
|
|
5
|
+
export declare const calendarStyles = "\n.calendar-container {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n box-sizing: border-box;\n font-family: inherit;\n /* Nav is a fixed width and the grid is fit-content, so in any host wider\n than them (a popup, a stretched cell) the default stretch alignment\n left-pinned both. Centring keeps the block centred in whatever space it\n gets \u2014 without widening the calendar itself to fill it. */\n align-items: center;\n}\n\n@media (max-width: 320px) {\n .calendar-container {\n gap: 0.375rem;\n }\n}\n";
|